Compare commits

...

10 commits

Author SHA1 Message Date
Claude
5f9ffff8f6 Add customizable color preferences with separate settings window
## New Features
- **Customizable Progress Bar Colors**: Users can now customize the colors for normal, warning, and danger states through a dedicated settings window
- **Separate Settings Window**: Settings now open in a frameless, resizable window (500x680px) with live preview of color changes
- **CSS Variables**: Converted hardcoded colors to CSS custom properties for dynamic theming

## Files Modified
- **main.js**: Added settings window creation, color preference storage (electron-store), and IPC handlers
- **preload.js**: Exposed color preference and settings IPC methods to renderer
- **app.js**: Implemented color preference loading and live updates from settings window
- **styles.css**: Added CSS variables for customizable colors, updated scrollbar styling
- **index.html**: Removed unused settings overlay
- **.gitignore**: Added CLAUDE_NOTES.md to prevent credential leaks

## Files Added
- **src/renderer/settings.html**: Settings window UI with 2-column color picker layout
- **src/renderer/settings.js**: Settings window logic and color management
- **src/renderer/settings-styles.css**: Settings window styling
- **CHANGES.md**: Tracking document for modifications

## Technical Details
- Color preferences stored in electron-store with encryption
- Live color updates via IPC communication between settings and main windows
- Default color scheme: Purple (normal), Orange (warning), Red (danger)
- Settings accessible via tray menu or settings button

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-24 10:21:36 -05:00
d616b885b5 Merge remote repository - keep detailed README 2025-12-24 09:10:29 -05:00
Slavomir Durej
cb33ed9619 trying to fix the log out issue 2025-12-19 16:49:02 +00:00
Slavomir Durej
568503f3e0 css error fix 2025-12-17 08:41:56 +00:00
Slavomir Durej
8500ccfaad no usage display notice implemented
remembering the last known position implemented
2025-12-17 08:41:19 +00:00
Slavomir Durej
ba504455e8 tweaks to logo 2025-12-16 10:48:50 +00:00
Slavomir Durej
8c21bcba74 Update link to Releases in README 2025-12-15 19:58:10 +00:00
Slavomir Durej
7dab135369 Update screenshot path in README.md 2025-12-15 19:47:55 +00:00
Slavomir Durej
b1fb98539f added project logo 2025-12-15 19:46:16 +00:00
Slavomir Durej
f72cbf9e91 initial commit 2025-12-14 18:27:30 +00:00
21 changed files with 2938 additions and 1 deletions

View file

@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(dir:*)"
]
}
}

39
.gitignore vendored Normal file
View file

@ -0,0 +1,39 @@
# 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/
CLAUDE_NOTES.md

39
CHANGES.md Normal file
View file

@ -0,0 +1,39 @@
# Changes for Upstream PR
## Feature: Customizable Progress Bar Colors
### Modified Files
#### main.js
- Added `DEFAULT_COLOR_PREFERENCES` constant with default color scheme (normal, warning, danger states)
- Added IPC handler `get-color-preferences` to retrieve stored color preferences from electron-store
- Added IPC handler `set-color-preferences` to save color preferences to electron-store
- Storage schema: `colorPreferences: { normal: {start, end}, warning: {start, end}, danger: {start, end} }`
#### src/renderer/styles.css
- TODO: Add CSS custom properties (--color-normal-start, --color-normal-end, etc.)
- TODO: Convert hardcoded gradient colors to use CSS variables
- TODO: Apply to both .progress-fill and .timer-progress elements
#### src/renderer/app.js
- TODO: Add `applyColorPreferences()` function to set CSS custom properties
- TODO: Load and apply color preferences on init
- TODO: Add event listeners for color picker changes
#### src/renderer/index.html
- TODO: Add color picker UI in settings overlay (6 inputs for simplified scheme)
- TODO: Add "Reset to Defaults" button
#### preload.js
- TODO: Expose `getColorPreferences` and `setColorPreferences` IPC methods
---
## Feature: Dynamic Tray Icon with Usage Display (Planned)
- Not yet started
---
## Storage Changes
- Using existing electron-store, no additional files
- New keys: `colorPreferences`, `trayDisplayMode` (planned)

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
View file

