initial commit

This commit is contained in:
Slavomir Durej 2025-12-14 18:27:30 +00:00
commit f72cbf9e91
13 changed files with 2009 additions and 0 deletions

38
.gitignore vendored Normal file
View file

@ -0,0 +1,38 @@
# Dependencies
node_modules/
package-lock.json
# Build output
dist/
build/
out/
# OS files
.DS_Store
Thumbs.db
desktop.ini
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Environment
.env
.env.local
.env.*.local
# Electron
*.electron/
# Cache
.cache/
.temp/

89
INSTALL.md Normal file
View file

@ -0,0 +1,89 @@
# Installation Instructions
## For End Users
### Option 1: Download Installer (Recommended)
1. Download `Claude-Usage-Widget-Setup.exe` from releases
2. Run the installer
3. Launch from Start Menu
4. Login when prompted
### Option 2: Build from Source
```bash
# Install Node.js from https://nodejs.org (if not already installed)
# Clone or download this project
cd claude-usage-widget
# Install dependencies
npm install
# Run the widget
npm start
# Or build installer
npm run build:win
```
## First Time Setup
1. **Launch the widget** - A frameless window appears
2. **Click "Login to Claude"** - Browser window opens
3. **Login to Claude.ai** - Use your normal credentials
4. **Widget activates** - Usage data appears automatically
5. **Minimize to tray** - Click the minus icon
## System Requirements
- **OS:** Windows 10 or later (64-bit)
- **RAM:** 200 MB
- **Disk:** 100 MB
- **Internet:** Required for Claude.ai API
## What Gets Installed
- Executable: `%LOCALAPPDATA%\Programs\claude-usage-widget\`
- Settings: `%APPDATA%\claude-usage-widget\` (encrypted)
- Start Menu shortcut
- Desktop shortcut (optional)
## Uninstallation
**Windows:**
1. Settings → Apps → Claude Usage Widget → Uninstall
2. Or run `Uninstall Claude Usage Widget.exe` from install directory
**Manual cleanup:**
```
%APPDATA%\claude-usage-widget\
%LOCALAPPDATA%\Programs\claude-usage-widget\
```
## Troubleshooting Install Issues
### "Windows protected your PC"
1. Click "More info"
2. Click "Run anyway"
3. This is normal for unsigned apps
### Installer won't run
- Ensure you have admin rights
- Disable antivirus temporarily
- Download again (file may be corrupted)
### Can't find after install
- Check Start Menu → All Apps
- Search for "Claude Usage Widget"
- Check Desktop for shortcut
## Security Notes
**Your data stays local** - credentials stored encrypted on your machine
**Open source** - code is available for review
**No telemetry** - no usage data sent anywhere
**Direct API** - only communicates with claude.ai
---
For development setup, see [QUICKSTART.md](QUICKSTART.md)
For full documentation, see [README.md](README.md)

195
QUICKSTART.md Normal file
View file

@ -0,0 +1,195 @@
# Quick Start Guide
## Installation & Development
### 1. Install Dependencies
```bash
cd claude-usage-widget
npm install
```
### 2. Run in Development Mode
```bash
npm start
```
This will:
- Launch the widget with DevTools open
- Enable hot-reload for debugging
- Show console logs
### 3. Test the Application
**First Run:**
1. Widget appears (frameless window)
2. Click "Login to Claude"
3. Browser window opens to claude.ai
4. Login with your credentials
5. Widget automatically captures session
6. Usage data displays
**Features to Test:**
- [ ] Drag widget around screen
- [ ] Refresh button updates data
- [ ] Minimize to system tray
- [ ] Right-click tray icon shows menu
- [ ] Progress bars animate smoothly
- [ ] Timers count down correctly
- [ ] Re-login from tray menu works
### 4. Build for Production
```bash
npm run build:win
```
Output: `dist/Claude-Usage-Widget-Setup.exe`
## Development Tips
### Enable DevTools
Already enabled in dev mode. To disable, edit `main.js`:
```javascript
if (process.env.NODE_ENV === 'development') {
// Comment out this line:
// mainWindow.webContents.openDevTools({ mode: 'detach' });
}
```
### Test Without Building
```bash
npm start
```
### Debug Authentication
Check the console for:
- Cookie capture events
- Organization ID extraction
- API responses
### Change Update Frequency
Edit `src/renderer/app.js`:
```javascript
const UPDATE_INTERVAL = 1 * 60 * 1000; // 1 minute for testing
```
### Mock API Response
For testing UI without API calls, add to `fetchUsageData()`:
```javascript
const mockData = {
five_hour: { utilization: 45.5, resets_at: "2025-12-13T20:00:00Z" },
seven_day: { utilization: 78.2, resets_at: "2025-12-17T07:00:00Z" }
};
updateUI(mockData);
return;
```
## File Structure
```
claude-usage-widget/
├── main.js # Electron main process
├── preload.js # IPC bridge
├── package.json # Dependencies & build config
├── src/
│ └── renderer/
│ ├── index.html # Widget UI
│ ├── styles.css # Styling
│ └── app.js # Frontend logic
└── assets/
├── icon.ico # App icon
└── tray-icon.png # Tray icon
```
## Common Issues
### Port Already in Use
Electron doesn't use ports, so this shouldn't happen.
### White Screen on Launch
Check console for errors. Usually means:
- Missing file paths
- JavaScript errors in app.js
- CSS not loading
### Login Window Not Capturing Session
Check `main.js` - the `did-finish-load` event handler should:
1. Check URL contains 'chat' or 'new'
2. Extract sessionKey cookie
3. Try to get organization ID
### API Returns 401
Session expired. Click "Re-login" from tray menu.
## Adding Features
### Custom Themes
Edit `styles.css` - change gradient colors:
```css
.widget-container {
background: linear-gradient(135deg, #your-color 0%, #another-color 100%);
}
```
### Notification Alerts
Add to `updateUI()` in `app.js`:
```javascript
if (weeklyUtilization >= 90) {
new Notification('Claude Usage Alert', {
body: 'You\'re at 90% of weekly limit!'
});
}
```
### Keyboard Shortcuts
Add to `main.js`:
```javascript
const { globalShortcut } = require('electron');
app.whenReady().then(() => {
globalShortcut.register('CommandOrControl+Shift+C', () => {
if (mainWindow) {
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show();
}
});
});
```
## Debugging
### Console Logs
- Main process: Check terminal where you ran `npm start`
- Renderer process: Check DevTools console (F12)
### Network Requests
DevTools → Network tab shows all API calls
### Storage
Check stored credentials:
```javascript
// In DevTools console:
await window.electronAPI.getCredentials()
```
## Publishing
1. Update version in `package.json`
2. Run `npm run build:win`
3. Test the installer in `dist/`
4. Create GitHub release
5. Upload the `.exe` file
## Next Steps
- [ ] Add app icon (`.ico` file)
- [ ] Add tray icon (16x16 PNG)
- [ ] Test on clean Windows machine
- [ ] Create installer screenshots
- [ ] Write changelog
- [ ] Submit to releases
---
Happy coding! 🚀

188
README.md Normal file
View file

@ -0,0 +1,188 @@
# Claude Usage Widget
A beautiful, standalone Windows desktop widget that displays your Claude.ai usage statistics in real-time.
![Claude Usage Widget](screenshot.png)
## Features
- 🎯 **Real-time Usage Tracking** - Monitor both session and weekly usage limits
- 📊 **Visual Progress Bars** - Clean, gradient progress indicators
- ⏱️ **Countdown Timers** - Circular timers showing time until reset
- 🔄 **Auto-refresh** - Updates every 5 minutes automatically
- 🎨 **Modern UI** - Sleek, draggable widget with dark theme
- 🔒 **Secure** - Encrypted credential storage
- 📍 **Always on Top** - Stays visible across all workspaces
- 💾 **System Tray** - Minimizes to tray for easy access
## Installation
### Download Pre-built Release
1. Download the latest `Claude-Usage-Widget-Setup.exe` from [Releases](releases)
2. Run the installer
3. Launch "Claude Usage Widget" from Start Menu
### Build from Source
**Prerequisites:**
- Node.js 18+ ([Download](https://nodejs.org))
- npm (comes with Node.js)
**Steps:**
```bash
# Clone the repository
git clone https://github.com/yourusername/claude-usage-widget.git
cd claude-usage-widget
# Install dependencies
npm install
# Run in development mode
npm start
# Build installer for Windows
npm run build:win
```
The installer will be created in the `dist/` folder.
## Usage
### First Launch
1. Launch the widget
2. Click "Login to Claude" when prompted
3. A browser window will open - login to your Claude.ai account
4. The widget will automatically capture your session
5. Usage data will start displaying immediately
### Widget Controls
- **Drag** - Click and drag the title bar to move the widget
- **Refresh** - Click the refresh icon to update data immediately
- **Minimize** - Click the minus icon to hide to system tray
- **Close** - Click the X to minimize to tray (doesn't exit)
### System Tray Menu
Right-click the tray icon for:
- Show/Hide widget
- Refresh usage data
- Re-login (if session expires)
- Settings (coming soon)
- Exit application
## Understanding the Display
### Current Session
- **Progress Bar** - Shows usage from 0-100%
- **Timer** - Time remaining until 5-hour session resets
- **Color Coding**:
- Purple: Normal usage (0-74%)
- Orange: High usage (75-89%)
- Red: Critical usage (90-100%)
### Weekly Limit
- **Progress Bar** - Shows weekly usage from 0-100%
- **Timer** - Time remaining until weekly reset (Wednesdays 7:00 AM)
- **Same color coding** as session usage
## Configuration
### Auto-start on Windows Boot
1. Press `Win + R`
2. Type `shell:startup` and press Enter
3. Create a shortcut to the widget executable in this folder
### Custom Refresh Interval
Edit `src/renderer/app.js`:
```javascript
const UPDATE_INTERVAL = 5 * 60 * 1000; // Change to your preference (in milliseconds)
```
## Troubleshooting
### "Login Required" keeps appearing
- Your Claude.ai session may have expired
- Click "Login to Claude" to re-authenticate
- Check that you're logging into the correct account
### Widget not updating
- Check your internet connection
- Click the refresh button manually
- Ensure Claude.ai is accessible in your region
- Try re-logging in from the system tray menu
### Widget position not saving
- Position is reset on each launch
- Future version will include position memory
### Build errors
```bash
# Clear cache and reinstall
rm -rf node_modules package-lock.json
npm install
```
## Privacy & Security
- Your session credentials are stored **locally only** using encrypted storage
- No data is sent to any third-party servers
- The widget only communicates with Claude.ai official API
- Session cookies are stored using Electron's secure storage
## Technical Details
**Built with:**
- Electron 28.0.0
- Pure JavaScript (no framework overhead)
- Native Node.js APIs
- electron-store for secure storage
**API Endpoint:**
```
https://claude.ai/api/organizations/{org_id}/usage
```
**Storage Location:**
```
%APPDATA%/claude-usage-widget/config.json (encrypted)
```
## Roadmap
- [ ] macOS support
- [ ] Linux support
- [ ] Custom themes
- [ ] Notification alerts at usage thresholds
- [ ] Remember window position
- [ ] Settings panel
- [ ] Usage history graphs
- [ ] Multiple account support
- [ ] Keyboard shortcuts
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
## License
MIT License - feel free to use and modify as needed.
## Disclaimer
This is an unofficial tool and is not affiliated with or endorsed by Anthropic. Use at your own discretion.
## Support
If you encounter issues:
1. Check the [Issues](issues) page
2. Create a new issue with details about your problem
3. Include your OS version and any error messages
---
Made with ❤️ for the Claude.ai community

75
assets/ICONS-NEEDED.md Normal file
View file

@ -0,0 +1,75 @@
# Icon Assets Required
## What You Need
### 1. App Icon (icon.ico)
**Location:** `assets/icon.ico`
**Requirements:**
- Format: .ico file
- Sizes: 16x16, 32x32, 48x48, 256x256 (multi-resolution)
- Design: Claude-themed logo or "C" symbol
- Style: Should match the purple/blue gradient theme
**Quick Creation:**
- Use an online tool like https://favicon.io or https://convertio.co
- Upload a PNG (256x256) of your design
- Convert to .ico with multiple sizes
### 2. Tray Icon (tray-icon.png)
**Location:** `assets/tray-icon.png`
**Requirements:**
- Format: PNG with transparency
- Size: 16x16 or 32x32 pixels
- Design: Simple, recognizable at small size
- Style: Monochrome or minimal color (for visibility on light/dark taskbars)
**Tips:**
- Keep it simple - just a "C" or claude symbol
- Use white/light color for dark taskbars
- Test on both dark and light Windows themes
## Design Suggestions
### Color Palette (from widget)
- Primary Purple: #8b5cf6
- Light Purple: #a78bfa
- Blue: #3b82f6
- Background: #1e1e2e
### Icon Ideas
1. **Letter "C"** in gradient circle
2. **Chat bubble** with "C" inside
3. **Brain/AI symbol** in purple
4. **Minimalist robot** head
5. **Abstract neural network** pattern
## Temporary Solution
Until you create custom icons, you can use:
- Placeholder icon.ico from any Electron template
- Or remove icon references from `main.js` temporarily
```javascript
// In main.js, comment out:
// tray = new Tray(path.join(__dirname, 'assets/tray-icon.png'));
```
## Online Tools
- **Icon Generator:** https://www.icongenerator.com
- **ICO Converter:** https://convertio.co/png-ico/
- **Free Icons:** https://icons8.com or https://www.flaticon.com
## Example Commands (if you have ImageMagick)
```bash
# Create .ico from PNG
convert icon-256.png -define icon:auto-resize=256,128,96,64,48,32,16 icon.ico
# Create tray icon
convert logo.png -resize 16x16 tray-icon.png
```
---
**Note:** The app will still work without icons, but they improve the user experience significantly!

20
assets/claude-logo.svg Normal file
View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="katman_1" data-name="katman 1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 841.89 595.28">
<defs>
<style>
.cls-1 {
fill: #0f0f0d;
}
.cls-1, .cls-2 {
stroke-width: 0px;
}
.cls-2 {
fill: #d97757;
}
</style>
</defs>
<path class="cls-2" d="M105.01,322.07l29.14-16.35.49-1.42-.49-.79h-1.42l-4.87-.3-16.65-.45-14.44-.6-13.99-.75-3.52-.75-3.3-4.35.34-2.17,2.96-1.99,4.24.37,9.37.64,14.06.97,10.2.6,15.11,1.57h2.4l.34-.97-.82-.6-.64-.6-14.55-9.86-15.75-10.42-8.25-6-4.46-3.04-2.25-2.85-.97-6.22,4.05-4.46,5.44.37,1.39.37,5.51,4.24,11.77,9.11,15.37,11.32,2.25,1.87.9-.64.11-.45-1.01-1.69-8.36-15.11-8.92-15.37-3.97-6.37-1.05-3.82c-.37-1.57-.64-2.89-.64-4.5l4.61-6.26,2.55-.82,6.15.82,2.59,2.25,3.82,8.74,6.19,13.76,9.6,18.71,2.81,5.55,1.5,5.14.56,1.57h.97v-.9l.79-10.54,1.46-12.94,1.42-16.65.49-4.69,2.32-5.62,4.61-3.04,3.6,1.72,2.96,4.24-.41,2.74-1.76,11.44-3.45,17.92-2.25,12h1.31l1.5-1.5,6.07-8.06,10.2-12.75,4.5-5.06,5.25-5.59,3.37-2.66h6.37l4.69,6.97-2.1,7.2-6.56,8.32-5.44,7.05-7.8,10.5-4.87,8.4.45.67,1.16-.11,17.62-3.75,9.52-1.72,11.36-1.95,5.14,2.4.56,2.44-2.02,4.99-12.15,3-14.25,2.85-21.22,5.02-.26.19.3.37,9.56.9,4.09.22h10.01l18.64,1.39,4.87,3.22,2.92,3.94-.49,3-7.5,3.82-10.12-2.4-23.62-5.62-8.1-2.02h-1.12v.67l6.75,6.6,12.37,11.17,15.49,14.4.79,3.56-1.99,2.81-2.1-.3-13.61-10.24-5.25-4.61-11.89-10.01h-.79v1.05l2.74,4.01,14.47,21.75.75,6.67-1.05,2.17-3.75,1.31-4.12-.75-8.47-11.89-8.74-13.39-7.05-12-.86.49-4.16,44.81-1.95,2.29-4.5,1.72-3.75-2.85-1.99-4.61,1.99-9.11,2.4-11.89,1.95-9.45,1.76-11.74,1.05-3.9-.07-.26-.86.11-8.85,12.15-13.46,18.19-10.65,11.4-2.55,1.01-4.42-2.29.41-4.09,2.47-3.64,14.74-18.75,8.89-11.62,5.74-6.71-.04-.97h-.34l-39.15,25.42-6.97.9-3-2.81.37-4.61,1.42-1.5,11.77-8.1-.04.04Z" shape-rendering="optimizeQuality"/>
<path class="cls-1" d="M317.73,349.33c-18.82,0-31.69-10.5-37.76-26.66-3.17-8.42-4.74-17.36-4.61-26.36,0-27.11,12.15-45.94,39-45.94,18.04,0,29.17,7.87,35.51,26.66h7.72l-1.05-25.91c-10.8-6.97-24.3-10.5-40.72-10.5-23.14,0-42.82,10.35-53.77,29.02-5.66,9.86-8.53,21.07-8.32,32.44,0,20.74,9.79,39.11,28.16,49.31,10.06,5.37,21.34,8.04,32.74,7.72,17.92,0,32.14-3.41,44.74-9.37l3.26-28.57h-7.87c-4.72,13.05-10.35,20.89-19.69,25.05-4.57,2.06-10.35,3.11-17.32,3.11ZM398.91,250.37l.75-12.75h-5.32l-23.7,7.12v3.86l10.5,4.87v89.17c0,6.07-3.11,7.42-11.25,8.44v6.52h40.31v-6.52c-8.17-1.01-11.25-2.36-11.25-8.44v-92.24l-.04-.04ZM559.22,359.12h3.11l27.26-5.17v-6.67l-3.82-.3c-6.37-.6-8.02-1.91-8.02-7.12v-47.55l.75-15.26h-4.31l-25.76,3.71v6.52l2.51.45c6.97,1.01,9.04,2.96,9.04,7.84v42.37c-6.67,5.17-13.05,8.44-20.62,8.44-8.4,0-13.61-4.27-13.61-14.25v-39.79l.75-15.26h-4.42l-25.8,3.71v6.52l2.66.45c6.97,1.01,9.04,2.96,9.04,7.84v39.11c0,16.57,9.37,24.45,24.3,24.45,11.4,0,20.74-6.07,27.75-14.51l-.75,14.51-.04-.04ZM484.3,306.36c0-21.19-11.25-29.32-31.57-29.32-17.92,0-30.94,7.42-30.94,19.72,0,3.67,1.31,6.49,3.97,8.44l13.65-1.8c-.6-4.12-.9-6.64-.9-7.69,0-6.97,3.71-10.5,11.25-10.5,11.14,0,16.76,7.84,16.76,20.44v4.12l-28.12,8.44c-9.37,2.55-14.7,4.76-18.26,9.94-1.89,3.17-2.8,6.82-2.62,10.5,0,12,8.25,20.47,22.35,20.47,10.2,0,19.24-4.61,27.11-13.35,2.81,8.74,7.12,13.35,14.81,13.35,6.22,0,11.85-2.51,16.87-7.42l-1.5-5.17c-2.17.6-4.27.9-6.49.9-4.31,0-6.37-3.41-6.37-10.09v-30.97ZM448.3,347.12c-7.69,0-12.45-4.46-12.45-12.3,0-5.32,2.51-8.44,7.87-10.24l22.8-7.24v21.9c-7.27,5.51-11.55,7.87-18.22,7.87ZM685.66,353.94v-6.67l-3.86-.3c-6.37-.6-7.99-1.91-7.99-7.12v-89.47l.75-12.75h-5.36l-23.7,7.12v3.86l10.5,4.87v29.32c-5.91-4.05-12.98-6.08-20.14-5.77-23.55,0-41.92,17.92-41.92,44.74,0,22.09,13.2,37.35,34.95,37.35,11.25,0,21.04-5.47,27.11-13.95l-.75,13.95h3.15l27.26-5.17h0ZM636.31,285.92c11.25,0,19.69,6.52,19.69,18.52v33.75c-5.18,5.16-12.23,8-19.54,7.87-16.12,0-24.3-12.75-24.3-29.77,0-19.12,9.34-30.37,24.15-30.37ZM743.3,302.8c-2.1-9.9-8.17-15.52-16.61-15.52-12.6,0-21.34,9.49-21.34,23.1,0,20.14,10.65,33.19,27.86,33.19,11.48-.12,22.04-6.33,27.71-16.31l5.02,1.35c-2.25,17.47-18.07,30.52-37.5,30.52-22.8,0-38.51-16.87-38.51-40.87s17.06-41.21,39.86-41.21c17.02,0,29.02,10.24,32.89,28.01l-59.4,18.22v-8.02l40.01-12.41v-.04Z" shape-rendering="optimizeQuality"/>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
assets/tray-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 KiB

362
main.js Normal file
View file

@ -0,0 +1,362 @@
const { app, BrowserWindow, ipcMain, Tray, Menu, session, shell } = require('electron');
const path = require('path');
const Store = require('electron-store');
const axios = require('axios');
const store = new Store({
encryptionKey: 'claude-widget-secure-key-2024'
});
let mainWindow = null;
let loginWindow = null;
let tray = null;
// Window configuration
const WIDGET_WIDTH = 480;
const WIDGET_HEIGHT = 140;
function createMainWindow() {
mainWindow = new BrowserWindow({
width: WIDGET_WIDTH,
height: WIDGET_HEIGHT,
frame: false,
transparent: true,
alwaysOnTop: true,
resizable: false,
skipTaskbar: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
}
});
mainWindow.loadFile('src/renderer/index.html');
// Make window draggable
mainWindow.setAlwaysOnTop(true, 'floating');
mainWindow.setVisibleOnAllWorkspaces(true);
mainWindow.on('closed', () => {
mainWindow = null;
});
// Development tools
if (process.env.NODE_ENV === 'development') {
mainWindow.webContents.openDevTools({ mode: 'detach' });
}
}
function createLoginWindow() {
loginWindow = new BrowserWindow({
width: 800,
height: 700,
parent: mainWindow,
modal: true,
webPreferences: {
nodeIntegration: false,
contextIsolation: true
}
});
loginWindow.loadURL('https://claude.ai');
let loginCheckInterval = null;
let hasLoggedIn = false;
// Function to check login status
async function checkLoginStatus() {
if (hasLoggedIn || !loginWindow) return;
try {
const cookies = await session.defaultSession.cookies.get({
url: 'https://claude.ai',
name: 'sessionKey'
});
if (cookies.length > 0) {
const sessionKey = cookies[0].value;
console.log('Session key found, attempting to get org ID...');
// Fetch org ID from API
let orgId = null;
try {
const response = await axios.get('https://claude.ai/api/organizations', {
headers: {
'Cookie': `sessionKey=${sessionKey}`,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
});
if (response.data && Array.isArray(response.data) && response.data.length > 0) {
orgId = response.data[0].uuid || response.data[0].id;
console.log('Org ID fetched from API:', orgId);
}
} catch (err) {
console.log('API not ready yet:', err.message);
}
if (sessionKey && orgId) {
hasLoggedIn = true;
if (loginCheckInterval) {
clearInterval(loginCheckInterval);
loginCheckInterval = null;
}
console.log('Sending login-success to main window...');
store.set('sessionKey', sessionKey);
store.set('organizationId', orgId);
if (mainWindow) {
mainWindow.webContents.send('login-success', { sessionKey, organizationId: orgId });
console.log('login-success sent');
} else {
console.error('mainWindow is null, cannot send login-success');
}
loginWindow.close();
}
}
} catch (error) {
console.error('Error in login check:', error);
}
}
// Check on page load
loginWindow.webContents.on('did-finish-load', async () => {
const url = loginWindow.webContents.getURL();
console.log('Login page loaded:', url);
if (url.includes('claude.ai')) {
await checkLoginStatus();
}
});
// Also check on navigation (URL changes)
loginWindow.webContents.on('did-navigate', async (event, url) => {
console.log('Navigated to:', url);
if (url.includes('claude.ai')) {
await checkLoginStatus();
}
});
// Poll periodically in case the session becomes ready without a page navigation
loginCheckInterval = setInterval(async () => {
if (!hasLoggedIn && loginWindow) {
await checkLoginStatus();
} else if (loginCheckInterval) {
clearInterval(loginCheckInterval);
loginCheckInterval = null;
}
}, 2000);
loginWindow.on('closed', () => {
if (loginCheckInterval) {
clearInterval(loginCheckInterval);
loginCheckInterval = null;
}
loginWindow = null;
});
}
function createTray() {
try {
tray = new Tray(path.join(__dirname, 'assets/tray-icon.png'));
const contextMenu = Menu.buildFromTemplate([
{
label: 'Show Widget',
click: () => {
if (mainWindow) {
mainWindow.show();
} else {
createMainWindow();
}
}
},
{
label: 'Refresh',
click: () => {
if (mainWindow) {
mainWindow.webContents.send('refresh-usage');
}
}
},
{ type: 'separator' },
{
label: 'Settings',
click: () => {
// TODO: Open settings window
}
},
{
label: 'Re-login',
click: () => {
store.delete('sessionKey');
store.delete('organizationId');
createLoginWindow();
}
},
{ type: 'separator' },
{
label: 'Exit',
click: () => {
app.quit();
}
}
]);
tray.setToolTip('Claude Usage Widget');
tray.setContextMenu(contextMenu);
tray.on('click', () => {
if (mainWindow) {
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show();
}
});
} catch (error) {
console.error('Failed to create tray:', error);
}
}
// IPC Handlers
ipcMain.handle('get-credentials', () => {
return {
sessionKey: store.get('sessionKey'),
organizationId: store.get('organizationId')
};
});
ipcMain.handle('save-credentials', (event, { sessionKey, organizationId }) => {
store.set('sessionKey', sessionKey);
if (organizationId) {
store.set('organizationId', organizationId);
}
return true;
});
ipcMain.handle('delete-credentials', async () => {
store.delete('sessionKey');
store.delete('organizationId');
// Clear the session cookie to ensure actual logout
try {
await session.defaultSession.cookies.remove('https://claude.ai', 'sessionKey');
// Also try checking for other auth cookies or clear storage if needed
// await session.defaultSession.clearStorageData({ storages: ['cookies'] });
} catch (error) {
console.error('Failed to clear cookies:', error);
}
return true;
});
ipcMain.on('open-login', () => {
createLoginWindow();
});
ipcMain.on('minimize-window', () => {
if (mainWindow) mainWindow.hide();
});
ipcMain.on('close-window', () => {
app.quit();
});
ipcMain.handle('get-window-position', () => {
if (mainWindow) {
return mainWindow.getBounds();
}
return null;
});
ipcMain.handle('set-window-position', (event, { x, y }) => {
if (mainWindow) {
mainWindow.setPosition(x, y);
return true;
}
return false;
});
ipcMain.on('open-external', (event, url) => {
shell.openExternal(url);
});
ipcMain.handle('fetch-usage-data', async () => {
console.log('[Main] fetch-usage-data handler called');
const sessionKey = store.get('sessionKey');
const organizationId = store.get('organizationId');
console.log('[Main] Credentials:', {
hasSessionKey: !!sessionKey,
organizationId
});
if (!sessionKey || !organizationId) {
throw new Error('Missing credentials');
}
try {
console.log('[Main] Making API request to:', `https://claude.ai/api/organizations/${organizationId}/usage`);
const response = await axios.get(
`https://claude.ai/api/organizations/${organizationId}/usage`,
{
headers: {
'Cookie': `sessionKey=${sessionKey}`,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
}
);
console.log('[Main] API request successful, status:', response.status);
return response.data;
} catch (error) {
console.error('[Main] API request failed:', error.message);
if (error.response) {
console.error('[Main] Response status:', error.response.status);
if (error.response.status === 401 || error.response.status === 403) {
throw new Error('Unauthorized');
}
}
throw error;
}
});
// App lifecycle
app.whenReady().then(() => {
createMainWindow();
createTray();
// Check if we have credentials
// const hasCredentials = store.get('sessionKey') && store.get('organizationId');
// if (!hasCredentials) {
// setTimeout(() => {
// createLoginWindow();
// }, 1000);
// }
});
app.on('window-all-closed', () => {
// Don't quit on macOS
if (process.platform !== 'darwin') {
// Keep running in tray
}
});
app.on('activate', () => {
if (mainWindow === null) {
createMainWindow();
}
});
// Prevent multiple instances
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
} else {
app.on('second-instance', () => {
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
}
});
}

50
package.json Normal file
View file

@ -0,0 +1,50 @@
{
"name": "claude-usage-widget",
"version": "1.0.0",
"description": "Desktop widget for Claude.ai usage monitoring",
"main": "main.js",
"scripts": {
"start": "electron .",
"build": "electron-builder",
"build:win": "electron-builder --win",
"dev": "cross-env NODE_ENV=development electron ."
},
"keywords": [
"claude",
"usage",
"widget",
"electron"
],
"author": "Your Name",
"license": "MIT",
"devDependencies": {
"electron": "^28.0.0",
"electron-builder": "^24.9.1",
"cross-env": "^7.0.3"
},
"dependencies": {
"electron-store": "^8.1.0",
"axios": "^1.6.2"
},
"build": {
"appId": "com.claudeusage.widget",
"productName": "Claude Usage Widget",
"win": {
"target": [
"nsis"
],
"icon": "assets/icon.ico"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true
},
"files": [
"**/*",
"!**/*.ts",
"!*.md"
]
}
}

31
preload.js Normal file
View file

@ -0,0 +1,31 @@
const { contextBridge, ipcRenderer } = require('electron');
// Expose protected methods that allow the renderer process to use
// the ipcRenderer without exposing the entire object
contextBridge.exposeInMainWorld('electronAPI', {
// Credentials management
getCredentials: () => ipcRenderer.invoke('get-credentials'),
saveCredentials: (credentials) => ipcRenderer.invoke('save-credentials', credentials),
deleteCredentials: () => ipcRenderer.invoke('delete-credentials'),
// Window controls
minimizeWindow: () => ipcRenderer.send('minimize-window'),
closeWindow: () => ipcRenderer.send('close-window'),
openLogin: () => ipcRenderer.send('open-login'),
// Window position
getWindowPosition: () => ipcRenderer.invoke('get-window-position'),
setWindowPosition: (position) => ipcRenderer.invoke('set-window-position', position),
// Event listeners
onLoginSuccess: (callback) => {
ipcRenderer.on('login-success', (event, data) => callback(data));
},
onRefreshUsage: (callback) => {
ipcRenderer.on('refresh-usage', () => callback());
},
// API
fetchUsageData: () => ipcRenderer.invoke('fetch-usage-data'),
openExternal: (url) => ipcRenderer.send('open-external', url)
});