@ -1,2 +1,188 @@
# claude-usage-widget
# Claude Usage Widget
A beautiful, standalone Windows desktop widget that displays your Claude.ai usage statistics in real-time.
![Claude Usage Widget](assets/claude-usage-screenshot.jpg)
## 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](https://github.com/SlavomirDurej/claude-usage-widget/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
- Window position is now saved automatically when you drag it
- Position will be restored when you restart the app
### 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
- [x] 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
assets/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

BIN
assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
assets/tray-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 KiB

600
main.js Normal file
View file

@ -0,0 +1,600 @@
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 silentLoginWindow = null;
let settingsWindow = null;
let tray = null;
// Window configuration
const WIDGET_WIDTH = 480;
const WIDGET_HEIGHT = 140;
const SETTINGS_WIDTH = 500;
const SETTINGS_HEIGHT = 680;
function createMainWindow() {
// Load saved position or use defaults
const savedPosition = store.get('windowPosition');
const windowOptions = {
width: WIDGET_WIDTH,
height: WIDGET_HEIGHT,
frame: false,
transparent: true,
alwaysOnTop: true,
resizable: false,
skipTaskbar: false,
icon: path.join(__dirname, 'assets/icon.ico'),
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
}
};
// Apply saved position if it exists
if (savedPosition) {
windowOptions.x = savedPosition.x;
windowOptions.y = savedPosition.y;
}
mainWindow = new BrowserWindow(windowOptions);
mainWindow.loadFile('src/renderer/index.html');
// Make window draggable
mainWindow.setAlwaysOnTop(true, 'floating');
mainWindow.setVisibleOnAllWorkspaces(true);
// Save position when window is moved
mainWindow.on('move', () => {
const position = mainWindow.getBounds();
store.set('windowPosition', { x: position.x, y: position.y });
});
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;
});
}
// Attempt silent login in a hidden browser window
async function attemptSilentLogin() {
console.log('[Main] Attempting silent login...');
// Notify renderer that we're trying to auto-login
if (mainWindow) {
mainWindow.webContents.send('silent-login-started');
}
return new Promise((resolve) => {
silentLoginWindow = new BrowserWindow({
width: 800,
height: 700,
show: false, // Hidden window
webPreferences: {
nodeIntegration: false,
contextIsolation: true
}
});
silentLoginWindow.loadURL('https://claude.ai');
let loginCheckInterval = null;
let hasLoggedIn = false;
const SILENT_LOGIN_TIMEOUT = 15000; // 15 seconds timeout
// Function to check login status
async function checkLoginStatus() {
if (hasLoggedIn || !silentLoginWindow) 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('[Main] Silent login: 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('[Main] Silent login: Org ID fetched from API:', orgId);
}
} catch (err) {
console.log('[Main] Silent login: API not ready yet:', err.message);
}
if (sessionKey && orgId) {
hasLoggedIn = true;
if (loginCheckInterval) {
clearInterval(loginCheckInterval);
loginCheckInterval = null;
}
console.log('[Main] Silent login successful!');
store.set('sessionKey', sessionKey);
store.set('organizationId', orgId);
if (mainWindow) {
mainWindow.webContents.send('login-success', { sessionKey, organizationId: orgId });
}
silentLoginWindow.close();
resolve(true);
}
}
} catch (error) {
console.error('[Main] Silent login check error:', error);
}
}
// Check on page load
silentLoginWindow.webContents.on('did-finish-load', async () => {
const url = silentLoginWindow.webContents.getURL();
console.log('[Main] Silent login page loaded:', url);
if (url.includes('claude.ai')) {
await checkLoginStatus();
}
});
// Also check on navigation
silentLoginWindow.webContents.on('did-navigate', async (event, url) => {
console.log('[Main] Silent login navigated to:', url);
if (url.includes('claude.ai')) {
await checkLoginStatus();
}
});
// Poll periodically
loginCheckInterval = setInterval(async () => {
if (!hasLoggedIn && silentLoginWindow) {
await checkLoginStatus();
} else if (loginCheckInterval) {
clearInterval(loginCheckInterval);
loginCheckInterval = null;
}
}, 1000);
// Timeout - if silent login doesn't work, fall back to visible login
setTimeout(() => {
if (!hasLoggedIn) {
console.log('[Main] Silent login timeout, falling back to visible login...');
if (loginCheckInterval) {
clearInterval(loginCheckInterval);
loginCheckInterval = null;
}
if (silentLoginWindow) {
silentLoginWindow.close();
}
// Notify renderer that silent login failed
if (mainWindow) {
mainWindow.webContents.send('silent-login-failed');
}
// Open visible login window
createLoginWindow();
resolve(false);
}
}, SILENT_LOGIN_TIMEOUT);
silentLoginWindow.on('closed', () => {
if (loginCheckInterval) {
clearInterval(loginCheckInterval);
loginCheckInterval = null;
}
silentLoginWindow = 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: () => {
createSettingsWindow();
}
},
{
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);
}
}
function createSettingsWindow() {
if (settingsWindow) {
settingsWindow.focus();
return;
}
settingsWindow = new BrowserWindow({
width: SETTINGS_WIDTH,
height: SETTINGS_HEIGHT,
title: 'Settings - Claude Usage Widget',
frame: false,
resizable: true,
minimizable: true,
maximizable: false,
icon: path.join(__dirname, 'assets/icon.ico'),
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
}
});
settingsWindow.loadFile('src/renderer/settings.html');
settingsWindow.on('closed', () => {
settingsWindow = null;
});
// Development tools
if (process.env.NODE_ENV === 'development') {
settingsWindow.webContents.openDevTools({ mode: 'detach' });
}
}
// 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('open-settings', () => {
createSettingsWindow();
});
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);
});
// Color preferences handlers
const DEFAULT_COLOR_PREFERENCES = {
normal: { start: '#8b5cf6', end: '#a78bfa' },
warning: { start: '#f59e0b', end: '#fbbf24' },
danger: { start: '#ef4444', end: '#f87171' }
};
ipcMain.handle('get-color-preferences', () => {
const saved = store.get('colorPreferences');
return saved || DEFAULT_COLOR_PREFERENCES;
});
ipcMain.handle('set-color-preferences', (event, preferences) => {
store.set('colorPreferences', preferences);
return true;
});
ipcMain.handle('notify-color-change', (event, preferences) => {
// Notify main window to update colors
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('colors-changed', preferences);
}
return true;
});
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) {
// Session expired - attempt silent re-login
console.log('[Main] Session expired, attempting silent re-login...');
store.delete('sessionKey');
store.delete('organizationId');
// Don't clear cookies - we need them for silent login to work with OAuth
// The silent login will use existing Google/OAuth session if available
// Attempt silent login (will notify renderer appropriately)
attemptSilentLogin();
throw new Error('SessionExpired');
}
}
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.3.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"
]
}
}

49
preload.js Normal file
View file

@ -0,0 +1,49 @@
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'),
openSettings: () => ipcRenderer.send('open-settings'),
// 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());
},
onSessionExpired: (callback) => {
ipcRenderer.on('session-expired', () => callback());
},
onSilentLoginStarted: (callback) => {
ipcRenderer.on('silent-login-started', () => callback());
},
onSilentLoginFailed: (callback) => {
ipcRenderer.on('silent-login-failed', () => callback());
},
// API
fetchUsageData: () => ipcRenderer.invoke('fetch-usage-data'),
openExternal: (url) => ipcRenderer.send('open-external', url),
// Color preferences
getColorPreferences: () => ipcRenderer.invoke('get-color-preferences'),
setColorPreferences: (preferences) => ipcRenderer.invoke('set-color-preferences', preferences),
notifyColorChange: (preferences) => ipcRenderer.invoke('notify-color-change', preferences),
onColorsChanged: (callback) => {
ipcRenderer.on('colors-changed', (event, preferences) => callback(preferences));
}
});

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

@ -0,0 +1,428 @@
// 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'),
noUsageContainer: document.getElementById('noUsageContainer'),
autoLoginContainer: document.getElementById('autoLoginContainer'),
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')
};
// Initialize
async function init() {
setupEventListeners();
credentials = await window.electronAPI.getCredentials();
// Load and apply color preferences
const colorPrefs = await window.electronAPI.getColorPreferences();
applyColorPreferences(colorPrefs);
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 button
elements.settingsBtn.addEventListener('click', () => {
window.electronAPI.openSettings();
});
// 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();
});
// Listen for session expiration events (403 errors) - only used as fallback
window.electronAPI.onSessionExpired(() => {
console.log('Session expired event received');
credentials = { sessionKey: null, organizationId: null };
showLoginRequired();
});
// Listen for silent login attempts
window.electronAPI.onSilentLoginStarted(() => {
console.log('Silent login started...');
showAutoLoginAttempt();
});
// Listen for silent login failures (falls back to visible login)
window.electronAPI.onSilentLoginFailed(() => {
console.log('Silent login failed, manual login required');
showLoginRequired();
});
// Listen for color preference changes from settings window
window.electronAPI.onColorsChanged((preferences) => {
console.log('Colors changed, applying new preferences');
applyColorPreferences(preferences);
});
}
// 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('SessionExpired') || error.message.includes('Unauthorized')) {
// Session expired - silent login attempt is in progress
// Show auto-login UI while waiting
credentials = { sessionKey: null, organizationId: null };
showAutoLoginAttempt();
} else {
showError('Failed to fetch usage data');
}
}
}
// Check if there's no usage data
function hasNoUsage(data) {
const sessionUtilization = data.five_hour?.utilization || 0;
const sessionResetsAt = data.five_hour?.resets_at;
const weeklyUtilization = data.seven_day?.utilization || 0;
const weeklyResetsAt = data.seven_day?.resets_at;
return sessionUtilization === 0 && !sessionResetsAt &&
weeklyUtilization === 0 && !weeklyResetsAt;
}
// Update UI with usage data
function updateUI(data) {
latestUsageData = data;
// Check if there's no usage data
if (hasNoUsage(data)) {
showNoUsage();
return;
}
showMainContent();
refreshTimers();
startCountdown();
}
// 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 = '--:--';
textElement.style.opacity = '0.5';
textElement.title = 'Starts when a message is sent';
timerElement.style.strokeDashoffset = 63;
return;
}
// Clear the greyed out styling and tooltip when timer is active
textElement.style.opacity = '1';
textElement.title = '';
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.noUsageContainer.style.display = 'none';
elements.autoLoginContainer.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.noUsageContainer.style.display = 'none';
elements.autoLoginContainer.style.display = 'none';
elements.mainContent.style.display = 'none';
stopAutoUpdate();
}
function showNoUsage() {
elements.loadingContainer.style.display = 'none';
elements.loginContainer.style.display = 'none';
elements.noUsageContainer.style.display = 'flex';
elements.autoLoginContainer.style.display = 'none';
elements.mainContent.style.display = 'none';
}
function showAutoLoginAttempt() {
elements.loadingContainer.style.display = 'none';
elements.loginContainer.style.display = 'none';
elements.noUsageContainer.style.display = 'none';
elements.autoLoginContainer.style.display = 'flex';
elements.mainContent.style.display = 'none';
stopAutoUpdate();
}
function showMainContent() {
elements.loadingContainer.style.display = 'none';
elements.loginContainer.style.display = 'none';
elements.noUsageContainer.style.display = 'none';
elements.autoLoginContainer.style.display = 'none';
elements.mainContent.style.display = 'block';
}
function showError(message) {
// TODO: Implement error notification
console.error(message);
}
// Color preference management
function applyColorPreferences(prefs) {
const root = document.documentElement;
// Apply normal colors
root.style.setProperty('--color-normal-start', prefs.normal.start);
root.style.setProperty('--color-normal-end', prefs.normal.end);
// Apply warning colors
root.style.setProperty('--color-warning-start', prefs.warning.start);
root.style.setProperty('--color-warning-end', prefs.warning.end);
// Apply danger colors
root.style.setProperty('--color-danger-start', prefs.danger.start);
root.style.setProperty('--color-danger-end', prefs.danger.end);
}
// 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);
});

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