355
src/renderer/app.js Normal file
View file

@ -0,0 +1,355 @@
// Application state
let credentials = null;
let updateInterval = null;
let countdownInterval = null;
let latestUsageData = null;
const UPDATE_INTERVAL = 5 * 60 * 1000; // 5 minutes
// DOM elements
const elements = {
loadingContainer: document.getElementById('loadingContainer'),
loginContainer: document.getElementById('loginContainer'),
mainContent: document.getElementById('mainContent'),
loginBtn: document.getElementById('loginBtn'),
refreshBtn: document.getElementById('refreshBtn'),
minimizeBtn: document.getElementById('minimizeBtn'),
closeBtn: document.getElementById('closeBtn'),
sessionPercentage: document.getElementById('sessionPercentage'),
sessionProgress: document.getElementById('sessionProgress'),
sessionTimer: document.getElementById('sessionTimer'),
sessionTimeText: document.getElementById('sessionTimeText'),
weeklyPercentage: document.getElementById('weeklyPercentage'),
weeklyProgress: document.getElementById('weeklyProgress'),
weeklyTimer: document.getElementById('weeklyTimer'),
weeklyTimeText: document.getElementById('weeklyTimeText'),
settingsBtn: document.getElementById('settingsBtn'),
settingsOverlay: document.getElementById('settingsOverlay'),
closeSettingsBtn: document.getElementById('closeSettingsBtn'),
logoutBtn: document.getElementById('logoutBtn'),
coffeeBtn: document.getElementById('coffeeBtn')
};
// Initialize
async function init() {
setupEventListeners();
credentials = await window.electronAPI.getCredentials();
if (credentials.sessionKey && credentials.organizationId) {
showMainContent();
await fetchUsageData();
startAutoUpdate();
} else {
showLoginRequired();
}
}
// Event Listeners
function setupEventListeners() {
elements.loginBtn.addEventListener('click', () => {
window.electronAPI.openLogin();
});
elements.refreshBtn.addEventListener('click', async () => {
console.log('Refresh button clicked');
elements.refreshBtn.classList.add('spinning');
await fetchUsageData();
elements.refreshBtn.classList.remove('spinning');
});
elements.minimizeBtn.addEventListener('click', () => {
window.electronAPI.minimizeWindow();
});
elements.closeBtn.addEventListener('click', () => {
window.electronAPI.closeWindow(); // Exit application completely
});
// Settings calls
elements.settingsBtn.addEventListener('click', () => {
elements.settingsOverlay.style.display = 'flex';
});
elements.closeSettingsBtn.addEventListener('click', () => {
elements.settingsOverlay.style.display = 'none';
});
elements.logoutBtn.addEventListener('click', async () => {
await window.electronAPI.deleteCredentials();
elements.settingsOverlay.style.display = 'none';
showLoginRequired();
window.electronAPI.openLogin();
});
elements.coffeeBtn.addEventListener('click', () => {
window.electronAPI.openExternal('https://paypal.me/SlavomirDurej?country.x=GB&locale.x=en_GB');
});
// Listen for login success
window.electronAPI.onLoginSuccess(async (data) => {
console.log('Renderer received login-success event', data);
credentials = data;
await window.electronAPI.saveCredentials(data);
console.log('Credentials saved, showing main content');
showMainContent();
await fetchUsageData();
startAutoUpdate();
});
// Listen for refresh requests from tray
window.electronAPI.onRefreshUsage(async () => {
await fetchUsageData();
});
}
// Fetch usage data from Claude API
async function fetchUsageData() {
console.log('fetchUsageData called', { credentials });
if (!credentials.sessionKey || !credentials.organizationId) {
console.log('Missing credentials, showing login');
showLoginRequired();
return;
}
try {
console.log('Calling electronAPI.fetchUsageData...');
const data = await window.electronAPI.fetchUsageData();
console.log('Received usage data:', data);
updateUI(data);
} catch (error) {
console.error('Error fetching usage data:', error);
if (error.message.includes('Unauthorized')) {
await window.electronAPI.deleteCredentials();
showLoginRequired();
} else {
showError('Failed to fetch usage data');
}
}
}
// Update UI with usage data
// Update UI with usage data
function updateUI(data) {
latestUsageData = data;
refreshTimers();
startCountdown();
// Update timestamp
// Update timestamp
// const now = new Date();
// elements.lastUpdate.textContent = `Updated ${now.toLocaleTimeString()}`;
}
// Track if we've already triggered a refresh for expired timers
let sessionResetTriggered = false;
let weeklyResetTriggered = false;
function refreshTimers() {
if (!latestUsageData) return;
// Session data
const sessionUtilization = latestUsageData.five_hour?.utilization || 0;
const sessionResetsAt = latestUsageData.five_hour?.resets_at;
// Check if session timer has expired and we need to refresh
if (sessionResetsAt) {
const sessionDiff = new Date(sessionResetsAt) - new Date();
if (sessionDiff <= 0 && !sessionResetTriggered) {
sessionResetTriggered = true;
console.log('Session timer expired, triggering refresh...');
// Wait a few seconds for the server to update, then refresh
setTimeout(() => {
fetchUsageData();
}, 3000);
} else if (sessionDiff > 0) {
sessionResetTriggered = false; // Reset flag when timer is active again
}
}
updateProgressBar(
elements.sessionProgress,
elements.sessionPercentage,
sessionUtilization
);
updateTimer(
elements.sessionTimer,
elements.sessionTimeText,
sessionResetsAt,
5 * 60 // 5 hours in minutes
);
// Weekly data
const weeklyUtilization = latestUsageData.seven_day?.utilization || 0;
const weeklyResetsAt = latestUsageData.seven_day?.resets_at;
// Check if weekly timer has expired and we need to refresh
if (weeklyResetsAt) {
const weeklyDiff = new Date(weeklyResetsAt) - new Date();
if (weeklyDiff <= 0 && !weeklyResetTriggered) {
weeklyResetTriggered = true;
console.log('Weekly timer expired, triggering refresh...');
setTimeout(() => {
fetchUsageData();
}, 3000);
} else if (weeklyDiff > 0) {
weeklyResetTriggered = false;
}
}
updateProgressBar(
elements.weeklyProgress,
elements.weeklyPercentage,
weeklyUtilization,
true
);
updateTimer(
elements.weeklyTimer,
elements.weeklyTimeText,
weeklyResetsAt,
7 * 24 * 60 // 7 days in minutes
);
}
function startCountdown() {
if (countdownInterval) clearInterval(countdownInterval);
countdownInterval = setInterval(() => {
refreshTimers();
}, 1000);
}
// Update progress bar
function updateProgressBar(progressElement, percentageElement, value, isWeekly = false) {
const percentage = Math.min(Math.max(value, 0), 100);
progressElement.style.width = `${percentage}%`;
percentageElement.textContent = `${Math.round(percentage)}%`;
// Update color based on usage level
progressElement.classList.remove('warning', 'danger');
if (percentage >= 90) {
progressElement.classList.add('danger');
} else if (percentage >= 75) {
progressElement.classList.add('warning');
}
}
// Update circular timer
function updateTimer(timerElement, textElement, resetsAt, totalMinutes) {
if (!resetsAt) {
textElement.textContent = '--:--';
timerElement.style.strokeDashoffset = 63;
return;
}
const resetDate = new Date(resetsAt);
const now = new Date();
const diff = resetDate - now;
if (diff <= 0) {
textElement.textContent = 'Resetting...';
timerElement.style.strokeDashoffset = 0;
return;
}
// Calculate remaining time
const hours = Math.floor(diff / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
// const seconds = Math.floor((diff % (1000 * 60)) / 1000); // Optional seconds
// Format time display
if (hours >= 24) {
const days = Math.floor(hours / 24);
const remainingHours = hours % 24;
textElement.textContent = `${days}d ${remainingHours}h`;
} else if (hours > 0) {
textElement.textContent = `${hours}h ${minutes}m`;
} else {
textElement.textContent = `${minutes}m`;
}
// Calculate progress (elapsed percentage)
const totalMs = totalMinutes * 60 * 1000;
const elapsedMs = totalMs - diff;
const elapsedPercentage = (elapsedMs / totalMs) * 100;
// Update circle (63 is ~2*pi*10)
const circumference = 63;
const offset = circumference - (elapsedPercentage / 100) * circumference;
timerElement.style.strokeDashoffset = offset;
// Update color based on remaining time
timerElement.classList.remove('warning', 'danger');
if (elapsedPercentage >= 90) {
timerElement.classList.add('danger');
} else if (elapsedPercentage >= 75) {
timerElement.classList.add('warning');
}
}
// UI State Management
function showLoading() {
elements.loadingContainer.style.display = 'block';
elements.loginContainer.style.display = 'none';
elements.mainContent.style.display = 'none';
}
function showLoginRequired() {
elements.loadingContainer.style.display = 'none';
elements.loginContainer.style.display = 'flex'; // Use flex to preserve centering
elements.mainContent.style.display = 'none';
stopAutoUpdate();
}
function showMainContent() {
elements.loadingContainer.style.display = 'none';
elements.loginContainer.style.display = 'none';
elements.mainContent.style.display = 'block';
}
function showError(message) {
// TODO: Implement error notification
console.error(message);
}
// Auto-update management
function startAutoUpdate() {
stopAutoUpdate();
updateInterval = setInterval(() => {
fetchUsageData();
}, UPDATE_INTERVAL);
}
function stopAutoUpdate() {
if (updateInterval) {
clearInterval(updateInterval);
updateInterval = null;
}
}
// Add spinning animation for refresh button
const style = document.createElement('style');
style.textContent = `
@keyframes spin-refresh {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.refresh-btn.spinning svg {
animation: spin-refresh 1s linear;
}
`;
document.head.appendChild(style);
// Start the application
init();
// Cleanup on unload
window.addEventListener('beforeunload', () => {
stopAutoUpdate();
if (countdownInterval) clearInterval(countdownInterval);
});

126
src/renderer/index.html Normal file
View file

@ -0,0 +1,126 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Claude Usage Widget</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Libre+Baskerville:wght@400;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div id="app">
<div class="widget-container" id="widgetContainer">
<!-- Title Bar -->
<div class="title-bar" id="titleBar">
<div class="title">Claude Usage</div>
<div class="controls">
<button class="control-btn settings-btn" id="settingsBtn" title="Settings">⚙️</button>
<button class="control-btn refresh-btn" id="refreshBtn" title="Refresh">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2">
<path d="M23 4v6h-6M1 20v-6h6" />
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" />
</svg>
</button>
<button class="control-btn minimize-btn" id="minimizeBtn" title="Minimise to System Tray"></button>
<button class="control-btn close-btn" id="closeBtn" title="Exit Application">×</button>
</div>
</div>
<!-- Loading State -->
<div class="loading-container" id="loadingContainer">
<div class="spinner"></div>
<p>Loading usage data...</p>
</div>
<!-- Login Required State -->
<div class="login-container" id="loginContainer" style="display: none;">
<div class="login-content">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4" />
<polyline points="10 17 15 12 10 7" />
<line x1="15" y1="12" x2="3" y2="12" />
</svg>
<div class="login-text-group">
<h3>Login Required</h3>
<p>Log in to view usage stats</p>
</div>
<button class="login-btn" id="loginBtn">Log In</button>
</div>
</div>
<!-- Main Content -->
<div class="content" id="mainContent" style="display: none;">
<!-- Session Usage -->
<div class="usage-section">
<span class="usage-label">Current Session</span>
<div class="progress-bar">
<div class="progress-fill" id="sessionProgress" style="width: 0%"></div>
</div>
<span class="usage-percentage" id="sessionPercentage">0%</span>
<div class="timer-container">
<div class="timer-text" id="sessionTimeText">--:--</div>
<svg class="mini-timer" width="24" height="24" viewBox="0 0 24 24">
<circle class="timer-bg" cx="12" cy="12" r="10" />
<circle class="timer-progress" id="sessionTimer" cx="12" cy="12" r="10"
style="stroke-dasharray: 63; stroke-dashoffset: 63" />
</svg>
</div>
</div>
<!-- Weekly Usage -->
<div class="usage-section">
<span class="usage-label">Weekly Limit</span>
<div class="progress-bar">
<div class="progress-fill weekly" id="weeklyProgress" style="width: 0%"></div>
</div>
<span class="usage-percentage" id="weeklyPercentage">0%</span>
<div class="timer-container">
<div class="timer-text" id="weeklyTimeText">--:--</div>
<svg class="mini-timer" width="24" height="24" viewBox="0 0 24 24">
<circle class="timer-bg" cx="12" cy="12" r="10" />
<circle class="timer-progress weekly" id="weeklyTimer" cx="12" cy="12" r="10"
style="stroke-dasharray: 63; stroke-dashoffset: 63" />
</svg>
</div>
</div>
<!-- Hidden Footer element (removed) -->
<span id="lastUpdate" style="display: none;"></span>
</div>
<!-- Settings Overlay -->
<div class="settings-overlay" id="settingsOverlay" style="display: none;">
<div class="settings-content">
<button class="icon-close-settings-btn" id="closeSettingsBtn" title="Close">×</button>
<p class="disclaimer">
<strong>Disclaimer:</strong> Unofficial tool not affiliated with Anthropic. Use at your own discretion.
</p>
<button class="coffee-btn" id="coffeeBtn">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M17 8h1a4 4 0 1 1 0 8h-1"/>
<path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z"/>
<line x1="6" y1="2" x2="6" y2="4"/>
<line x1="10" y1="2" x2="10" y2="4"/>
<line x1="14" y1="2" x2="14" y2="4"/>
</svg>
If you find this useful, buy me a coffee!
</button>
<div class="settings-actions">
<button class="logout-btn" id="logoutBtn">Log Out</button>
</div>
</div>
</div>
</div>
</div>
<script src="app.js"></script>
</body>
</html>

480
src/renderer/styles.css Normal file
View file

@ -0,0 +1,480 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
outline: none;
/* Remove default focus outline */
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
overflow: hidden;
background: transparent;
-webkit-app-region: no-drag;
}
#app {
width: 100%;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.widget-container {
width: 100vw;
height: 100vh;
background: linear-gradient(135deg, #1e1e2e 0%, #2a2a3e 100%);
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
flex-direction: column;
}
/* Title Bar */
.title-bar {
-webkit-app-region: drag;
background: rgba(0, 0, 0, 0.3);
padding: 8px 12px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
height: 36px;
}
.title {
color: #e0e0e0;
font-family: 'Libre Baskerville', serif;
font-size: 14px;
font-weight: 400;
letter-spacing: 0.5px;
}
.controls {
-webkit-app-region: no-drag;
display: flex;
gap: 8px;
margin-right: -10px;
}
.control-btn {
width: 28px;
height: 28px;
border: none;
background: rgba(255, 255, 255, 0.05);
color: #a0a0a0;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
transition: all 0.2s ease;
}
.control-btn:hover {
background: rgba(255, 255, 255, 0.1);
color: #ffffff;
}
.refresh-btn svg {
display: block;
}
.close-btn:hover {
background: #e74c3c;
color: white;
}
/* Loading State */
.loading-container {
padding: 60px 20px;
text-align: center;
color: #a0a0a0;
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(255, 255, 255, 0.1);
border-top-color: #8b5cf6;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 16px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Login State */
.login-container {
padding: 0 16px;
flex: 1;
/* Fill remaining vertical space */
display: flex;
align-items: center;
justify-content: center;
}
.login-content {
display: flex;
align-items: center;
gap: 16px;
text-align: left;
width: 100%;
justify-content: space-between;
}
.login-content svg {
color: #8b5cf6;
margin-bottom: 0;
width: 32px;
height: 32px;
flex-shrink: 0;
}
.login-text-group {
display: flex;
flex-direction: column;
justify-content: center;
margin-right: auto;
/* Push button to right if space permits */
padding-left: 12px;
}
.login-content h3 {
color: #e0e0e0;
font-size: 14px;
margin-bottom: 2px;
}
.login-content p {
color: #a0a0a0;
font-size: 11px;
margin-bottom: 0;
}
.login-btn {
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
flex-shrink: 0;
}
.login-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
}
/* Main Content */
.content {
padding: 20px;
}
.usage-section {
display: grid;
grid-template-columns: 125px 1fr 45px 100px;
/* Fixed width labels for alignment */
align-items: center;
gap: 12px;
margin-bottom: 2px;
padding: 0 16px;
height: 32px;
}
.usage-section:last-of-type {
margin-bottom: 0;
}
/* usage-header was removed */
/* We need to re-target the children of usage-header because display:contents strips the wrapper box */
/* But the HTML structure is:
.usage-section > .usage-header > span, span
.usage-section > .progress-bar
.usage-section > .timer-container
*/
/* Actually, let's just make usage-header NOT display contents, but hide it or remove it in HTML?
Better: make usage-section a Flex Row?
Or keeps Grid:
Column 1: Label
Column 2: Progress (flexible)
Column 3: Percentage
Column 4: Timer
*/
.usage-label {
color: #a0a0a0;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
white-space: nowrap;
}
.usage-percentage {
color: #e0e0e0;
font-size: 13px;
font-weight: 700;
text-align: right;
}
/* Progress Bar */
.progress-bar {
height: 6px;
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
overflow: hidden;
margin: 0;
/* Remove margin as it is in grid */
min-width: 80px;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #8b5cf6 0%, #a78bfa 100%);
border-radius: 3px;
transition: width 0.6s ease;
position: relative;
box-shadow: 0 0 10px rgba(139, 92, 246, 0.3);
}
/* Shimmer removed */
.progress-fill.weekly {
background: linear-gradient(90deg, #3b82f6 0%, #60a5fa 100%);
box-shadow: 0 0 10px rgba(59, 130, 246, 0.3);
}
/* Timer Container */
.timer-container {
display: flex;
align-items: center;
justify-content: space-between;
/* Text on left, chart on right */
gap: 8px;
/* space between text and pie chart */
padding-left: 8px;
/* Alignment adjustment */
}
.mini-timer {
transform: rotate(-90deg);
}
.timer-bg {
fill: none;
stroke: rgba(255, 255, 255, 0.1);
stroke-width: 4;
}
.timer-progress {
fill: none;
stroke: #8b5cf6;
stroke-width: 4;
transition: stroke-dashoffset 0.6s ease;
}
.timer-progress.weekly {
stroke: #3b82f6;
}
.timer-text {
color: #e0e0e0;
font-size: 13px;
font-weight: 500;
font-variant-numeric: tabular-nums;
font-family: inherit;
}
/* Settings Overlay */
.settings-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: linear-gradient(135deg, #1e1e2e 0%, #2a2a3e 100%);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.settings-content {
padding: 0 24px;
width: 100%;
text-align: center;
position: relative;
display: flex;
flex-direction: column;
gap: 12px;
max-width: 400px;
}
.icon-close-settings-btn {
position: fixed;
top: 8px;
right: 8px;
background: rgba(255, 255, 255, 0.05);
border: none;
color: #a0a0a0;
font-size: 24px;
line-height: 1;
cursor: pointer;
padding: 4px;
border-radius: 6px;
transition: all 0.2s;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
}
.icon-close-settings-btn:hover {
color: #fff;
background: rgba(255, 255, 255, 0.1);
}
.disclaimer {
color: #a0a0a0;
font-size: 9px;
line-height: 1.2;
margin-bottom: 12px;
white-space: nowrap;
}
.coffee-btn {
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
color: #1a1a1a;
border: none;
padding: 8px 12px;
border-radius: 6px;
font-size: 11px;
font-weight: 700;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
text-decoration: none;
transition: all 0.2s;
width: 100%;
}
.coffee-btn svg {
display: block;
flex-shrink: 0;
}
.coffee-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(255, 165, 0, 0.4);
}
.settings-actions {
display: flex;
justify-content: center;
gap: 12px;
padding-top: 4px;
}
.logout-btn {
background: transparent;
color: #ef4444;
border: 1px solid rgba(239, 68, 68, 0.5);
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 11px;
font-weight: 600;
transition: all 0.2s;
width: 100%;
}
.logout-btn:hover {
background: rgba(239, 68, 68, 0.15);
border-color: rgba(239, 68, 68, 0.7);
}
.close-settings-btn {
background: rgba(255, 255, 255, 0.1);
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
font-weight: 600;
transition: all 0.2s;
}
.close-settings-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
.settings-btn {
font-size: 14px;
}
.footer {
padding-top: 12px;
border-top: 1px solid rgba(255, 255, 255, 0.05);
}
.last-update {
color: #666;
font-size: 11px;
display: block;
text-align: center;
}
/* Scrollbar (if needed) */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}
/* Warning states */
.progress-fill.warning {
background: linear-gradient(90deg, #f59e0b 0%, #fbbf24 100%);
}
.progress-fill.danger {
background: linear-gradient(90deg, #ef4444 0%, #f87171 100%);
}
.timer-progress.warning {
stroke: #f59e0b;
}
.timer-progress.danger {
stroke: #ef4444;
}