@ -0,0 +1,129 @@
<!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">
<img src="../../assets/logo.png" alt="Logo" class="app-logo">
<span>Claude Usage</span>
</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>
<!-- No Usage State -->
<div class="no-usage-container" id="noUsageContainer" style="display: none;">
<div class="no-usage-content">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
</svg>
<div class="no-usage-text-group">
<h3>No Usage Yet</h3>
<p>Start chatting with Claude to see your usage stats</p>
</div>
</div>
</div>
<!-- Auto-Login Attempt State -->
<div class="auto-login-container" id="autoLoginContainer" style="display: none;">
<div class="auto-login-content">
<div class="spinner"></div>
<div class="auto-login-text-group">
<h3>Trying to auto-login...</h3>
<p>Please wait while we reconnect</p>
</div>
</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>
</div>
</div>
<script src="app.js"></script>
</body>
</html>

View file

@ -0,0 +1,230 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #1e1e2e 0%, #2a2a3e 100%);
color: #e0e0e0;
overflow: hidden;
}
/* Title Bar */
.title-bar {
-webkit-app-region: drag;
background: rgba(0, 0, 0, 0.3);
padding: 8px 12px;
padding-left: 6px;
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;
display: flex;
align-items: center;
gap: 8px;
}
.app-logo {
width: 19px;
height: 19px;
border-radius: 6px;
opacity: 0.8;
}
.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;
}
.close-btn:hover {
background: #e74c3c;
color: white;
}
#settings-app {
width: 100%;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.settings-container {
max-width: 600px;
margin: 0 auto;
padding: 24px;
flex: 1;
}
.settings-section {
margin-bottom: 24px;
}
.settings-section h2 {
font-size: 16px;
font-weight: 600;
margin-bottom: 16px;
color: #e0e0e0;
}
/* 2-Column Grid for Color Pickers */
.color-grid-2col {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
margin-bottom: 16px;
}
.color-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.color-item label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #a0a0a0;
text-align: center;
}
.color-item input[type="color"] {
width: 80px;
height: 50px;
border: 2px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
background: transparent;
cursor: pointer;
padding: 4px;
transition: all 0.2s ease;
}
.color-item input[type="color"]:hover {
border-color: rgba(255, 255, 255, 0.4);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.color-item input[type="color"]::-webkit-color-swatch-wrapper {
padding: 0;
}
.color-item input[type="color"]::-webkit-color-swatch {
border: none;
border-radius: 6px;
}
/* Buttons */
.btn-secondary,
.btn-danger,
.btn-coffee {
width: 100%;
padding: 10px 16px;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
border: none;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn-secondary {
background: rgba(255, 255, 255, 0.1);
color: #e0e0e0;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.15);
transform: translateY(-1px);
}
.btn-danger {
background: transparent;
color: #ef4444;
border: 1px solid rgba(239, 68, 68, 0.5);
}
.btn-danger:hover {
background: rgba(239, 68, 68, 0.15);
border-color: rgba(239, 68, 68, 0.7);
}
.btn-coffee {
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
color: #1a1a1a;
}
.btn-coffee:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(255, 165, 0, 0.4);
}
.btn-coffee svg {
flex-shrink: 0;
}
/* Divider */
.divider {
width: 100%;
height: 1px;
background: rgba(255, 255, 255, 0.15);
margin: 24px 0;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
/* Disclaimer */
.disclaimer {
font-size: 11px;
color: #666;
text-align: center;
line-height: 1.5;
}
.disclaimer strong {
color: #888;
}
/* Disclaimer spacing */
.disclaimer {
margin-top: 16px;
}

105
src/renderer/settings.html Normal file
View file

@ -0,0 +1,105 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Settings - 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="settings-styles.css">
</head>
<body>
<div id="settings-app">
<!-- Title Bar -->
<div class="title-bar" id="titleBar">
<div class="title">
<img src="../../assets/logo.png" alt="Logo" class="app-logo">
<span>Settings</span>
</div>
<div class="controls">
<button class="control-btn minimize-btn" id="minimizeBtn" title="Minimize"></button>
<button class="control-btn close-btn" id="closeBtn" title="Close">×</button>
</div>
</div>
<div class="settings-container">
<!-- Color Customization -->
<section class="settings-section">
<h2>Customize Colors</h2>
<div class="color-grid-2col">
<div class="color-item">
<label>Normal Start</label>
<input type="color" id="colorNormalStart" value="#8b5cf6">
</div>
<div class="color-item">
<label>Normal End</label>
<input type="color" id="colorNormalEnd" value="#a78bfa">
</div>
<div class="color-item">
<label>Warning Start</label>
<input type="color" id="colorWarningStart" value="#f59e0b">
</div>
<div class="color-item">
<label>Warning End</label>
<input type="color" id="colorWarningEnd" value="#fbbf24">
</div>
<div class="color-item">
<label>Danger Start</label>
<input type="color" id="colorDangerStart" value="#ef4444">
</div>
<div class="color-item">
<label>Danger End</label>
<input type="color" id="colorDangerEnd" value="#f87171">
</div>
</div>
<button class="btn-secondary" id="resetColorsBtn">Reset to Defaults</button>
</section>
<div class="divider"></div>
<!-- Account Actions -->
<section class="settings-section">
<h2>Account</h2>
<button class="btn-danger" id="logoutBtn">Log Out</button>
</section>
<div class="divider"></div>
<!-- Support -->
<section class="settings-section">
<button class="btn-coffee" 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>
<!-- Disclaimer -->
<p class="disclaimer">
<strong>Disclaimer:</strong> Unofficial tool not affiliated with Anthropic. Use at your own
discretion.
</p>
</section>
</div>
</div>
<script src="settings.js"></script>
</body>
</html>

107
src/renderer/settings.js Normal file
View file

@ -0,0 +1,107 @@
// DOM elements
const elements = {
colorNormalStart: document.getElementById('colorNormalStart'),
colorNormalEnd: document.getElementById('colorNormalEnd'),
colorWarningStart: document.getElementById('colorWarningStart'),
colorWarningEnd: document.getElementById('colorWarningEnd'),
colorDangerStart: document.getElementById('colorDangerStart'),
colorDangerEnd: document.getElementById('colorDangerEnd'),
resetColorsBtn: document.getElementById('resetColorsBtn'),
logoutBtn: document.getElementById('logoutBtn'),
coffeeBtn: document.getElementById('coffeeBtn'),
minimizeBtn: document.getElementById('minimizeBtn'),
closeBtn: document.getElementById('closeBtn')
};
// Initialize
async function init() {
setupEventListeners();
// Load and apply color preferences
const colorPrefs = await window.electronAPI.getColorPreferences();
loadColorPickerValues(colorPrefs);
}
// Event Listeners
function setupEventListeners() {
// Color picker event listeners
const colorInputs = [
elements.colorNormalStart,
elements.colorNormalEnd,
elements.colorWarningStart,
elements.colorWarningEnd,
elements.colorDangerStart,
elements.colorDangerEnd
];
colorInputs.forEach(input => {
input.addEventListener('change', async () => {
await saveCurrentColors();
});
});
elements.resetColorsBtn.addEventListener('click', async () => {
const defaults = {
normal: { start: '#8b5cf6', end: '#a78bfa' },
warning: { start: '#f59e0b', end: '#fbbf24' },
danger: { start: '#ef4444', end: '#f87171' }
};
await window.electronAPI.setColorPreferences(defaults);
loadColorPickerValues(defaults);
// Notify main window to update colors immediately
await window.electronAPI.notifyColorChange(defaults);
});
elements.logoutBtn.addEventListener('click', async () => {
await window.electronAPI.deleteCredentials();
window.close();
window.electronAPI.openLogin();
});
elements.coffeeBtn.addEventListener('click', () => {
window.electronAPI.openExternal('https://paypal.me/SlavomirDurej?country.x=GB&locale.x=en_GB');
});
// Window controls
elements.minimizeBtn.addEventListener('click', () => {
window.electronAPI.minimizeWindow();
});
elements.closeBtn.addEventListener('click', () => {
window.close();
});
}
// Helper functions
function loadColorPickerValues(prefs) {
elements.colorNormalStart.value = prefs.normal.start;
elements.colorNormalEnd.value = prefs.normal.end;
elements.colorWarningStart.value = prefs.warning.start;
elements.colorWarningEnd.value = prefs.warning.end;
elements.colorDangerStart.value = prefs.danger.start;
elements.colorDangerEnd.value = prefs.danger.end;
}
async function saveCurrentColors() {
const prefs = {
normal: {
start: elements.colorNormalStart.value,
end: elements.colorNormalEnd.value
},
warning: {
start: elements.colorWarningStart.value,
end: elements.colorWarningEnd.value
},
danger: {
start: elements.colorDangerStart.value,
end: elements.colorDangerEnd.value
}
};
await window.electronAPI.setColorPreferences(prefs);
// Notify main window to update colors
await window.electronAPI.notifyColorChange(prefs);
}
// Start the application
init();

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

@ -0,0 +1,589 @@
:root {
/* Customizable color scheme - normal state */
--color-normal-start: #8b5cf6;
--color-normal-end: #a78bfa;
/* Warning state (>=75% usage) */
--color-warning-start: #f59e0b;
--color-warning-end: #fbbf24;
/* Danger state (>=90% usage) */
--color-danger-start: #ef4444;
--color-danger-end: #f87171;
}
* {
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;
padding-left: 6px;
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;
display: flex;
align-items: center;
gap: 8px;
}
.app-logo {
width: 19px;
height: 19px;
border-radius: 6px;
opacity: 0.8;
}
.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);
}
/* No Usage State */
.no-usage-container {
padding: 0 16px;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.no-usage-content {
display: flex;
align-items: center;
gap: 16px;
text-align: left;
width: 100%;
}
.no-usage-content svg {
color: #8b5cf6;
width: 32px;
height: 32px;
flex-shrink: 0;
}
.no-usage-text-group {
display: flex;
flex-direction: column;
justify-content: center;
}
.no-usage-content h3 {
color: #e0e0e0;
font-size: 14px;
margin-bottom: 2px;
}
.no-usage-content p {
color: #a0a0a0;
font-size: 11px;
margin-bottom: 0;
}
/* Auto-Login Attempt State */
.auto-login-container {
padding: 0 16px;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.auto-login-content {
display: flex;
align-items: center;
gap: 16px;
text-align: left;
width: 100%;
}
.auto-login-content .spinner {
width: 32px;
height: 32px;
flex-shrink: 0;
margin: 0;
}
.auto-login-text-group {
display: flex;
flex-direction: column;
justify-content: center;
}
.auto-login-content h3 {
color: #e0e0e0;
font-size: 14px;
margin-bottom: 2px;
}
.auto-login-content p {
color: #a0a0a0;
font-size: 11px;
margin-bottom: 0;
}
/* 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, var(--color-normal-start) 0%, var(--color-normal-end) 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, var(--color-normal-start) 0%, var(--color-normal-end) 100%);
box-shadow: 0 0 10px rgba(139, 92, 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: var(--color-normal-start);
stroke-width: 4;
transition: stroke-dashoffset 0.6s ease;
}
.timer-progress.weekly {
stroke: var(--color-normal-start);
}
.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: rgba(0, 0, 0, 0.2);
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.15);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.25);
}
/* Warning states */
.progress-fill.warning {
background: linear-gradient(90deg, var(--color-warning-start) 0%, var(--color-warning-end) 100%);
}
.progress-fill.danger {
background: linear-gradient(90deg, var(--color-danger-start) 0%, var(--color-danger-end) 100%);
}
.timer-progress.warning {
stroke: var(--color-warning-start);
}
.timer-progress.danger {
stroke: var(--color-danger-start);
}