├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── claude-code-review.yml │ ├── claude.yml │ └── release.yml ├── .gitignore ├── .vscode └── extensions.json ├── LICENSE.md ├── README.md ├── art └── banner.png ├── build-themes.js ├── debug-themes.js ├── generate-keys.sh ├── package-lock.json ├── package.json ├── release.sh ├── setup-updates.sh ├── src-tauri ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── build.rs ├── capabilities │ └── default.json ├── icons │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── 32x32.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square30x30Logo.png │ ├── Square310x310Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── StoreLogo.png │ ├── icon.icns │ ├── icon.ico │ └── icon.png ├── src │ ├── lib.rs │ └── main.rs ├── tauri.conf.json └── tauri.conf.json.backup ├── src ├── assets │ ├── javascript.svg │ └── tauri.svg ├── components │ └── update-notification.js ├── core │ └── pomodoro-timer.js ├── docs │ ├── TEST_MANUAL.md │ ├── THEMES.md │ ├── UPDATER_SETUP.md │ └── UPDATES.md ├── index.html ├── main.js ├── managers │ ├── auth-manager.js │ ├── navigation-manager.js │ ├── session-manager.js │ ├── settings-manager.js │ ├── tag-manager.js │ ├── team-manager.js │ ├── update-manager-browser-compatible.js │ ├── update-manager-fixed.js │ ├── update-manager-global.js │ ├── update-manager-v2-corrected.js │ ├── update-manager-v2.js │ └── update-manager.js ├── styles │ ├── README.md │ ├── animations.css │ ├── calendar.css │ ├── controls.css │ ├── layout.css │ ├── main.css │ ├── modals.css │ ├── notifications.css │ ├── progress.css │ ├── responsive.css │ ├── settings.css │ ├── shared-components.css │ ├── sidebar.css │ ├── smart-indicator.css │ ├── statistics.css │ ├── tasks.css │ ├── team.css │ ├── themes │ │ ├── espresso.css │ │ ├── pipboy.css │ │ └── pommodore64.css │ ├── timeline.css │ ├── timer.css │ └── variables.css ├── utils │ ├── analytics.js │ ├── common-utils.js │ ├── supabase.js │ ├── tag-statistics.js │ ├── theme-loader.js │ └── timer-themes.js └── version.js └── verify-updates.sh /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG] " 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Version [e.g. 22] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/workflows/claude-code-review.yml: -------------------------------------------------------------------------------- 1 | name: Claude Code Review 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize] 6 | # Optional: Only run on specific file changes 7 | # paths: 8 | # - "src/**/*.ts" 9 | # - "src/**/*.tsx" 10 | # - "src/**/*.js" 11 | # - "src/**/*.jsx" 12 | 13 | jobs: 14 | claude-review: 15 | # Optional: Filter by PR author 16 | # if: | 17 | # github.event.pull_request.user.login == 'external-contributor' || 18 | # github.event.pull_request.user.login == 'new-developer' || 19 | # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' 20 | 21 | runs-on: ubuntu-latest 22 | permissions: 23 | contents: read 24 | pull-requests: read 25 | issues: read 26 | id-token: write 27 | 28 | steps: 29 | - name: Checkout repository 30 | uses: actions/checkout@v4 31 | with: 32 | fetch-depth: 1 33 | 34 | - name: Run Claude Code Review 35 | id: claude-review 36 | uses: anthropics/claude-code-action@beta 37 | with: 38 | claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} 39 | 40 | # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4) 41 | # model: "claude-opus-4-20250514" 42 | 43 | # Direct prompt for automated review (no @claude mention needed) 44 | direct_prompt: | 45 | Please review this pull request and provide feedback on: 46 | - Code quality and best practices 47 | - Potential bugs or issues 48 | - Performance considerations 49 | - Security concerns 50 | - Test coverage 51 | 52 | Be constructive and helpful in your feedback. 53 | 54 | # Optional: Use sticky comments to make Claude reuse the same comment on subsequent pushes to the same PR 55 | # use_sticky_comment: true 56 | 57 | # Optional: Customize review based on file types 58 | # direct_prompt: | 59 | # Review this PR focusing on: 60 | # - For TypeScript files: Type safety and proper interface usage 61 | # - For API endpoints: Security, input validation, and error handling 62 | # - For React components: Performance, accessibility, and best practices 63 | # - For tests: Coverage, edge cases, and test quality 64 | 65 | # Optional: Different prompts for different authors 66 | # direct_prompt: | 67 | # ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' && 68 | # 'Welcome! Please review this PR from a first-time contributor. Be encouraging and provide detailed explanations for any suggestions.' || 69 | # 'Please provide a thorough code review focusing on our coding standards and best practices.' }} 70 | 71 | # Optional: Add specific tools for running tests or linting 72 | # allowed_tools: "Bash(npm run test),Bash(npm run lint),Bash(npm run typecheck)" 73 | 74 | # Optional: Skip review for certain conditions 75 | # if: | 76 | # !contains(github.event.pull_request.title, '[skip-review]') && 77 | # !contains(github.event.pull_request.title, '[WIP]') 78 | 79 | -------------------------------------------------------------------------------- /.github/workflows/claude.yml: -------------------------------------------------------------------------------- 1 | name: Claude Code 2 | 3 | on: 4 | issue_comment: 5 | types: [created] 6 | pull_request_review_comment: 7 | types: [created] 8 | issues: 9 | types: [opened, assigned] 10 | pull_request_review: 11 | types: [submitted] 12 | 13 | jobs: 14 | claude: 15 | if: | 16 | (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || 17 | (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || 18 | (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || 19 | (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) 20 | runs-on: ubuntu-latest 21 | permissions: 22 | contents: read 23 | pull-requests: read 24 | issues: read 25 | id-token: write 26 | actions: read # Required for Claude to read CI results on PRs 27 | steps: 28 | - name: Checkout repository 29 | uses: actions/checkout@v4 30 | with: 31 | fetch-depth: 1 32 | 33 | - name: Run Claude Code 34 | id: claude 35 | uses: anthropics/claude-code-action@beta 36 | with: 37 | claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} 38 | 39 | # This is an optional setting that allows Claude to read CI results on PRs 40 | additional_permissions: | 41 | actions: read 42 | 43 | # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4) 44 | # model: "claude-opus-4-20250514" 45 | 46 | # Optional: Customize the trigger phrase (default: @claude) 47 | # trigger_phrase: "/claude" 48 | 49 | # Optional: Trigger when specific user is assigned to an issue 50 | # assignee_trigger: "claude-bot" 51 | 52 | # Optional: Allow Claude to run specific commands 53 | # allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)" 54 | 55 | # Optional: Add custom instructions for Claude to customize its behavior for your project 56 | # custom_instructions: | 57 | # Follow our coding standards 58 | # Ensure all new code has tests 59 | # Use TypeScript for new files 60 | 61 | # Optional: Custom environment variables for Claude 62 | # claude_env: | 63 | # NODE_ENV: test 64 | 65 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | workflow_dispatch: 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | publish-tauri: 15 | permissions: 16 | contents: write 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | include: 21 | - platform: 'macos-latest' 22 | args: '--target aarch64-apple-darwin --bundles app' 23 | arch: 'aarch64' 24 | - platform: 'macos-latest' 25 | args: '--target x86_64-apple-darwin --bundles app' 26 | arch: 'x86_64' 27 | 28 | runs-on: ${{ matrix.platform }} 29 | steps: 30 | - name: Checkout repository 31 | uses: actions/checkout@v4 32 | 33 | - name: Setup Node.js 34 | uses: actions/setup-node@v4 35 | with: 36 | node-version: 'lts/*' 37 | cache: 'npm' 38 | 39 | - name: Install Rust stable 40 | uses: dtolnay/rust-toolchain@stable 41 | with: 42 | targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} 43 | 44 | - name: Rust cache 45 | uses: swatinem/rust-cache@v2 46 | with: 47 | workspaces: './src-tauri -> target' 48 | 49 | - name: Install frontend dependencies 50 | run: npm ci 51 | 52 | - name: Build and release 53 | uses: tauri-apps/tauri-action@v0 54 | env: 55 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 56 | TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} 57 | TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} 58 | TAURI_UPDATER_SUFFIX: "-${{ matrix.arch }}" 59 | with: 60 | tagName: v__VERSION__ # the action automatically replaces __VERSION__ with the app version. 61 | releaseName: 'v__VERSION__' 62 | releaseBody: | 63 | ## What's Changed 64 | 65 | See the assets below to download this version and install. 66 | 67 | ### Installation Notes 68 | 69 | **macOS**: Download the `.app` file for your architecture: 70 | - `presto_x.x.x_aarch64.app.tar.gz` for Apple Silicon (M1/M2/M3 Macs) 71 | - `presto_x.x.x_x64.app.tar.gz` for Intel Macs 72 | 73 | ### Auto-Updates 74 | 75 | This release supports automatic updates. If you're running a previous version, you'll be notified when this update is available and can install it directly from the app. 76 | 77 | **Full Changelog**: https://github.com/${{ github.repository }}/compare/v${{ github.event.before }}...${{ github.ref_name }} 78 | releaseDraft: false 79 | prerelease: false 80 | args: ${{ matrix.args }} 81 | 82 | - name: List generated files 83 | run: ls -R src-tauri/target/ || true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | .claude 27 | 28 | claude.md -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"] 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Presto Business Source License 1.1 2 | 3 | License: BSL 1.1 4 | 5 | Licensor: Stefano Novelli 6 | 7 | Licensed Work: Presto The Licensed Work is © 2025 8 | Stefano Novelli 9 | 10 | Additional Use Grant: You may make use of the Licensed Work, 11 | provided that you may not use the Licensed Work for a Streaming or 12 | Queuing Service. A "Streaming or Queueing Service" is a commercial 13 | offering that allows third parties (other than your employees and 14 | individual contractors) to access the functionality of the Licensed Work 15 | by performing an action directly or indirectly that causes the creation 16 | of a topic in the Licensed Work. For clarity, a Streaming or Queuing 17 | Service would include providers of infrastructure services, such as 18 | cloud services, hosting services, data center services and similarly 19 | situated third parties (including affiliates of such entities) that 20 | would offer the Licensed Work in connection with a broader service 21 | offering to customers or subscribers of such of such third party’s core 22 | services. 23 | 24 | Change Date: Change date is 2029-06-12 (YYYY-MM-DD format) 25 | 26 | Change License: [Apache License, Version 27 | 2.0](https://www.apache.org/licenses/LICENSE-2.0), as published by the 28 | Apache Foundation. 29 | 30 | Text of BSL 1.1 31 | 32 | The Licensor hereby grants you the right to copy, modify, create 33 | derivative works, redistribute, and make non-production use of the 34 | Licensed Work. The Licensor may make an Additional Use Grant, above, 35 | permitting limited production use. 36 | 37 | Effective on the Change Date, or the fifth anniversary of the first 38 | publicly available distribution of a specific version of the Licensed 39 | Work under this License, whichever comes first, the Licensor hereby 40 | grants you rights under the terms of the Change License, and the rights 41 | granted in the paragraph above terminate. 42 | 43 | If your use of the Licensed Work does not comply with the requirements 44 | currently in effect as described in this License, you must purchase a 45 | commercial license from the Licensor, its affiliated entities, or 46 | authorized resellers, or you must refrain from using the Licensed Work. 47 | 48 | All copies of the original and modified Licensed Work, and derivative 49 | works of the Licensed Work, are subject to this License. This License 50 | applies separately for each version of the Licensed Work and the Change 51 | Date may vary for each version of the Licensed Work released by 52 | Licensor. 53 | 54 | You must conspicuously display this License on each original or modified 55 | copy of the Licensed Work. If you receive the Licensed Work in original 56 | or modified form from a third party, the terms and conditions set forth 57 | in this License apply to your use of that work. 58 | 59 | Any use of the Licensed Work in violation of this License will 60 | automatically terminate your rights under this License for the current 61 | and all other versions of the Licensed Work. 62 | 63 | This License does not grant you any right in any trademark or logo of 64 | Licensor or its affiliates (provided that you may use a trademark or 65 | logo of Licensor as expressly required by this License).TO THE EXTENT 66 | PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON AN “AS IS” 67 | BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, EXPRESS 68 | OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF 69 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND 70 | TITLE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Presto banner 2 | 3 | # Presto - Pomodoro Timer 4 | 5 | A modern, cross-platform Pomodoro timer application built with Tauri (Rust + HTML/CSS/JavaScript). Presto helps you boost productivity using the proven Pomodoro Technique with a beautiful, intuitive interface. 6 | 7 | ## ✨ Features 8 | 9 | ### 🍅 Pomodoro Technique 10 | - **Standard Pomodoro cycles**: 25-minute work sessions 11 | - **Smart breaks**: 5-minute short breaks, 20-minute long breaks every 4 cycles 12 | - **Daily goal**: Track progress through 10 daily Pomodoro sessions 13 | - **Visual progress**: Dot indicators showing session completion 14 | 15 | ### ⏱️ Timer Management 16 | - **Flexible controls**: Start, pause, reset, and skip functionality 17 | - **Visual feedback**: Dynamic UI that changes based on session type (work/break) 18 | - **Audio notifications**: Sound alerts for session transitions 19 | - **Desktop notifications**: System notifications to keep you informed 20 | 21 | ### 📋 Task Management 22 | - **Task tracking**: Add and manage tasks for each Pomodoro session 23 | - **Task completion**: Mark tasks as completed with visual feedback 24 | - **Persistence**: Tasks are automatically saved and restored 25 | 26 | ### 📊 Statistics & History 27 | - **Weekly statistics**: Track your productivity patterns 28 | - **Session history**: View detailed history of completed sessions 29 | - **Progress tracking**: Monitor your daily and weekly Pomodoro completion 30 | 31 | ### ⌨️ Keyboard Shortcuts 32 | - **Space**: Start/Pause timer 33 | - **Cmd/Ctrl + R**: Reset current session 34 | - **Cmd/Ctrl + S**: Skip current session 35 | - **Cmd/Ctrl + H**: Show/hide history modal 36 | 37 | ### 🎨 Modern UI 38 | - **Dark mode design**: Easy on the eyes for long work sessions 39 | - **Responsive layout**: Works on different screen sizes 40 | - **Smooth animations**: Polished user experience 41 | - **Protection**: Prevents accidental closure during active sessions 42 | 43 | ## 🚀 Getting Started 44 | 45 | ### Installation via Homebrew (Recommended) 46 | 47 | The easiest way to install Presto on macOS is through Homebrew: 48 | 49 | ```bash 50 | brew install --cask murdercode/presto/presto 51 | ``` 52 | 53 | #### ⚠️ Troubleshooting: "Presto is damaged and can't be opened" 54 | 55 | If you see this error when launching Presto for the first time, it's a temporary issue that occurs because the app lacks an Apple Developer signature (which requires paying $99 to Apple). This is a common situation for open-source applications. To resolve it, run this command in Terminal: 56 | 57 | ```bash 58 | xattr -d com.apple.quarantine /Applications/presto.app 59 | ``` 60 | 61 | Then you can launch Presto normally from your Applications folder or Spotlight. 62 | 63 | ### Installation from Source 64 | 65 | If you prefer to build from source, you'll need: 66 | 67 | #### Prerequisites 68 | - [Node.js](https://nodejs.org/) (v16 or higher) 69 | - [Rust](https://rustup.rs/) (latest stable) 70 | - [Tauri CLI](https://tauri.app/v1/guides/getting-started/prerequisites) 71 | 72 | #### Steps 73 | 74 | 1. **Clone the repository** 75 | ```bash 76 | git clone https://github.com/murdercode/presto.git 77 | cd presto 78 | ``` 79 | 80 | 2. **Install dependencies** 81 | ```bash 82 | npm install 83 | ``` 84 | 85 | 3. **Run in development mode** 86 | ```bash 87 | npm run tauri dev 88 | ``` 89 | 90 | 4. **Build for production** 91 | ```bash 92 | npm run tauri build 93 | ``` 94 | 95 | ## 🏗️ Project Structure 96 | 97 | ``` 98 | tempo/ 99 | ├── src/ # Frontend source files 100 | │ ├── index.html # Main HTML interface 101 | │ ├── styles.css # CSS styles and animations 102 | │ └── main.js # JavaScript application logic 103 | ├── src-tauri/ # Rust backend 104 | │ ├── src/ 105 | │ │ └── lib.rs # Tauri commands and data persistence 106 | │ ├── Cargo.toml # Rust dependencies 107 | │ └── tauri.conf.json # Tauri configuration 108 | ├── package.json # Node.js dependencies and scripts 109 | └── README.md # This file 110 | ``` 111 | 112 | ## 🔧 Technical Details 113 | 114 | ### Frontend (HTML/CSS/JavaScript) 115 | - **Pure vanilla JavaScript**: No frameworks, lightweight and fast 116 | - **CSS Grid & Flexbox**: Modern responsive layouts 117 | - **CSS Custom Properties**: Consistent theming and easy customization 118 | - **Local Storage**: Client-side data persistence 119 | 120 | ### Backend (Rust/Tauri) 121 | - **Tauri framework**: Secure, fast native app wrapper 122 | - **File-based storage**: JSON files for data persistence 123 | - **Small bundle size**: Efficient Rust backend 124 |
- **Cross-platform**: Works on Windows, macOS, and Linux 125 | 126 | ### Data Persistence 127 | The application stores data in the following locations: 128 | - **Session data**: Current timer state and progress 129 | - **Tasks**: User-created task list 130 | - **Statistics**: Daily and weekly productivity stats 131 | - **History**: Historical session data 132 | 133 | ## 🎯 The Pomodoro Technique 134 | 135 | The Pomodoro Technique is a time management method developed by Francesco Cirillo: 136 | 137 | 1. **Choose a task** to work on 138 | 2. **Set timer for 25 minutes** (one "Pomodoro") 139 | 3. **Work on the task** until timer rings 140 | 4. **Take a 5-minute break** 141 | 5. **Repeat steps 1-4** 142 | 6. **After 4 Pomodoros**, take a longer 20-minute break 143 | 144 | ### Benefits 145 | - Improved focus and concentration 146 | - Better time estimation skills 147 | - Reduced mental fatigue 148 | - Enhanced productivity 149 | - Better work-life balance 150 | 151 | ## 🛠️ Development 152 | 153 | ### Available Scripts 154 | - `npm run tauri dev` - Start development server 155 | - `npm run tauri build` - Build production app 156 | - `cargo check` - Check Rust code (in src-tauri/) 157 | - `cargo test` - Run Rust tests (in src-tauri/) 158 | 159 | ### Recommended IDE Setup 160 | - [VS Code](https://code.visualstudio.com/) 161 | - [Tauri Extension](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) 162 | - [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) 163 | 164 | ## 📱 Platform Support 165 | 166 | - **macOS** (10.13+) 167 | - _**Windows** (coming soon TBA)_ 168 | - _**Linux** (coming soon TBA)_ 169 | 170 | ## 🤝 Contributing 171 | 172 | 1. Fork the repository 173 | 2. Create a feature branch (`git checkout -b feature/amazing-feature`) 174 | 3. Commit your changes (`git commit -m 'Add amazing feature'`) 175 | 4. Push to the branch (`git push origin feature/amazing-feature`) 176 | 5. Open a Pull Request 177 | 178 | ## 📄 License 179 | 180 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 181 | 182 | ## 🙏 Acknowledgments 183 | 184 | - [Francesco Cirillo](https://francescocirillo.com/) for creating the Pomodoro Technique 185 | - [Tauri](https://tauri.app/) for the amazing framework 186 | - The Rust and web development communities 187 | 188 | --- 189 | 190 | **Start your productive journey with Presto!** 🍅✨ 191 | 192 | ## 🔄 Automatic Updates 193 | 194 | Presto includes an automatic update system that allows you to receive new versions directly from the app interface. 195 | 196 | ### Features 197 | 198 | - **Automatic checking**: The app checks every hour for available updates 199 | - **Non-invasive notifications**: Elegant notification that appears when an update is available 200 | - **Progressive download**: Progress bar during download 201 | - **Automatic installation**: Update is applied on restart 202 | - **Security**: All updates are digitally signed 203 | 204 | ### Developer Configuration 205 | 206 | If you want to configure the update system for your fork: 207 | 208 | 1. **Automatic setup**: 209 | ```bash 210 | ./setup-updates.sh 211 | ``` 212 | 213 | 2. **Manual setup**: 214 | - Generate keys: `./generate-keys.sh` 215 | - Configure `src-tauri/tauri.conf.json` with your public key 216 | - Add GitHub secrets for the private key 217 | - Update repository references in the code 218 | 219 | 3. **Publishing**: 220 | ```bash 221 | git tag v1.0.0 222 | git push origin v1.0.0 223 | ``` 224 | 225 | For more details see [UPDATES.md](UPDATES.md). 226 | 227 | ### For Users 228 | 229 | - Updates are checked automatically 230 | - You can disable automatic checking in settings 231 | - You can manually check in the "Updates" section of settings 232 | - Downloads happen in background without interrupting work 233 | -------------------------------------------------------------------------------- /art/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/murdercode/presto/6a9c643a6bc6f698cecdb54cffed66d1ac8187a4/art/banner.png -------------------------------------------------------------------------------- /build-themes.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // Build script to auto-discover themes and update theme-loader.js 3 | 4 | import fs from 'fs'; 5 | import path from 'path'; 6 | import { fileURLToPath } from 'url'; 7 | 8 | const __filename = fileURLToPath(import.meta.url); 9 | const __dirname = path.dirname(__filename); 10 | 11 | // Paths 12 | const themesDir = path.join(__dirname, 'src/styles/themes'); 13 | const themeLoaderPath = path.join(__dirname, 'src/utils/theme-loader.js'); 14 | 15 | function discoverThemeFiles() { 16 | try { 17 | if (!fs.existsSync(themesDir)) { 18 | console.log('❌ Themes directory not found:', themesDir); 19 | return []; 20 | } 21 | 22 | const files = fs.readdirSync(themesDir); 23 | const themeFiles = files 24 | .filter(file => file.endsWith('.css')) 25 | .filter(file => !file.startsWith('.')) // Ignore hidden files 26 | .sort(); 27 | 28 | console.log(`🎨 Discovered ${themeFiles.length} theme files:`, themeFiles); 29 | return themeFiles; 30 | } catch (error) { 31 | console.error('❌ Error discovering theme files:', error); 32 | return []; 33 | } 34 | } 35 | 36 | function updateThemeLoader(themeFiles) { 37 | try { 38 | let content = fs.readFileSync(themeLoaderPath, 'utf-8'); 39 | 40 | // Create the new themes array 41 | const themesArray = themeFiles.map(file => `'${file}'`).join(',\n '); 42 | 43 | // Replace the knownThemes array in the file 44 | const newKnownThemes = ` const knownThemes = [ 45 | ${themesArray} 46 | ];`; 47 | 48 | // Find and replace the knownThemes array 49 | const knownThemesRegex = /const knownThemes = \[[\s\S]*?\];/; 50 | 51 | if (knownThemesRegex.test(content)) { 52 | content = content.replace(knownThemesRegex, newKnownThemes); 53 | 54 | fs.writeFileSync(themeLoaderPath, content, 'utf-8'); 55 | console.log('✅ Updated theme-loader.js with discovered themes'); 56 | return true; 57 | } else { 58 | console.log('⚠️ Could not find knownThemes array in theme-loader.js'); 59 | return false; 60 | } 61 | } catch (error) { 62 | console.error('❌ Error updating theme-loader.js:', error); 63 | return false; 64 | } 65 | } 66 | 67 | function main() { 68 | console.log('🔧 Starting theme discovery build script...'); 69 | 70 | const themeFiles = discoverThemeFiles(); 71 | 72 | if (themeFiles.length === 0) { 73 | console.log('⚠️ No theme files discovered'); 74 | return; 75 | } 76 | 77 | const success = updateThemeLoader(themeFiles); 78 | 79 | if (success) { 80 | console.log('🎉 Theme discovery build completed successfully!'); 81 | console.log(`📊 Total themes: ${themeFiles.length}`); 82 | themeFiles.forEach((file, index) => { 83 | console.log(` ${index + 1}. ${file}`); 84 | }); 85 | } else { 86 | console.log('❌ Theme discovery build failed'); 87 | process.exit(1); 88 | } 89 | } 90 | 91 | // Run the script 92 | main(); 93 | -------------------------------------------------------------------------------- /debug-themes.js: -------------------------------------------------------------------------------- 1 | // Debug script for theme verification 2 | 3 | console.log('=== TEMA DEBUG INFO ==='); 4 | 5 | const html = document.documentElement; 6 | console.log('data-theme:', html.getAttribute('data-theme')); 7 | console.log('data-timer-theme:', html.getAttribute('data-timer-theme')); 8 | 9 | const computedStyle = getComputedStyle(html); 10 | console.log('CSS Variables:'); 11 | console.log('--focus-color:', computedStyle.getPropertyValue('--focus-color').trim()); 12 | console.log('--focus-bg:', computedStyle.getPropertyValue('--focus-bg').trim()); 13 | console.log('--focus-timer-color:', computedStyle.getPropertyValue('--focus-timer-color').trim()); 14 | console.log('--matrix-text:', computedStyle.getPropertyValue('--matrix-text').trim()); 15 | 16 | console.log('localStorage theme-preference:', localStorage.getItem('theme-preference')); 17 | console.log('localStorage timer-theme-preference:', localStorage.getItem('timer-theme-preference')); 18 | 19 | console.log('System prefers dark:', window.matchMedia('(prefers-color-scheme: dark)').matches); 20 | 21 | const testElement = document.createElement('div'); 22 | testElement.style.cssText = 'color: var(--matrix-text, red);'; 23 | document.body.appendChild(testElement); 24 | const matrixColor = getComputedStyle(testElement).color; 25 | document.body.removeChild(testElement); 26 | console.log('Matrix text color test:', matrixColor); 27 | 28 | console.log('Stylesheets loaded:'); 29 | Array.from(document.styleSheets).forEach((sheet, i) => { 30 | try { 31 | console.log(`${i}: ${sheet.href || 'inline'}`); 32 | } catch (e) { 33 | console.log(`${i}: (CORS protected)`); 34 | } 35 | }); 36 | 37 | console.log('=== END DEBUG ==='); 38 | -------------------------------------------------------------------------------- /generate-keys.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script per generare le chiavi di firma per gli aggiornamenti Tauri 4 | # Questo script deve essere eseguito nella directory del progetto 5 | 6 | echo "🔐 Generating update signing keys for Tauri..." 7 | 8 | # Verifica se npm è disponibile 9 | if ! command -v npm &> /dev/null; then 10 | echo "❌ npm not found. Please install Node.js first." 11 | exit 1 12 | fi 13 | 14 | # Controlla se è stata passata l'opzione --force 15 | FORCE_OPTION="" 16 | if [[ "$@" == *"--force"* ]]; then 17 | FORCE_OPTION="--force" 18 | echo "⚠️ Force option detected. Will overwrite existing keys." 19 | fi 20 | 21 | # Genera le chiavi di firma 22 | echo "📝 Generating signing keypair..." 23 | npx tauri signer generate -w ~/.tauri/tempo_signing_key $FORCE_OPTION 24 | 25 | if [ $? -eq 0 ]; then 26 | echo "✅ Keys generated successfully!" 27 | echo "" 28 | echo "🔑 Your public key is:" 29 | npx tauri signer sign -k ~/.tauri/tempo_signing_key --password "" | head -1 30 | echo "" 31 | echo "📋 Next steps:" 32 | echo "1. Copy the public key above" 33 | echo "2. Replace 'YOUR_PUBLIC_KEY_HERE' in src-tauri/tauri.conf.json with your public key" 34 | echo "3. Keep your private key secure (~/.tauri/tempo_signing_key)" 35 | echo "4. Add the private key to your GitHub Actions secrets as TAURI_SIGNING_PRIVATE_KEY" 36 | echo "" 37 | echo "⚠️ Important: Never commit your private key to version control!" 38 | else 39 | echo "❌ Failed to generate keys" 40 | exit 1 41 | fi 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "presto", 3 | "private": true, 4 | "version": "0.4.4", 5 | "type": "module", 6 | "scripts": { 7 | "tauri": "tauri", 8 | "dev": "tauri dev", 9 | "build": "tauri build", 10 | "build-themes": "node build-themes.js", 11 | "prebuild": "npm run build-themes", 12 | "predev": "npm run build-themes" 13 | }, 14 | "devDependencies": { 15 | "@tauri-apps/cli": "^2.5.0" 16 | }, 17 | "dependencies": { 18 | "@aptabase/tauri": "^0.4.1", 19 | "@tauri-apps/api": "^2.5.0", 20 | "@tauri-apps/plugin-dialog": "^2.2.2", 21 | "@tauri-apps/plugin-notification": "^2", 22 | "@tauri-apps/plugin-opener": "^2", 23 | "@tauri-apps/plugin-process": "^2", 24 | "@tauri-apps/plugin-updater": "^2.7.1", 25 | "xlsx": "^0.18.5" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /setup-updates.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script di configurazione per Tempo Update System 4 | # Questo script ti guiderà attraverso la configurazione degli aggiornamenti automatici 5 | 6 | echo "🍅 Tempo - Configurazione Sistema Aggiornamenti" 7 | echo "==================================================" 8 | echo "" 9 | 10 | # Funzione per richiedere input 11 | read_input() { 12 | local prompt="$1" 13 | local variable_name="$2" 14 | local default_value="$3" 15 | 16 | if [ -n "$default_value" ]; then 17 | read -p "$prompt [$default_value]: " input 18 | if [ -z "$input" ]; then 19 | input="$default_value" 20 | fi 21 | else 22 | read -p "$prompt: " input 23 | fi 24 | 25 | eval "$variable_name='$input'" 26 | } 27 | 28 | # Raccolta informazioni 29 | echo "1. Configurazione Repository GitHub" 30 | echo "-----------------------------------" 31 | read_input "Username GitHub" github_username 32 | read_input "Nome Repository" github_repo "tempo" 33 | 34 | echo "" 35 | echo "2. Configurazione Chiavi" 36 | echo "------------------------" 37 | read_input "Nome file chiave" key_name "tempo_signing_key" 38 | 39 | # Creazione della directory per le chiavi 40 | key_dir="$HOME/.tauri" 41 | mkdir -p "$key_dir" 42 | 43 | echo "" 44 | echo "📝 Generazione chiavi di firma..." 45 | 46 | # Controlla se tauri CLI è disponibile 47 | if ! command -v tauri &> /dev/null; then 48 | echo "❌ Tauri CLI non trovato. Installandolo..." 49 | npm install --save-dev @tauri-apps/cli@latest 50 | 51 | if ! command -v npx &> /dev/null; then 52 | echo "❌ NPM non trovato. Installa Node.js prima di continuare." 53 | exit 1 54 | fi 55 | 56 | # Usa npx se tauri non è nel PATH 57 | TAURI_CMD="npx tauri" 58 | else 59 | TAURI_CMD="tauri" 60 | fi 61 | 62 | # Genera le chiavi 63 | echo "🔑 Generazione keypair..." 64 | $TAURI_CMD signer generate -w "$key_dir/$key_name" 65 | 66 | if [ $? -eq 0 ]; then 67 | echo "✅ Chiavi generate con successo!" 68 | 69 | # Ottieni la chiave pubblica 70 | echo "" 71 | echo "🔑 La tua chiave pubblica è:" 72 | echo "----------------------------------------" 73 | public_key=$($TAURI_CMD signer sign -k "$key_dir/$key_name" --password "" 2>/dev/null | head -1) 74 | echo "$public_key" 75 | echo "----------------------------------------" 76 | 77 | # Aggiorna tauri.conf.json 78 | echo "" 79 | echo "📝 Aggiornamento configurazione..." 80 | 81 | # Sostituisce i placeholder nel file di configurazione 82 | config_file="src-tauri/tauri.conf.json" 83 | if [ -f "$config_file" ]; then 84 | # Backup del file originale 85 | cp "$config_file" "$config_file.backup" 86 | 87 | # Sostituzioni 88 | sed -i.tmp "s/{{OWNER}}/$github_username/g" "$config_file" 89 | sed -i.tmp "s/{{REPO}}/$github_repo/g" "$config_file" 90 | sed -i.tmp "s/YOUR_PUBLIC_KEY_HERE/$public_key/g" "$config_file" 91 | rm "$config_file.tmp" 2>/dev/null 92 | 93 | echo "✅ Configurazione aggiornata!" 94 | else 95 | echo "⚠️ File $config_file non trovato" 96 | fi 97 | 98 | # Aggiorna il main.js con i link del repository 99 | main_js_file="src/main.js" 100 | if [ -f "$main_js_file" ]; then 101 | sed -i.tmp "s/YOUR_USERNAME/$github_username/g" "$main_js_file" 102 | sed -i.tmp "s/YOUR_REPO/$github_repo/g" "$main_js_file" 103 | rm "$main_js_file.tmp" 2>/dev/null 104 | echo "✅ Link repository aggiornati!" 105 | fi 106 | 107 | # Aggiorna l'update manager 108 | update_manager_file="src/managers/update-manager.js" 109 | if [ -f "$update_manager_file" ]; then 110 | sed -i.tmp "s/USERNAME\/REPOSITORY/$github_username\/$github_repo/g" "$update_manager_file" 111 | rm "$update_manager_file.tmp" 2>/dev/null 112 | echo "✅ Update manager configurato!" 113 | fi 114 | 115 | echo "" 116 | echo "🎉 Configurazione completata!" 117 | echo "" 118 | echo "📋 Prossimi passi:" 119 | echo "1. Aggiungi questi secrets al tuo repository GitHub:" 120 | echo " - TAURI_SIGNING_PRIVATE_KEY: (contenuto di $key_dir/$key_name)" 121 | echo " - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: (lascia vuoto se non hai impostato una password)" 122 | echo "" 123 | echo "2. Per ottenere la chiave privata:" 124 | echo " cat $key_dir/$key_name" 125 | echo "" 126 | echo "3. Crea una release su GitHub per testare gli aggiornamenti:" 127 | echo " git tag v0.2.0" 128 | echo " git push origin v0.2.0" 129 | echo "" 130 | echo "4. L'app controllerà automaticamente gli aggiornamenti all'avvio" 131 | echo "" 132 | echo "⚠️ IMPORTANTE: Non committare mai la chiave privata nel repository!" 133 | echo " La chiave è salvata in: $key_dir/$key_name" 134 | 135 | else 136 | echo "❌ Errore nella generazione delle chiavi" 137 | exit 1 138 | fi 139 | -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Generated by Tauri 6 | # will have schema files for capabilities auto-completion 7 | /gen/schemas 8 | -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "presto" 3 | version = "0.4.4" 4 | description = "A Tauri App" 5 | authors = ["Stefano Novelli"] 6 | edition = "2021" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [lib] 11 | # The `_lib` suffix may seem redundant but it is necessary 12 | # to make the lib name unique and wouldn't conflict with the bin name. 13 | # This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 14 | name = "presto_lib" 15 | crate-type = ["staticlib", "cdylib", "rlib"] 16 | 17 | # Ottimizzazioni per compilazione veloce 18 | [profile.dev] 19 | opt-level = 0 20 | debug = true 21 | split-debuginfo = "unpacked" 22 | 23 | [profile.release] 24 | opt-level = "s" # Ottimizza per dimensione invece che velocità 25 | lto = "thin" # Link time optimization leggero 26 | codegen-units = 1 27 | panic = "abort" 28 | strip = true # Rimuove simboli debug 29 | 30 | [build-dependencies] 31 | tauri-build = { version = "2", features = [] } 32 | 33 | [dependencies] 34 | tauri = { version = "2", features = ["tray-icon"] } 35 | tauri-plugin-opener = { version = "2", default-features = false } 36 | tauri-plugin-global-shortcut = { version = "2", default-features = false } 37 | tauri-plugin-dialog = { version = "2", default-features = false } 38 | tauri-plugin-notification = { version = "2", default-features = false } 39 | tauri-plugin-autostart = { version = "2", default-features = false } 40 | tauri-plugin-updater = { version = "2", default-features = false } 41 | tauri-plugin-process = { version = "2", default-features = false } 42 | tauri-plugin-oauth = { git = "https://github.com/FabianLars/tauri-plugin-oauth", branch = "v2" } 43 | tauri-plugin-aptabase = "1.0.0" 44 | serde = { version = "1", features = ["derive"], default-features = false } 45 | serde_json = { version = "1", default-features = false } 46 | chrono = { version = "0.4", features = ["serde", "clock"], default-features = false } 47 | dotenv = "0.15" 48 | base64 = "0.21" 49 | 50 | [target.'cfg(target_os = "macos")'.dependencies] 51 | core-graphics = "0.23" 52 | core-foundation = "0.9" 53 | libc = "0.2" 54 | cocoa = "0.25" 55 | objc = "0.2" 56 | 57 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/capabilities/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../gen/schemas/desktop-schema.json", 3 | "identifier": "default", 4 | "description": "Capability for the main window", 5 | "windows": [ 6 | "main" 7 | ], 8 | "permissions": [ 9 | "core:default", 10 | "core:path:default", 11 | "core:event:default", 12 | "core:app:default", 13 | "core:app:allow-version", 14 | "core:webview:default", 15 | "core:webview:allow-internal-toggle-devtools", 16 | "opener:default", 17 | "opener:allow-open-url", 18 | "notification:default", 19 | "notification:allow-is-permission-granted", 20 | "notification:allow-request-permission", 21 | "notification:allow-notify", 22 | "dialog:default", 23 | "dialog:allow-ask", 24 | "dialog:allow-message", 25 | "global-shortcut:default", 26 | "global-shortcut:allow-register", 27 | "global-shortcut:allow-unregister", 28 | "global-shortcut:allow-is-registered", 29 | "autostart:default", 30 | "autostart:allow-enable", 31 | "autostart:allow-disable", 32 | "autostart:allow-is-enabled", 33 | "updater:default", 34 | "updater:allow-check", 35 | "updater:allow-download-and-install", 36 | "oauth:default", 37 | "oauth:allow-start", 38 | "oauth:allow-cancel" 39 | ] 40 | } -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/murdercode/presto/6a9c643a6bc6f698cecdb54cffed66d1ac8187a4/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/murdercode/presto/6a9c643a6bc6f698cecdb54cffed66d1ac8187a4/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/murdercode/presto/6a9c643a6bc6f698cecdb54cffed66d1ac8187a4/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/murdercode/presto/6a9c643a6bc6f698cecdb54cffed66d1ac8187a4/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/murdercode/presto/6a9c643a6bc6f698cecdb54cffed66d1ac8187a4/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/murdercode/presto/6a9c643a6bc6f698cecdb54cffed66d1ac8187a4/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/murdercode/presto/6a9c643a6bc6f698cecdb54cffed66d1ac8187a4/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/murdercode/presto/6a9c643a6bc6f698cecdb54cffed66d1ac8187a4/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/murdercode/presto/6a9c643a6bc6f698cecdb54cffed66d1ac8187a4/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/murdercode/presto/6a9c643a6bc6f698cecdb54cffed66d1ac8187a4/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/murdercode/presto/6a9c643a6bc6f698cecdb54cffed66d1ac8187a4/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/murdercode/presto/6a9c643a6bc6f698cecdb54cffed66d1ac8187a4/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/murdercode/presto/6a9c643a6bc6f698cecdb54cffed66d1ac8187a4/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/murdercode/presto/6a9c643a6bc6f698cecdb54cffed66d1ac8187a4/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/murdercode/presto/6a9c643a6bc6f698cecdb54cffed66d1ac8187a4/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/murdercode/presto/6a9c643a6bc6f698cecdb54cffed66d1ac8187a4/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | fn main() { 5 | presto_lib::run() 6 | } 7 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.tauri.app/config/2", 3 | "productName": "presto", 4 | "version": "0.4.4", 5 | "identifier": "com.presto.app", 6 | "build": { 7 | "frontendDist": "../src" 8 | }, 9 | "app": { 10 | "withGlobalTauri": true, 11 | "windows": [ 12 | { 13 | "title": "presto", 14 | "width": 800, 15 | "height": 600, 16 | "closable": true, 17 | "hiddenTitle": true, 18 | "minimizable": true, 19 | "resizable": true 20 | } 21 | ], 22 | "security": { 23 | "csp": null 24 | } 25 | }, 26 | "bundle": { 27 | "active": true, 28 | "targets": [ 29 | "app" 30 | ], 31 | "createUpdaterArtifacts": true, 32 | "icon": [ 33 | "icons/32x32.png", 34 | "icons/128x128.png", 35 | "icons/128x128@2x.png", 36 | "icons/icon.icns", 37 | "icons/icon.ico" 38 | ] 39 | }, 40 | "plugins": { 41 | "updater": { 42 | "endpoints": [ 43 | "https://github.com/murdercode/presto/releases/latest/download/latest-{target}.json" 44 | ], 45 | "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IENCNDYxMDAyQjIxQkM4MjAKUldRZ3lCdXlBaEJHeXp3bnRHYWRwOVM2WFo1T3IzMXJWUnlJYUtwWlEzcnBKVHpnaXpJNjhmK1gK", 46 | "dangerousInsecureTransportProtocol": false 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json.backup: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.tauri.app/config/2", 3 | "productName": "tempo", 4 | "version": "0.1.0", 5 | "identifier": "com.tempo.app", 6 | "build": { 7 | "frontendDist": "../src" 8 | }, 9 | "app": { 10 | "withGlobalTauri": true, 11 | "windows": [ 12 | { 13 | "title": "tempo", 14 | "width": 800, 15 | "height": 600, 16 | "closable": true, 17 | "hiddenTitle": false, 18 | "minimizable": true, 19 | "resizable": true 20 | } 21 | ], 22 | "security": { 23 | "csp": null 24 | }, 25 | "trayIcon": { 26 | "iconPath": "icons/32x32.png", 27 | "iconAsTemplate": true 28 | } 29 | }, 30 | "bundle": { 31 | "targets": "all", 32 | "icon": [ 33 | "icons/32x32.png", 34 | "icons/128x128.png", 35 | "icons/128x128@2x.png", 36 | "icons/icon.icns", 37 | "icons/icon.ico" 38 | ] 39 | }, 40 | "plugins": { 41 | "updater": { 42 | "endpoints": [ 43 | "https://api.github.com/repos/{{OWNER}}/{{REPO}}/releases/latest" 44 | ], 45 | "pubkey": "YOUR_PUBLIC_KEY_HERE" 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/assets/javascript.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/tauri.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/docs/TEST_MANUAL.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/murdercode/presto/6a9c643a6bc6f698cecdb54cffed66d1ac8187a4/src/docs/TEST_MANUAL.md -------------------------------------------------------------------------------- /src/docs/THEMES.md: -------------------------------------------------------------------------------- 1 | # 🎨 Automatic Theme Management System 2 | 3 | This system allows you to add new themes to the Pomodoro timer **without manually modifying the code**. Themes are automatically discovered and loaded. 4 | 5 | ## 🚀 How to Add a New Theme 6 | 7 | ### 1. Create the Theme CSS File 8 | 9 | Create a new CSS file in the `src/styles/themes/` folder with the following format: 10 | 11 | ```css 12 | /* Timer Theme: [Theme Name] 13 | * Author: [Your Name] 14 | * Description: [Theme description] 15 | * Supports: [Light mode only / Dark mode only / Light + Dark mode] 16 | */ 17 | 18 | /* Font imports if needed */ 19 | @import url('https://fonts.googleapis.com/css2?family=...'); 20 | 21 | /* Theme color definitions */ 22 | :root[data-timer-theme="[theme-id]"] { 23 | /* Timer Colors */ 24 | --focus-color: #focus-color; 25 | --break-color: #break-color; 26 | --long-break-color: #long-break-color; 27 | 28 | /* Background Colors */ 29 | --focus-bg: #focus-background; 30 | --break-bg: #break-background; 31 | --long-break-bg: #long-break-background; 32 | 33 | /* Timer Text Colors */ 34 | --focus-timer-color: #focus-text; 35 | --break-timer-color: #break-text; 36 | --long-break-timer-color: #long-break-text; 37 | 38 | /* Button Colors */ 39 | --focus-primary-btn: #focus-button; 40 | --break-primary-btn: #break-button; 41 | --long-break-primary-btn: #long-break-button; 42 | 43 | --focus-secondary-btn: #focus-secondary-button; 44 | --break-secondary-btn: #break-secondary-button; 45 | --long-break-secondary-btn: #long-break-secondary-button; 46 | } 47 | 48 | /* Custom theme styles */ 49 | :root[data-timer-theme="[theme-id]"] .timer-minutes, 50 | :root[data-timer-theme="[theme-id]"] .timer-seconds { 51 | /* Timer number customizations */ 52 | } 53 | 54 | /* Other custom styles... */ 55 | ``` 56 | 57 | ### 2. Examples of Existing Themes 58 | 59 | - **`espresso.css`** - Default theme with warm colors 60 | - **`pommodore64.css`** - Retro theme inspired by Commodore 64 61 | - **`pipboy.css`** - Theme inspired by Fallout (PipBoy) 62 | 63 | ### 3. The System Does the Rest! 64 | 65 | Once you create the CSS file: 66 | 67 | 1. **Automatic Discovery**: The system automatically discovers the new theme 68 | 2. **Dynamic Loading**: CSS is loaded dynamically 69 | 3. **Registration**: The theme is automatically registered 70 | 4. **Availability**: Appears immediately in the theme selector 71 | 72 | ## 🛠️ Technical System 73 | 74 | ### Automatic Build Script 75 | 76 | The `build-themes.js` file automatically scans the `src/styles/themes/` folder and updates the list of available themes. 77 | 78 | **Automatic execution:** 79 | - Before `npm run dev` 80 | - Before `npm run build` 81 | - Manually with `npm run build-themes` 82 | 83 | ### Theme Loader 84 | 85 | The `src/utils/theme-loader.js` handles: 86 | - Automatic discovery of CSS files 87 | - Dynamic theme loading 88 | - Automatic metadata extraction 89 | - System registration 90 | 91 | ### Automatic Metadata 92 | 93 | The system automatically extracts: 94 | - **Name** from "Timer Theme:" comment 95 | - **Description** from "Description:" comment 96 | - **Supported modes** from "Supports:" comment 97 | - **Preview colors** from CSS variables `--focus-color`, `--break-color`, `--long-break-color` 98 | 99 | ## 📝 Metadata Structure 100 | 101 | Metadata is extracted from CSS comments: 102 | 103 | ```css 104 | /* Timer Theme: Beautiful Name 105 | * Author: Your Name 106 | * Description: An engaging theme description 107 | * Supports: Light + Dark mode 108 | */ 109 | ``` 110 | 111 | **Supported values for "Supports":** 112 | - `Light mode only` - Light mode only 113 | - `Dark mode only` - Dark mode only 114 | - `Light + Dark mode` - Both modes 115 | 116 | ## 🎯 System Benefits 117 | 118 | ### ✅ For Developers 119 | - **Zero configuration** - add a CSS file and it works 120 | - **No code changes** - no manual imports 121 | - **Automatic metadata** - extracted from CSS comments 122 | - **Hot reload** - works with dev server 123 | 124 | ### ✅ For Designers 125 | - **Focus on creativity** - concentrate on colors and styles 126 | - **Clear examples** - follow existing theme structure 127 | - **Immediate preview** - see results instantly 128 | - **Visual feedback** - mode compatibility shown automatically 129 | 130 | ### ✅ For Users 131 | - **More choice** - always updated themes 132 | - **Clean interface** - automatic selection for compatibility 133 | - **Smooth experience** - instant theme switching 134 | 135 | ## 🔄 Development Workflow 136 | 137 | 1. **Create** `src/styles/themes/my-theme.css` 138 | 2. **Develop** using existing examples 139 | 3. **Test** with `npm run dev` (auto-reload) 140 | 4. **Share** - the theme is ready! 141 | 142 | ## 🎨 Quick Theme Template 143 | 144 | Copy and customize this template: 145 | 146 | ```css 147 | /* Timer Theme: My Theme 148 | * Author: My Name 149 | * Description: My description 150 | * Supports: Light + Dark mode 151 | */ 152 | 153 | :root[data-timer-theme="my-theme"] { 154 | --focus-color: #e74c3c; 155 | --break-color: #2ecc71; 156 | --long-break-color: #3498db; 157 | 158 | --focus-bg: #FFF2F2; 159 | --break-bg: #F0FAF0; 160 | --long-break-bg: #E8F4FF; 161 | 162 | --focus-timer-color: #471515; 163 | --break-timer-color: #14401D; 164 | --long-break-timer-color: #153047; 165 | 166 | --focus-primary-btn: #FF7c7c; 167 | --break-primary-btn: #8CE8A1; 168 | --long-break-primary-btn: #8BCAFF; 169 | 170 | --focus-secondary-btn: #FFD9D9; 171 | --break-secondary-btn: #DAFAE0; 172 | --long-break-secondary-btn: #D9EEFF; 173 | } 174 | ``` 175 | 176 | --- 177 | 178 | **🎉 Have fun creating fantastic themes!** 179 | -------------------------------------------------------------------------------- /src/docs/UPDATER_SETUP.md: -------------------------------------------------------------------------------- 1 | # Setup Aggiornamenti Sicuri per macOS - VERSIONE CORRETTA 2 | 3 | ## ⚠️ IMPORTANTE: Sistema Ibrido Implementato 4 | 5 | Questo progetto usa un **approccio ibrido** per gestire gli aggiornamenti: 6 | 7 | 1. **Controllo Versione**: Usa GitHub API per controllare se c'è una nuova versione 8 | 2. **Download/Install**: Usa l'API ufficiale Tauri quando possibile 9 | 3. **Fallback**: Se l'API Tauri fallisce, guida l'utente al download manuale 10 | 11 | ### Perché questo approccio? 12 | 13 | L'API GitHub restituisce un formato JSON diverso da quello richiesto da Tauri v2: 14 | 15 | **GitHub API** → `{"tag_name": "v0.2.2", "assets": [...], ...}` 16 | **Tauri richiede** → `{"version": "0.2.2", "platforms": {"darwin-x86_64": {...}}, ...}` 17 | 18 | Il nostro UpdateManager gestisce questa conversione automaticamente. 19 | 20 | ## 🔧 Configurazione Attuale 21 | 22 | Il sistema è già configurato e funzionante: 23 | 24 | ✅ **tauri.conf.json**: Configurato per usare GitHub API 25 | ✅ **UpdateManager**: Gestisce conversione formato + fallback 26 | ✅ **UI**: Integrata con progress bar e notifiche 27 | ✅ **Test Mode**: Disponibile per sviluppo 28 | 29 | ## 🧪 Test Immediato 30 | 31 | Puoi testare subito il sistema: 32 | 33 | ```javascript 34 | // Apri console browser e prova: 35 | 36 | // Test simulato (sempre disponibile) 37 | window.updateManagerV2Debug.testUpdate() 38 | 39 | // Test reale (controlla GitHub per aggiornamenti veri) 40 | window.updateManagerV2Debug.checkRealUpdate() 41 | 42 | // Verifica stato 43 | window.updateManagerV2Debug.getStatus() 44 | ``` 45 | 46 | ## � Cosa Succede Durante un Aggiornamento 47 | 48 | ### 1. **Controllo Automatico** 49 | - ✅ Ogni ora (se abilitato) 50 | - ✅ All'avvio dell'app (dopo 30 secondi) 51 | - ✅ Manuale con pulsante "Check for Updates" 52 | 53 | ### 2. **Processo di Verifica** 54 | 1. Controlla GitHub API per l'ultima release 55 | 2. Confronta con versione locale 56 | 3. Se disponibile, prova API Tauri per download automatico 57 | 4. Se API Tauri fallisce, offre download manuale 58 | 59 | ### 3. **Download e Installazione** 60 | - **Automatico**: Se l'API Tauri funziona 61 | - **Manuale**: Se necessario, apre pagina download 62 | - **Progress**: Barra di progresso in tempo reale 63 | - **Riavvio**: Automatico dopo installazione 64 | 65 | ## 🔐 Setup Chiavi di Firma (Opzionale) 66 | 67 | Per aggiornamenti completamente automatici, genera le chiavi: 68 | 69 | ```bash 70 | npm run tauri signer generate -- -w ~/.tauri/presto.key 71 | ``` 72 | 73 | Poi aggiorna la `pubkey` in `tauri.conf.json` con il contenuto di `~/.tauri/presto.key.pub`. 74 | 75 | **NOTA**: Anche senza chiavi, il sistema funziona con download manuale. 76 | 77 | ## 🔍 Verifica Sistema 78 | 79 | ```bash 80 | # Verifica configurazione 81 | npm run dev 82 | 83 | # In console browser: 84 | window.updateManagerV2Debug.getStatus() 85 | ``` 86 | 87 | **Output atteso**: 88 | ``` 89 | { 90 | updateAvailable: false, 91 | isChecking: false, 92 | developmentMode: true, // true in dev, false in prod 93 | version: "v2-corrected" // conferma versione corretta 94 | } 95 | ``` 96 | 97 | ## 🚀 Deploy 98 | 99 | 1. **Build**: `npm run build` 100 | 2. **Test**: Installa l'app e prova il controllo aggiornamenti 101 | 3. **Release**: Pubblica su GitHub Releases (con file .app.tar.gz) 102 | 103 | ## 📁 File Necessari per Release 104 | 105 | Quando crei una release su GitHub, assicurati di includere: 106 | 107 | - `presto.app.tar.gz` (generato da `npm run build`) 108 | - `presto.app.tar.gz.sig` (se hai le chiavi di firma) 109 | 110 | Il sistema li troverà automaticamente. 111 | 112 | ## 🔧 Risoluzione Problemi 113 | 114 | ### "Aggiornamento non disponibile" 115 | - ✅ Normale se sei all'ultima versione 116 | - ✅ Usa `testUpdate()` per simulare aggiornamento 117 | 118 | ### "Download manuale richiesto" 119 | - ✅ Normale se non hai configurato le chiavi di firma 120 | - ✅ Il sistema aprirà automaticamente la pagina di download 121 | 122 | ### "Errore di rete" 123 | - ✅ Verifica connessione Internet 124 | - ✅ Controlla se GitHub è accessibile 125 | 126 | ## 🎯 Stato Attuale: PRONTO 127 | 128 | Il sistema è **completamente funzionante** e pronto per l'uso: 129 | 130 | - ✅ Controllo automatico abilitato 131 | - ✅ UI integrata e funzionante 132 | - ✅ Fallback per download manuale 133 | - ✅ Modalità test per sviluppo 134 | - ✅ Compatibile con macOS 135 | 136 | **Non serve configurare nulla di più** - funziona subito! 137 | -------------------------------------------------------------------------------- /src/docs/UPDATES.md: -------------------------------------------------------------------------------- 1 | # Automatic Updates Configuration 2 | 3 | This guide will help you configure the automatic update system for your Presto application. 4 | 5 | ## 🔐 Step 1: Generate Signing Keys 6 | 7 | Before publishing releases with automatic updates, you need to generate a key pair to sign the updates: 8 | 9 | ```bash 10 | # Run the key generation script 11 | ./generate-keys.sh 12 | ``` 13 | 14 | This script will generate: 15 | - A **private key** (keep it secret!) 16 | - A **public key** (to be inserted in the configuration) 17 | 18 | ## 🔧 Step 2: Configure tauri.conf.json 19 | 20 | 1. Copy the public key generated in the previous step 21 | 2. Open `src-tauri/tauri.conf.json` 22 | 3. Replace the placeholders in the `plugins.updater` section: 23 | 24 | ```json 25 | { 26 | "plugins": { 27 | "updater": { 28 | "endpoints": [ 29 | "https://api.github.com/repos/YOUR_USERNAME/YOUR_REPOSITORY/releases/latest" 30 | ], 31 | "pubkey": "YOUR_PUBLIC_KEY_HERE" 32 | } 33 | } 34 | } 35 | ``` 36 | 37 | Replace: 38 | - `YOUR_USERNAME` with your GitHub username 39 | - `YOUR_REPOSITORY` with your repository name 40 | - `YOUR_PUBLIC_KEY_HERE` with the generated public key 41 | 42 | ## 🚀 Step 3: Configure GitHub Actions 43 | 44 | ### Add Secrets 45 | 46 | In your GitHub repository, go to **Settings** → **Secrets and variables** → **Actions** and add: 47 | 48 | 1. **TAURI_SIGNING_PRIVATE_KEY**: The generated private key (content of `~/.tauri/presto_signing_key` file) 49 | 2. **TAURI_SIGNING_PRIVATE_KEY_PASSWORD**: The key password (leave empty if you didn't set one) 50 | 51 | ### Verify the Workflow 52 | 53 | The `.github/workflows/release.yml` file is already configured to: 54 | - Build the app for all platforms (macOS, Windows, Linux) 55 | - Sign files with your private key 56 | - Create a release on GitHub 57 | - Automatically publish assets 58 | 59 | ## 📦 Step 4: Create a Release 60 | 61 | To trigger the build and release process: 62 | 63 | ```bash 64 | # Create and push a version tag 65 | git tag v0.2.0 66 | git push origin v0.2.0 67 | ``` 68 | 69 | Or create a release directly from GitHub. 70 | 71 | ## 🔄 How Updates Work 72 | 73 | ### For Users 74 | 75 | 1. The app automatically checks for updates on startup 76 | 2. If an update is found, a notification appears 77 | 3. Users can choose to download and install immediately 78 | 4. Download happens in background with a progress bar 79 | 5. Update is applied on app restart 80 | 81 | ### For Developers 82 | 83 | 1. When you publish a release on GitHub, files are automatically signed 84 | 2. The app checks the GitHub API endpoint for new releases 85 | 3. Verifies the signature for security 86 | 4. Downloads and installs the update automatically 87 | 88 | ## ⚙️ Advanced Configurations 89 | 90 | ### Automatic Checking 91 | 92 | Users can configure automatic checking in settings: 93 | - **Check for updates automatically**: Checks every hour 94 | - **Include pre-release versions**: Also beta/RC versions 95 | 96 | ### Custom Endpoints 97 | 98 | You can modify the update endpoint in `tauri.conf.json` for: 99 | - Private repositories 100 | - Custom distribution servers 101 | - Different release channels 102 | 103 | ### Example for Private Repository 104 | 105 | ```json 106 | { 107 | "plugins": { 108 | "updater": { 109 | "endpoints": [ 110 | "https://api.github.com/repos/username/private-repo/releases/latest" 111 | ], 112 | "pubkey": "your_public_key", 113 | "headers": { 114 | "Authorization": "token ghp_xxxxxxxxxxxx" 115 | } 116 | } 117 | } 118 | } 119 | ``` 120 | 121 | ## 🛡️ Security 122 | 123 | - **Never commit the private key** to the repository 124 | - Always use GitHub Secrets to store keys 125 | - Signatures ensure updates come from you 126 | - The app will reject unsigned or invalid signature updates 127 | 128 | ## 🐛 Troubleshooting 129 | 130 | ### App doesn't find updates 131 | 132 | 1. Verify that the repository URL in `tauri.conf.json` is correct 133 | 2. Make sure there's at least one published release on GitHub 134 | 3. Check console logs for errors 135 | 136 | ### Signature errors 137 | 138 | 1. Verify that the public key in `tauri.conf.json` matches the private one 139 | 2. Make sure GitHub Actions has access to the private key 140 | 3. Check that the key hasn't expired 141 | 142 | ### Download fails 143 | 144 | 1. Check internet connection 145 | 2. Verify that release files have been uploaded correctly 146 | 3. Check repository permissions (public vs private) 147 | 148 | ## 📚 References 149 | 150 | - [Tauri Updater Documentation](https://tauri.app/v1/guides/distribution/updater) 151 | - [GitHub Actions for Tauri](https://tauri.app/v1/guides/building/cross-platform) 152 | - [GitHub Release Management](https://docs.github.com/en/repositories/releasing-projects-on-github) 153 | 154 | ## 🔄 Updating this Configuration 155 | 156 | If you modify the update system: 157 | 158 | 1. Update this documentation file 159 | 2. Test the process with a trial release 160 | 3. Inform users of changes in release notes 161 | -------------------------------------------------------------------------------- /src/styles/README.md: -------------------------------------------------------------------------------- 1 | # CSS Architecture Documentation 2 | 3 | Questo documento descrive la nuova architettura CSS modulare per l'applicazione Pomodoro Timer. 4 | 5 | ## Struttura dei File 6 | 7 | ### 📁 `styles/` 8 | 9 | La cartella `styles` contiene tutti i file CSS organizzati per funzionalità: 10 | 11 | #### **Core Styles** 12 | - **`variables.css`** - Variabili CSS, colori, font e stili base 13 | - **`timer.css`** - Stili per il display del timer principale 14 | - **`controls.css`** - Pulsanti di controllo e loro stati 15 | - **`progress.css`** - Indicatori di progresso e punti pomodoro 16 | 17 | #### **UI Components** 18 | - **`tasks.css`** - Sezione gestione task 19 | - **`statistics.css`** - Sezione statistiche 20 | - **`modals.css`** - Modal dialogs e finestre popup 21 | - **`notifications.css`** - Sistema di notifiche 22 | 23 | #### **Layout & Navigation** 24 | - **`layout.css`** - Layout generale e container delle viste 25 | - **`sidebar.css`** - Barra laterale di navigazione 26 | 27 | #### **Views & Pages** 28 | - **`calendar.css`** - Vista calendario con grafici e timeline 29 | - **`settings.css`** - Pagina impostazioni e configurazioni 30 | - **`team.css`** - Vista team per collaborazione 31 | - **`timeline.css`** - Timeline sessioni e gestione sessioni 32 | 33 | #### **Effects & Responsive** 34 | - **`animations.css`** - Animazioni e transizioni 35 | - **`responsive.css`** - Design responsivo e stili mobile 36 | 37 | #### **Entry Point** 38 | - **`main.css`** - File principale che importa tutti i moduli 39 | 40 | ## Vantaggi della Nuova Architettura 41 | 42 | ### 🚀 **Manutenibilità** 43 | - Ogni file si occupa di una specifica funzionalità 44 | - Facile individuare e modificare stili specifici 45 | - Riduzione dei conflitti CSS 46 | 47 | ### 🎯 **Organizzazione** 48 | - Separazione logica delle responsabilità 49 | - Struttura prevedibile e intuitiva 50 | - Documentazione implicita tramite nomi dei file 51 | 52 | ### 📱 **Performance** 53 | - CSS caricato in modo modulare 54 | - Possibilità di ottimizzazioni future (lazy loading) 55 | - Bundle size ottimizzato 56 | 57 | ### 🔧 **Sviluppo** 58 | - Lavoro parallelo su componenti diversi 59 | - Conflitti Git ridotti 60 | - Debug più semplice 61 | 62 | ## Come Utilizzare 63 | 64 | ### Import nel HTML 65 | ```html 66 | 67 | ``` 68 | 69 | ### Modificare Stili Specifici 70 | - **Timer**: modifica `timer.css` 71 | - **Colori**: modifica `variables.css` 72 | - **Mobile**: modifica `responsive.css` 73 | - **Calendario**: modifica `calendar.css` 74 | 75 | ### Aggiungere Nuovi Stili 76 | 1. Identifica la categoria appropriata 77 | 2. Modifica il file CSS corrispondente 78 | 3. Se necessario, crea un nuovo file e aggiornalo in `main.css` 79 | 80 | ## Struttura delle Variabili CSS 81 | 82 | Le variabili principali sono definite in `variables.css`: 83 | 84 | ```css 85 | :root { 86 | /* Timer Colors */ 87 | --focus-color: #e74c3c; 88 | --break-color: #2ecc71; 89 | --long-break-color: #3498db; 90 | --accent-color: #9b59b6; 91 | 92 | /* Background Colors */ 93 | --focus-bg: #FFF2F2; 94 | --break-bg: #F0FAF0; 95 | --long-break-bg: #E8F4FF; 96 | 97 | /* Button Colors */ 98 | --focus-primary-btn: #FF7c7c; 99 | --break-primary-btn: #8CE8A1; 100 | --long-break-primary-btn: #8BCAFF; 101 | } 102 | ``` 103 | 104 | ## Best Practices 105 | 106 | ### 📝 **Convenzioni di Naming** 107 | - Usa nomi descrittivi per le classi 108 | - Prefissi per componenti specifici (es. `calendar-`, `timer-`) 109 | - Variabili CSS per valori riutilizzati 110 | 111 | ### 🎨 **Organizzazione del Codice** 112 | - Commenti per sezioni logiche 113 | - Raggruppamento di proprietà correlate 114 | - Uso consistente di indentazione 115 | 116 | ### 📱 **Responsività** 117 | - Mobile-first approach 118 | - Breakpoint consistenti 119 | - Progressive enhancement 120 | 121 | ## Migrazione dal Vecchio Sistema 122 | 123 | Il vecchio file `styles.css` è stato suddiviso mantenendo: 124 | - ✅ Tutti gli stili esistenti 125 | - ✅ Funzionalità invariate 126 | - ✅ Compatibilità completa 127 | - ✅ Performance migliorate 128 | 129 | ## File di Backup 130 | 131 | Il file originale `styles.css` rimane disponibile come backup e riferimento. 132 | -------------------------------------------------------------------------------- /src/styles/animations.css: -------------------------------------------------------------------------------- 1 | /* Animations and Transitions */ 2 | 3 | @keyframes pulse { 4 | 0% { 5 | transform: scale(1); 6 | } 7 | 8 | 50% { 9 | transform: scale(1.05); 10 | } 11 | 12 | 100% { 13 | transform: scale(1); 14 | } 15 | } 16 | 17 | @keyframes fadeIn { 18 | from { 19 | opacity: 0; 20 | transform: translateY(20px); 21 | } 22 | 23 | to { 24 | opacity: 1; 25 | transform: translateY(0); 26 | } 27 | } 28 | 29 | @keyframes slideIn { 30 | from { 31 | transform: translateX(100%) scale(0.95); 32 | opacity: 0; 33 | } 34 | 35 | to { 36 | transform: translateX(0) scale(1); 37 | opacity: 1; 38 | } 39 | } 40 | 41 | @keyframes breathe { 42 | 43 | 0%, 44 | 100% { 45 | opacity: 0.7; 46 | } 47 | 48 | 50% { 49 | opacity: 1; 50 | } 51 | } 52 | 53 | @keyframes slideOut { 54 | from { 55 | transform: translateX(0) scale(1); 56 | opacity: 1; 57 | } 58 | 59 | to { 60 | transform: translateX(100%) scale(0.95); 61 | opacity: 0; 62 | } 63 | } 64 | 65 | @keyframes slideInMobile { 66 | from { 67 | transform: translateY(-100%) scale(0.95); 68 | opacity: 0; 69 | } 70 | 71 | 60% { 72 | transform: translateY(2%) scale(1.01); 73 | opacity: 0.95; 74 | } 75 | 76 | to { 77 | transform: translateY(0) scale(1); 78 | opacity: 1; 79 | } 80 | } 81 | 82 | @keyframes slideOutMobile { 83 | from { 84 | transform: translateY(0) scale(1); 85 | opacity: 1; 86 | } 87 | 88 | to { 89 | transform: translateY(-120%) scale(0.95); 90 | opacity: 0; 91 | } 92 | } 93 | 94 | @keyframes shake { 95 | 96 | 0%, 97 | 100% { 98 | transform: translateX(0); 99 | } 100 | 101 | 25% { 102 | transform: translateX(-2px); 103 | } 104 | 105 | 75% { 106 | transform: translateX(2px); 107 | } 108 | } 109 | 110 | @keyframes modalSlideIn { 111 | from { 112 | opacity: 0; 113 | transform: scale(0.9) translateY(-20px); 114 | } 115 | 116 | to { 117 | opacity: 1; 118 | transform: scale(1) translateY(0); 119 | } 120 | } 121 | 122 | @keyframes sessionAdd { 123 | from { 124 | opacity: 0; 125 | transform: translateY(-10px) scale(0.95); 126 | } 127 | 128 | to { 129 | opacity: 1; 130 | transform: translateY(0) scale(1); 131 | } 132 | } 133 | 134 | /* Animation Classes */ 135 | .timer-container.focus .timer-display { 136 | animation: pulse 2s infinite ease-in-out; 137 | } 138 | 139 | .timer-container.running .timer-display { 140 | color: var(--focus-color); 141 | text-shadow: 0 0 10px rgba(231, 76, 60, 0.3); 142 | } 143 | 144 | .timer-container.warning .timer-display { 145 | color: #e67e22; 146 | animation: pulse 1s infinite ease-in-out; 147 | } 148 | 149 | .timer-container.auto-paused .timer-display { 150 | color: #f39c12; 151 | animation: breathe 2s infinite ease-in-out; 152 | } 153 | 154 | .timer-container.auto-paused .timer-status { 155 | color: #f39c12; 156 | font-style: italic; 157 | } 158 | 159 | .task-item { 160 | animation: slideIn 0.3s ease-out; 161 | } 162 | 163 | .pomodoro-dot.completed { 164 | animation: fadeIn 0.5s ease-out; 165 | } 166 | 167 | .notification-ping.dismissing { 168 | animation: slideOut 0.3s ease-in forwards; 169 | } 170 | 171 | .timeline-session.overlap-warning { 172 | border: 2px solid #ff6b6b; 173 | animation: shake 0.5s ease; 174 | } 175 | 176 | .timeline-session.just-added { 177 | animation: sessionAdd 0.4s ease; 178 | } -------------------------------------------------------------------------------- /src/styles/controls.css: -------------------------------------------------------------------------------- 1 | /* Controls and Buttons Styles */ 2 | 3 | .controls { 4 | display: flex; 5 | gap: 0.2rem; 6 | margin-bottom: 2rem; 7 | justify-content: center; 8 | align-items: center; 9 | } 10 | 11 | .control-btn { 12 | border-radius: 1.5rem; 13 | border: none; 14 | width: 70px; 15 | height: 60px; 16 | font-size: 1rem; 17 | font-weight: 600; 18 | font-family: inherit; 19 | cursor: pointer; 20 | transition: all 0.2s ease; 21 | display: flex; 22 | align-items: center; 23 | justify-content: center; 24 | background-color: var(--focus-secondary-btn); 25 | color: var(--focus-timer-color); 26 | } 27 | 28 | .control-btn:active { 29 | transform: scale(0.95); 30 | } 31 | 32 | .control-btn.primary { 33 | background-color: var(--focus-primary-btn); 34 | color: var(--focus-timer-color); 35 | width: 90px; 36 | height: 70px; 37 | } 38 | 39 | .control-btn.primary:hover { 40 | opacity: 0.9; 41 | } 42 | 43 | .control-btn svg { 44 | width: 24px; 45 | height: 24px; 46 | } 47 | 48 | .control-btn.primary svg { 49 | width: 30px; 50 | height: 30px; 51 | } 52 | 53 | /* Timer state specific control styles */ 54 | .controls.focus .control-btn { 55 | background-color: var(--focus-secondary-btn); 56 | color: var(--focus-timer-color); 57 | } 58 | 59 | .controls.focus .control-btn.primary { 60 | background-color: var(--focus-primary-btn); 61 | color: var(--focus-timer-color); 62 | } 63 | 64 | .controls.break .control-btn { 65 | background-color: var(--break-secondary-btn); 66 | color: var(--break-timer-color); 67 | } 68 | 69 | .controls.break .control-btn.primary { 70 | background-color: var(--break-primary-btn); 71 | color: var(--break-timer-color); 72 | } 73 | 74 | .controls.longBreak .control-btn { 75 | background-color: var(--long-break-secondary-btn); 76 | color: var(--long-break-timer-color); 77 | } 78 | 79 | .controls.longBreak .control-btn.primary { 80 | background-color: var(--long-break-primary-btn); 81 | color: var(--long-break-timer-color); 82 | } 83 | 84 | /* Hover effects for each timer state */ 85 | .controls.focus .control-btn.primary:hover:not(:disabled) { 86 | opacity: 0.9; 87 | transform: translateY(-2px); 88 | box-shadow: 0 6px 20px color-mix(in srgb, var(--focus-color) 30%, transparent); 89 | } 90 | 91 | .controls.break .control-btn.primary:hover:not(:disabled) { 92 | opacity: 0.9; 93 | transform: translateY(-2px); 94 | box-shadow: 0 6px 20px color-mix(in srgb, var(--break-color) 30%, transparent); 95 | } 96 | 97 | .controls.longBreak .control-btn.primary:hover:not(:disabled) { 98 | opacity: 0.9; 99 | transform: translateY(-2px); 100 | box-shadow: 0 6px 20px color-mix(in srgb, var(--long-break-color) 30%, transparent); 101 | } -------------------------------------------------------------------------------- /src/styles/layout.css: -------------------------------------------------------------------------------- 1 | /* Default body background - neutral for all pages */ 2 | body { 3 | transition: background-color 0.3s ease; 4 | margin: 0; 5 | padding: 0; 6 | } 7 | 8 | /* Timer view specific - prevent scrolling */ 9 | body.timer-active { 10 | overflow: hidden; 11 | height: 100vh; 12 | } 13 | 14 | html.timer-active { 15 | overflow: hidden; 16 | height: 100vh; 17 | } 18 | 19 | /* Non-timer views - ensure scrolling is allowed */ 20 | body:not(.timer-active) { 21 | overflow: auto; 22 | height: auto; 23 | } 24 | 25 | html:not(.timer-active) { 26 | overflow: auto; 27 | height: auto; 28 | } 29 | 30 | /* Timer background states - applied to body when timer is active */ 31 | /* These styles have higher specificity and will override the default background */ 32 | body.focus { 33 | background: var(--focus-bg) !important; 34 | transition: background-color 0.3s ease; 35 | } 36 | 37 | body.break { 38 | background: var(--break-bg) !important; 39 | transition: background-color 0.3s ease; 40 | } 41 | 42 | body.longBreak { 43 | background: var(--long-break-bg) !important; 44 | transition: background-color 0.3s ease; 45 | } 46 | 47 | /* Views and Layout Styles */ 48 | 49 | /* Main container for centering content */ 50 | .container { 51 | display: flex; 52 | flex-direction: column; 53 | align-items: center; 54 | justify-content: center; 55 | min-height: 100vh; 56 | margin-left: 80px; 57 | /* Account for sidebar width */ 58 | box-sizing: border-box; 59 | } 60 | 61 | .view-container { 62 | display: block; 63 | animation: fadeIn 0.3s ease; 64 | width: 100%; 65 | } 66 | 67 | .view-container.hidden { 68 | display: none; 69 | } 70 | 71 | /* Specific styling for calendar view */ 72 | @keyframes fadeIn { 73 | from { 74 | opacity: 0; 75 | transform: translateY(10px); 76 | } 77 | 78 | to { 79 | opacity: 1; 80 | transform: translateY(0); 81 | } 82 | } 83 | 84 | /* Modal Overlay */ 85 | .modal-overlay { 86 | position: fixed; 87 | top: 0; 88 | left: 0; 89 | right: 0; 90 | bottom: 0; 91 | background: rgba(0, 0, 0, 0.5); 92 | display: none; 93 | align-items: center; 94 | justify-content: center; 95 | z-index: 1000; 96 | backdrop-filter: blur(2px); 97 | } 98 | 99 | .modal-overlay.show { 100 | display: flex; 101 | } 102 | 103 | /* Session Modal */ 104 | .session-modal { 105 | background: white; 106 | border-radius: 12px; 107 | padding: 0; 108 | width: 90%; 109 | max-width: 400px; 110 | max-height: 90vh; 111 | overflow: hidden; 112 | box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15); 113 | animation: modalSlideIn 0.3s ease; 114 | } 115 | 116 | .modal-header { 117 | display: flex; 118 | justify-content: space-between; 119 | align-items: center; 120 | padding: 1.25rem 1.5rem; 121 | border-bottom: 1px solid #e9ecef; 122 | background: #f8f9fa; 123 | } 124 | 125 | .modal-header h3 { 126 | margin: 0; 127 | font-size: 1.1rem; 128 | font-weight: 600; 129 | color: var(--text-color); 130 | } 131 | 132 | .modal-close-btn { 133 | background: none; 134 | border: none; 135 | cursor: pointer; 136 | padding: 0.25rem; 137 | border-radius: 4px; 138 | transition: all 0.2s ease; 139 | color: var(--text-light); 140 | } 141 | 142 | .modal-close-btn:hover { 143 | background: #e9ecef; 144 | color: var(--text-color); 145 | } 146 | 147 | .modal-close-btn svg { 148 | width: 20px; 149 | height: 20px; 150 | } 151 | 152 | /* Session Form */ 153 | .session-form { 154 | padding: 1.5rem; 155 | } 156 | 157 | .form-group { 158 | margin-bottom: 1.25rem; 159 | } 160 | 161 | .form-group label { 162 | display: block; 163 | margin-bottom: 0.5rem; 164 | font-weight: 500; 165 | color: var(--text-color); 166 | font-size: 0.9rem; 167 | } 168 | 169 | .form-group input, 170 | .form-group select { 171 | width: 100%; 172 | padding: 0.75rem; 173 | border: 2px solid #e9ecef; 174 | border-radius: 8px; 175 | font-size: 0.9rem; 176 | transition: all 0.2s ease; 177 | background: white; 178 | box-sizing: border-box; 179 | } 180 | 181 | .form-group input:focus, 182 | .form-group select:focus { 183 | outline: none; 184 | border-color: var(--focus-timer-color); 185 | box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); 186 | } 187 | 188 | .form-group input[type="number"] { 189 | appearance: textfield; 190 | -moz-appearance: textfield; 191 | } 192 | 193 | .form-group input[type="number"]::-webkit-outer-spin-button, 194 | .form-group input[type="number"]::-webkit-inner-spin-button { 195 | -webkit-appearance: none; 196 | margin: 0; 197 | } 198 | 199 | /* Modal Actions */ 200 | .modal-actions { 201 | display: flex; 202 | gap: 0.75rem; 203 | justify-content: flex-end; 204 | margin-top: 2rem; 205 | padding-top: 1.5rem; 206 | border-top: 1px solid #e9ecef; 207 | } 208 | 209 | .btn-primary { 210 | background: var(--shared-border); 211 | color: var(--shared-text); 212 | border: none; 213 | padding: 0.75rem 1.5rem; 214 | border-radius: 8px; 215 | font-weight: 500; 216 | cursor: pointer; 217 | transition: all 0.2s ease; 218 | font-size: 0.9rem; 219 | } 220 | 221 | .btn-primary:hover { 222 | background: var(--hover-bg); 223 | transform: translateY(-1px); 224 | } 225 | 226 | .btn-secondary { 227 | background: #6c757d; 228 | color: var(--shared-text); 229 | border: none; 230 | padding: 0.75rem 1.5rem; 231 | border-radius: 8px; 232 | font-weight: 500; 233 | cursor: pointer; 234 | transition: all 0.2s ease; 235 | font-size: 0.9rem; 236 | } 237 | 238 | .btn-secondary:hover { 239 | background: #5a6268; 240 | } 241 | 242 | .btn-danger { 243 | background: #dc3545; 244 | color: white; 245 | border: none; 246 | padding: 0.75rem 1.5rem; 247 | border-radius: 8px; 248 | font-weight: 500; 249 | cursor: pointer; 250 | transition: all 0.2s ease; 251 | font-size: 0.9rem; 252 | } 253 | 254 | .btn-danger:hover { 255 | background: #c82333; 256 | } 257 | 258 | /* Loading State */ 259 | .sessions-timeline.loading { 260 | opacity: 0.6; 261 | pointer-events: none; 262 | } 263 | 264 | /* Empty State */ 265 | .timeline-empty { 266 | text-align: center; 267 | color: var(--text-light); 268 | font-style: italic; 269 | padding: 1rem; 270 | } 271 | 272 | /* Responsive Modal */ 273 | @media (max-width: 480px) { 274 | 275 | /* Remove container padding and margin-left on mobile */ 276 | .container { 277 | padding: 0; 278 | margin-left: 0; 279 | } 280 | 281 | .session-modal { 282 | width: 95%; 283 | max-width: none; 284 | } 285 | 286 | .modal-header { 287 | padding: 1rem 1.25rem; 288 | } 289 | 290 | .session-form { 291 | padding: 1.25rem; 292 | } 293 | 294 | .modal-actions { 295 | flex-direction: column; 296 | } 297 | 298 | .modal-actions button { 299 | width: 100%; 300 | } 301 | } -------------------------------------------------------------------------------- /src/styles/main.css: -------------------------------------------------------------------------------- 1 | /* Main CSS Entry Point - Imports all modular CSS files */ 2 | 3 | /* Base styles and CSS variables */ 4 | @import 'variables.css'; 5 | 6 | /* Timer themes */ 7 | @import 'themes/espresso.css'; 8 | @import 'themes/pommodore64.css'; 9 | @import 'themes/pipboy.css'; 10 | 11 | /* Shared components for common patterns */ 12 | @import 'shared-components.css'; 13 | 14 | /* Core component styles */ 15 | @import 'timer.css'; 16 | @import 'controls.css'; 17 | @import 'progress.css'; 18 | @import 'tasks.css'; 19 | @import 'statistics.css'; 20 | @import 'smart-indicator.css'; 21 | 22 | /* Layout and navigation */ 23 | @import 'layout.css'; 24 | @import 'sidebar.css'; 25 | 26 | /* Views and pages */ 27 | @import 'calendar.css'; 28 | @import 'settings.css'; 29 | @import 'team.css'; 30 | @import 'timeline.css'; 31 | 32 | /* Interactive elements */ 33 | @import 'modals.css'; 34 | @import 'notifications.css'; 35 | 36 | /* Effects and animations */ 37 | @import 'animations.css'; 38 | 39 | /* Responsive design */ 40 | @import 'responsive.css'; -------------------------------------------------------------------------------- /src/styles/modals.css: -------------------------------------------------------------------------------- 1 | /* History Modal and Keyboard Shortcuts Styles */ 2 | 3 | .history-modal { 4 | position: fixed; 5 | top: 0; 6 | left: 0; 7 | width: 100%; 8 | height: 100%; 9 | background: rgba(0, 0, 0, 0.5); 10 | display: flex; 11 | justify-content: center; 12 | align-items: center; 13 | z-index: 1000; 14 | opacity: 0; 15 | visibility: hidden; 16 | transition: all 0.3s ease; 17 | } 18 | 19 | .history-modal.show { 20 | opacity: 1; 21 | visibility: visible; 22 | } 23 | 24 | .history-content { 25 | background: white; 26 | border-radius: 20px; 27 | padding: 2rem; 28 | max-width: 600px; 29 | max-height: 80vh; 30 | overflow-y: auto; 31 | transform: scale(0.8); 32 | transition: transform 0.3s ease; 33 | } 34 | 35 | .history-modal.show .history-content { 36 | transform: scale(1); 37 | } 38 | 39 | .history-header { 40 | display: flex; 41 | justify-content: space-between; 42 | align-items: center; 43 | margin-bottom: 1.5rem; 44 | } 45 | 46 | .close-btn { 47 | background: none; 48 | border: none; 49 | font-size: 1.5rem; 50 | cursor: pointer; 51 | color: var(--text-light); 52 | padding: 0; 53 | } 54 | 55 | .history-list { 56 | display: flex; 57 | flex-direction: column; 58 | gap: 1rem; 59 | } 60 | 61 | .history-item { 62 | padding: 1rem; 63 | background: #f8f9fa; 64 | border-radius: 10px; 65 | display: flex; 66 | justify-content: space-between; 67 | align-items: center; 68 | } 69 | 70 | .history-date { 71 | font-weight: 600; 72 | color: var(--accent-color); 73 | } 74 | 75 | .history-stats { 76 | display: flex; 77 | gap: 1rem; 78 | font-size: 0.9rem; 79 | color: var(--text-light); 80 | } 81 | 82 | /* Keyboard Shortcuts Info */ 83 | .shortcuts-info { 84 | margin-top: 1rem; 85 | padding: 1rem; 86 | background: rgba(155, 89, 182, 0.1); 87 | border-radius: 10px; 88 | border: 1px solid rgba(155, 89, 182, 0.2); 89 | } 90 | 91 | .shortcuts-info h4 { 92 | margin-bottom: 0.5rem; 93 | color: var(--accent-color); 94 | font-size: 0.9rem; 95 | text-align: center; 96 | } 97 | 98 | .shortcuts-grid { 99 | display: grid; 100 | grid-template-columns: repeat(2, 1fr); 101 | gap: 0.5rem; 102 | font-size: 0.8rem; 103 | } 104 | 105 | .shortcuts-grid span { 106 | color: var(--text-light); 107 | } 108 | -------------------------------------------------------------------------------- /src/styles/notifications.css: -------------------------------------------------------------------------------- 1 | /* Notification System Styles */ 2 | 3 | .notification-container { 4 | position: fixed; 5 | top: 20px; 6 | right: 20px; 7 | z-index: 1000; 8 | pointer-events: none; 9 | } 10 | 11 | .notification-ping { 12 | background: var(--focus-bg); 13 | color: var(--focus-timer-color); 14 | padding: 1rem 1.25rem; 15 | margin-bottom: 0.75rem; 16 | border-radius: 12px; 17 | box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); 18 | border: none; 19 | max-width: 280px; 20 | pointer-events: auto; 21 | font-weight: 500; 22 | font-size: 0.9rem; 23 | text-align: left; 24 | /* Default state - visible and positioned */ 25 | opacity: 1; 26 | transform: translateX(0); 27 | transition: all 0.3s ease-out; 28 | } 29 | 30 | /* Initial hidden state for new notifications */ 31 | .notification-ping.entering { 32 | opacity: 0; 33 | transform: translateX(100%); 34 | } 35 | 36 | /* Trigger animation by removing entering class */ 37 | .notification-ping:not(.entering) { 38 | opacity: 1; 39 | transform: translateX(0); 40 | } 41 | 42 | /* Exit animation */ 43 | .notification-ping.dismissing { 44 | opacity: 0; 45 | transform: translateX(100%); 46 | transition: all 0.3s ease-in; 47 | } 48 | 49 | /* Timer state notifications */ 50 | .notification-ping.focus { 51 | background: var(--focus-bg); 52 | color: var(--focus-timer-color); 53 | } 54 | 55 | .notification-ping.break { 56 | background: var(--break-bg); 57 | color: var(--break-timer-color); 58 | } 59 | 60 | .notification-ping.longBreak { 61 | background: var(--long-break-bg); 62 | color: var(--long-break-timer-color); 63 | } 64 | 65 | /* Notification types */ 66 | .notification-ping.success { 67 | background: var(--break-bg); 68 | color: var(--break-timer-color); 69 | } 70 | 71 | .notification-ping.warning { 72 | background: #FFF9E6; 73 | color: #8B5A00; 74 | } 75 | 76 | .notification-ping.error { 77 | background: var(--focus-bg); 78 | color: var(--focus-timer-color); 79 | } 80 | 81 | .notification-ping.info { 82 | background: var(--long-break-bg); 83 | color: var(--long-break-timer-color); 84 | } 85 | 86 | /* Refresh animation for duplicate notifications */ 87 | .notification-ping.refreshing { 88 | animation: refreshPulse 0.3s ease-out; 89 | } 90 | 91 | @keyframes refreshPulse { 92 | 0% { 93 | transform: scale(1); 94 | opacity: 1; 95 | } 96 | 97 | 50% { 98 | transform: scale(1.02); 99 | opacity: 0.95; 100 | } 101 | 102 | 100% { 103 | transform: scale(1); 104 | opacity: 1; 105 | } 106 | } 107 | 108 | /* Stacked notifications spacing */ 109 | .notification-ping+.notification-ping { 110 | margin-top: -4px; 111 | animation-delay: 0.1s; 112 | } 113 | 114 | .notification-ping:nth-child(2) { 115 | opacity: 0.95; 116 | transform: scale(0.98); 117 | } 118 | 119 | .notification-ping:nth-child(3) { 120 | opacity: 0.9; 121 | transform: scale(0.96); 122 | } 123 | 124 | .notification-ping:nth-child(n+4) { 125 | opacity: 0.8; 126 | transform: scale(0.94); 127 | } 128 | 129 | /* Mobile responsive for notifications */ 130 | @media (max-width: 768px) { 131 | .notification-container { 132 | top: 20px; 133 | right: 16px; 134 | left: 16px; 135 | z-index: 1001; 136 | } 137 | 138 | .notification-ping { 139 | max-width: none; 140 | margin: 0 0 0.75rem 0; 141 | padding: 1.25rem 1.5rem; 142 | border-radius: 16px; 143 | font-size: 1rem; 144 | line-height: 1.4; 145 | text-align: left; 146 | box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); 147 | border: none; 148 | cursor: pointer; 149 | transform: translateZ(0); 150 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0.05); 151 | /* Mobile-specific transition for smoother performance */ 152 | transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); 153 | } 154 | 155 | .notification-ping.entering { 156 | opacity: 0; 157 | transform: translateY(-100%) scale(0.95); 158 | } 159 | 160 | .notification-ping:not(.entering) { 161 | opacity: 1; 162 | transform: translateY(0) scale(1); 163 | } 164 | 165 | .notification-ping.dismissing { 166 | opacity: 0; 167 | transform: translateY(-120%) scale(0.95); 168 | transition: all 0.3s cubic-bezier(0.55, 0.055, 0.675, 0.19); 169 | } 170 | 171 | .notification-ping:active { 172 | transform: scale(0.98); 173 | transition: transform 0.1s ease; 174 | } 175 | } 176 | 177 | /* Small screens */ 178 | @media (max-width: 480px) { 179 | .notification-container { 180 | top: 16px; 181 | right: 12px; 182 | left: 12px; 183 | } 184 | 185 | .notification-ping { 186 | padding: 1.125rem 1.25rem; 187 | font-size: 0.95rem; 188 | border-radius: 14px; 189 | box-shadow: 0 3px 14px rgba(0, 0, 0, 0.09); 190 | } 191 | } 192 | 193 | /* Notification status indicator styles */ 194 | .notification-status { 195 | border-left: 3px solid #ccc; 196 | background-color: var(--bg-secondary); 197 | color: var(--text-secondary); 198 | transition: all 0.3s ease; 199 | } 200 | 201 | .notification-status.status-ready { 202 | border-left-color: #4CAF50; 203 | background-color: rgba(76, 175, 80, 0.1); 204 | color: #2E7D32; 205 | } 206 | 207 | .notification-status.status-warning { 208 | border-left-color: #FF9800; 209 | background-color: rgba(255, 152, 0, 0.1); 210 | color: #E65100; 211 | } 212 | 213 | .notification-status.status-error { 214 | border-left-color: #F44336; 215 | background-color: rgba(244, 67, 54, 0.1); 216 | color: #C62828; 217 | } 218 | 219 | .notification-status.status-disabled { 220 | border-left-color: #9E9E9E; 221 | background-color: rgba(158, 158, 158, 0.1); 222 | color: #616161; 223 | } 224 | 225 | #test-notifications-btn { 226 | background: var(--button-primary); 227 | color: var(--button-text); 228 | border: 1px solid var(--border); 229 | transition: background-color 0.2s ease, transform 0.1s ease; 230 | } 231 | 232 | #test-notifications-btn:hover { 233 | background: var(--button-primary-hover); 234 | transform: translateY(-1px); 235 | } 236 | 237 | #test-notifications-btn:active { 238 | transform: translateY(0); 239 | } 240 | 241 | /* Dark theme adjustments for notification status */ 242 | [data-theme="dark"] .notification-status.status-ready { 243 | background-color: rgba(76, 175, 80, 0.15); 244 | color: #81C784; 245 | } 246 | 247 | [data-theme="dark"] .notification-status.status-warning { 248 | background-color: rgba(255, 152, 0, 0.15); 249 | color: #FFB74D; 250 | } 251 | 252 | [data-theme="dark"] .notification-status.status-error { 253 | background-color: rgba(244, 67, 54, 0.15); 254 | color: #E57373; 255 | } 256 | 257 | [data-theme="dark"] .notification-status.status-disabled { 258 | background-color: rgba(158, 158, 158, 0.15); 259 | color: #BDBDBD; 260 | } -------------------------------------------------------------------------------- /src/styles/progress.css: -------------------------------------------------------------------------------- 1 | /* Progress Section Styles */ 2 | 3 | .progress-container { 4 | width: 100%; 5 | margin-bottom: 2rem; 6 | } 7 | 8 | .pomodoro-counter { 9 | background: white; 10 | padding: 1.5rem; 11 | border-radius: 15px; 12 | box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); 13 | text-align: center; 14 | } 15 | 16 | .pomodoro-counter h3 { 17 | margin-bottom: 1rem; 18 | color: var(--accent-color); 19 | } 20 | 21 | .pomodoro-dots { 22 | display: flex; 23 | justify-content: center; 24 | gap: 0.5rem; 25 | margin-bottom: 1rem; 26 | flex-wrap: wrap; 27 | } 28 | 29 | .pomodoro-dot { 30 | width: 20px; 31 | height: 20px; 32 | border-radius: 50%; 33 | background-color: #ecf0f1; 34 | border: 2px solid #bdc3c7; 35 | transition: all 0.3s ease; 36 | } 37 | 38 | .pomodoro-dot.completed { 39 | background-color: var(--focus-color); 40 | border-color: var(--focus-color); 41 | } 42 | 43 | .pomodoro-dot.current { 44 | background-color: var(--accent-color); 45 | border-color: var(--accent-color); 46 | transform: scale(1.2); 47 | } 48 | 49 | .stats { 50 | display: flex; 51 | justify-content: space-around; 52 | gap: 1rem; 53 | flex-wrap: wrap; 54 | } 55 | 56 | .stats span { 57 | color: var(--text-light); 58 | } 59 | 60 | .stats strong { 61 | color: var(--accent-color); 62 | } 63 | 64 | /* Progress Dots */ 65 | .progress-dots { 66 | display: flex; 67 | justify-content: center; 68 | gap: 8px; 69 | margin-bottom: 2rem; 70 | } 71 | 72 | .dot { 73 | width: 10px; 74 | height: 10px; 75 | border-radius: 50%; 76 | background-color: var(--focus-secondary-btn); 77 | transition: all 0.3s ease; 78 | border: 1px solid rgba(255, 255, 255, 0.4); 79 | } 80 | 81 | /* Dot variations for different modes */ 82 | .container.break .dot { 83 | background-color: var(--break-secondary-btn); 84 | } 85 | 86 | .container.longBreak .dot { 87 | background-color: var(--long-break-secondary-btn); 88 | } 89 | 90 | .dot.completed { 91 | background-color: var(--focus-timer-color); 92 | border: 1px solid var(--focus-timer-color); 93 | } 94 | 95 | .container.focus .dot.completed { 96 | background-color: var(--focus-timer-color); 97 | } 98 | 99 | .container.break .dot.completed { 100 | background-color: var(--break-timer-color); 101 | } 102 | 103 | .container.longBreak .dot.completed { 104 | background-color: var(--long-break-timer-color); 105 | } 106 | 107 | .dot.current { 108 | background-color: rgba(231, 76, 60, 0.4); 109 | transform: scale(1.2); 110 | box-shadow: 0 0 8px rgba(231, 76, 60, 0.3); 111 | } 112 | 113 | /* Current dot variations for different modes */ 114 | .container.focus .dot.current { 115 | background-color: var(--focus-primary-btn); 116 | } 117 | 118 | .container.break .dot.current { 119 | background-color: rgba(46, 204, 113, 0.4); 120 | border-color: var(--break-timer-color); 121 | box-shadow: 0 0 8px rgba(46, 204, 113, 0.3); 122 | } 123 | 124 | .container.longBreak .dot.current { 125 | background-color: rgba(52, 152, 219, 0.4); 126 | border-color: var(--long-break-timer-color); 127 | box-shadow: 0 0 8px rgba(52, 152, 219, 0.3); 128 | } 129 | 130 | /* Overflow Indicator */ 131 | .overflow-indicator { 132 | font-size: 0.8rem; 133 | font-weight: 600; 134 | color: var(--focus-timer-color); 135 | background-color: var(--focus-secondary-btn); 136 | border-radius: 12px; 137 | padding: 2px 6px; 138 | display: flex; 139 | align-items: center; 140 | justify-content: center; 141 | transition: all 0.3s ease; 142 | min-width: 24px; 143 | height: 16px; 144 | margin-top: -4px; 145 | } 146 | 147 | /* Overflow indicator variations for different modes */ 148 | .container.focus .overflow-indicator { 149 | color: var(--focus-timer-color); 150 | background-color: var(--focus-secondary-btn); 151 | border-color: var(--focus-timer-color); 152 | } 153 | 154 | .container.break .overflow-indicator { 155 | color: var(--break-timer-color); 156 | background-color: var(--break-secondary-btn); 157 | border-color: var(--break-timer-color); 158 | } 159 | 160 | .container.longBreak .overflow-indicator { 161 | color: var(--long-break-timer-color); 162 | background-color: var(--long-break-secondary-btn); 163 | border-color: var(--long-break-timer-color); 164 | } -------------------------------------------------------------------------------- /src/styles/responsive.css: -------------------------------------------------------------------------------- 1 | /* Responsive Design and Mobile Styles */ 2 | 3 | /* Tablet and small desktop responsive */ 4 | @media (max-width: 1024px) { 5 | 6 | /* Calendar layout adjustments for tablets and small desktops */ 7 | .calendar-main-layout { 8 | grid-template-columns: 1fr; 9 | gap: 1.5rem; 10 | max-width: 800px; 11 | } 12 | 13 | .focus-summary-grid { 14 | grid-template-columns: repeat(2, 1fr); 15 | gap: 1rem; 16 | } 17 | 18 | .weekly-chart { 19 | height: 100px; 20 | } 21 | 22 | .daily-chart { 23 | height: 70px; 24 | } 25 | 26 | #calendar-view { 27 | padding: 1.5rem !important; 28 | } 29 | 30 | .calendar-container, 31 | .mini-calendar-container, 32 | .focus-summary-card, 33 | .weekly-chart-card, 34 | .daily-chart-card { 35 | padding: 1.25rem; 36 | } 37 | } 38 | 39 | /* Mobile Sidebar Styles */ 40 | @media (max-width: 768px) { 41 | 42 | /* Adjust container for mobile layout */ 43 | .container { 44 | margin-left: 0; 45 | padding: 1rem; 46 | padding-bottom: 90px; 47 | /* Account for bottom sidebar height + margin */ 48 | } 49 | 50 | .sidebar { 51 | position: fixed; 52 | left: 0; 53 | right: 0; 54 | bottom: 0; 55 | top: auto; 56 | width: 100vw; 57 | height: 70px; 58 | flex-direction: row; 59 | justify-content: center; 60 | align-items: center; 61 | padding: 0.75rem 1rem; 62 | border: none; 63 | box-shadow: 0 -2px 20px rgba(231, 76, 60, 0.1); 64 | margin: 0; 65 | } 66 | 67 | /* Mobile theme states */ 68 | .sidebar.focus { 69 | box-shadow: 0 -2px 20px rgba(231, 76, 60, 0.15); 70 | } 71 | 72 | .sidebar.break { 73 | box-shadow: 0 -2px 20px rgba(46, 204, 113, 0.15); 74 | } 75 | 76 | .sidebar.longBreak { 77 | box-shadow: 0 -2px 20px rgba(52, 152, 219, 0.15); 78 | } 79 | 80 | .sidebar-icons { 81 | flex-direction: row; 82 | gap: 1rem; 83 | margin-bottom: 0; 84 | margin-right: 1rem; 85 | } 86 | 87 | .sidebar-bottom { 88 | margin-top: 0; 89 | margin-bottom: 0; 90 | display: flex; 91 | flex-direction: row; 92 | /* Orizzontale in mobile */ 93 | align-items: center; 94 | gap: 1rem; 95 | margin-left: auto; 96 | /* Spinge tutto a destra */ 97 | } 98 | 99 | /* User avatar adjustments for mobile */ 100 | .user-avatar-container { 101 | margin-bottom: 0; 102 | margin-left: 0; 103 | /* Reset, già gestito da sidebar-bottom */ 104 | } 105 | 106 | .user-avatar-btn { 107 | width: 44px; 108 | height: 44px; 109 | } 110 | 111 | .sidebar-icon, 112 | .sidebar-icon-large { 113 | width: 44px; 114 | height: 44px; 115 | border-radius: 10px; 116 | } 117 | 118 | .sidebar-icon svg, 119 | .sidebar-icon-large svg { 120 | width: 22px; 121 | height: 22px; 122 | } 123 | 124 | /* Use scale instead of vertical transform on mobile */ 125 | .sidebar.focus .sidebar-icon:hover, 126 | .sidebar.break .sidebar-icon:hover, 127 | .sidebar.longBreak .sidebar-icon:hover { 128 | transform: scale(1.05); 129 | } 130 | 131 | .sidebar.focus .sidebar-icon-large:hover, 132 | .sidebar.break .sidebar-icon-large:hover, 133 | .sidebar.longBreak .sidebar-icon-large:hover { 134 | transform: scale(1.05); 135 | } 136 | 137 | /* Main Content Layout Adjustment for mobile */ 138 | body { 139 | padding-left: 0; 140 | padding-bottom: 70px; 141 | /* Space for bottom sidebar */ 142 | } 143 | 144 | /* Team responsive design */ 145 | .team-stats-container { 146 | grid-template-columns: repeat(2, 1fr); 147 | gap: 0.8rem; 148 | } 149 | 150 | .team-stat-card { 151 | padding: 1rem; 152 | gap: 0.8rem; 153 | } 154 | 155 | .team-stat-card .stat-icon { 156 | font-size: 1.5rem; 157 | width: 50px; 158 | height: 50px; 159 | } 160 | 161 | .team-stat-card .stat-number { 162 | font-size: 1.5rem; 163 | } 164 | 165 | /* General padding for main views on mobile */ 166 | #calendar-view, 167 | #settings-view, 168 | #team-view { 169 | padding: 1.5rem !important; 170 | } 171 | 172 | .team-members-grid { 173 | grid-template-columns: 1fr; 174 | gap: 1rem; 175 | } 176 | 177 | .team-member-card { 178 | padding: 1.2rem; 179 | } 180 | 181 | /* Calendar responsive */ 182 | .calendar-main-layout { 183 | grid-template-columns: 1fr; 184 | gap: 1rem; 185 | } 186 | 187 | .calendar-container { 188 | padding: 1.5rem; 189 | } 190 | 191 | .day-stats { 192 | grid-template-columns: 1fr; 193 | gap: 1rem; 194 | } 195 | 196 | .weekly-chart { 197 | height: 120px; 198 | } 199 | 200 | .daily-chart { 201 | height: 60px; 202 | } 203 | } 204 | 205 | /* Extra small screens */ 206 | @media (max-width: 480px) { 207 | 208 | /* Remove all padding and margin from container on small screens */ 209 | .container { 210 | padding: 0; 211 | margin-left: 0; 212 | padding-bottom: 75px; 213 | /* Account for smaller bottom sidebar */ 214 | } 215 | 216 | .sidebar { 217 | height: 65px; 218 | padding: 0.5rem 0.75rem; 219 | width: 100vw; 220 | left: 0; 221 | right: 0; 222 | border: none; 223 | margin: 0; 224 | } 225 | 226 | .sidebar-icons { 227 | gap: 0.75rem; 228 | margin-right: 0.75rem; 229 | } 230 | 231 | .sidebar-bottom { 232 | gap: 0.75rem; 233 | /* Ridotto per schermi piccoli */ 234 | } 235 | 236 | .sidebar-icon, 237 | .sidebar-icon-large { 238 | width: 40px; 239 | height: 40px; 240 | border-radius: 8px; 241 | } 242 | 243 | .sidebar-icon svg, 244 | .sidebar-icon-large svg { 245 | width: 20px; 246 | height: 20px; 247 | } 248 | 249 | /* User avatar adjustments for extra small screens */ 250 | .user-avatar-btn { 251 | width: 32px; 252 | height: 32px; 253 | } 254 | 255 | body { 256 | padding-bottom: 65px; 257 | } 258 | 259 | /* Team responsive for very small screens */ 260 | .team-stats-container { 261 | grid-template-columns: 1fr; 262 | } 263 | 264 | #team-view { 265 | padding: 1rem; 266 | } 267 | 268 | /* General padding for main views on extra small screens */ 269 | #calendar-view, 270 | #settings-view { 271 | padding: 1rem !important; 272 | } 273 | 274 | /* Timer adjustments for mobile */ 275 | .timer-minutes, 276 | .timer-seconds { 277 | font-size: 8rem; 278 | } 279 | 280 | .timer-container { 281 | padding: 2rem 1rem; 282 | } 283 | 284 | /* Controls adjustments */ 285 | .controls { 286 | gap: 0.5rem; 287 | } 288 | 289 | .control-btn { 290 | width: 60px; 291 | height: 50px; 292 | } 293 | 294 | .control-btn.primary { 295 | width: 80px; 296 | height: 60px; 297 | } 298 | 299 | /* Calendar mobile adjustments */ 300 | .calendar-container { 301 | padding: 1rem; 302 | margin-bottom: 1rem; 303 | } 304 | 305 | .calendar-grid { 306 | gap: 4px; 307 | } 308 | 309 | .calendar-day { 310 | font-size: 0.8rem; 311 | } 312 | 313 | .calendar-day-number { 314 | font-size: 0.8rem; 315 | } 316 | 317 | /* Settings mobile adjustments */ 318 | .settings-section { 319 | padding: 1rem; 320 | margin-bottom: 1rem; 321 | } 322 | 323 | .shortcut-item { 324 | flex-direction: column; 325 | align-items: stretch; 326 | gap: 0.5rem; 327 | } 328 | 329 | .shortcut-input-container { 330 | max-width: none; 331 | } 332 | 333 | .setting-item { 334 | flex-direction: column; 335 | align-items: stretch; 336 | gap: 0.5rem; 337 | } 338 | 339 | /* Task section mobile */ 340 | .task-section { 341 | padding: 1rem; 342 | } 343 | 344 | /* Progress section mobile */ 345 | .progress-container { 346 | margin-bottom: 1rem; 347 | } 348 | 349 | .pomodoro-counter { 350 | padding: 1rem; 351 | } 352 | 353 | /* Statistics mobile */ 354 | .stats-section { 355 | padding: 1rem; 356 | } 357 | 358 | .weekly-stats { 359 | grid-template-columns: repeat(4, 1fr); 360 | gap: 0.25rem; 361 | } 362 | 363 | .day-stat { 364 | padding: 0.25rem; 365 | } 366 | 367 | .day-label { 368 | font-size: 0.7rem; 369 | } 370 | 371 | .day-count { 372 | font-size: 1rem; 373 | } 374 | 375 | .day-time { 376 | font-size: 0.6rem; 377 | } 378 | } 379 | 380 | /* Landscape orientation adjustments for mobile */ 381 | @media (max-height: 600px) and (orientation: landscape) { 382 | 383 | .timer-minutes, 384 | .timer-seconds { 385 | font-size: 6rem; 386 | } 387 | 388 | .timer-container { 389 | padding: 1rem; 390 | } 391 | 392 | h1 { 393 | font-size: 1.4rem; 394 | margin-bottom: 1rem; 395 | } 396 | 397 | .progress-container { 398 | margin-bottom: 1rem; 399 | } 400 | } 401 | 402 | /* High DPI screens */ 403 | @media (-webkit-min-device-pixel-ratio: 2), 404 | (min-resolution: 192dpi) { 405 | .sidebar { 406 | backdrop-filter: blur(40px); 407 | -webkit-backdrop-filter: blur(40px); 408 | } 409 | 410 | .notification-ping { 411 | box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); 412 | } 413 | } 414 | 415 | /* Print styles */ 416 | @media print { 417 | 418 | .sidebar, 419 | .controls, 420 | .notification-container { 421 | display: none !important; 422 | } 423 | 424 | body { 425 | padding-left: 0 !important; 426 | padding-bottom: 0 !important; 427 | } 428 | 429 | .container { 430 | background: white !important; 431 | } 432 | 433 | .calendar-container, 434 | .daily-details, 435 | .weekly-summary { 436 | break-inside: avoid; 437 | margin-bottom: 1rem; 438 | } 439 | } -------------------------------------------------------------------------------- /src/styles/sidebar.css: -------------------------------------------------------------------------------- 1 | /* Sidebar Navigation Styles */ 2 | 3 | .sidebar { 4 | position: fixed; 5 | left: 0; 6 | top: 0; 7 | width: 80px; 8 | height: 100vh; 9 | box-sizing: border-box; 10 | background: var(--focus-bg); 11 | backdrop-filter: blur(20px); 12 | -webkit-backdrop-filter: blur(20px); 13 | display: flex; 14 | flex-direction: column; 15 | align-items: center; 16 | padding: 1.5rem 0; 17 | z-index: 1000; 18 | box-shadow: 2px 0 20px color-mix(in srgb, var(--focus-color) 10%, transparent); 19 | transition: all 0.3s ease; 20 | } 21 | 22 | /* Sidebar theme states */ 23 | .sidebar.focus { 24 | background: var(--focus-bg); 25 | box-shadow: 2px 0 20px color-mix(in srgb, var(--focus-color) 15%, transparent); 26 | } 27 | 28 | .sidebar.break { 29 | background: var(--break-bg); 30 | box-shadow: 2px 0 20px color-mix(in srgb, var(--break-color) 15%, transparent); 31 | } 32 | 33 | .sidebar.longBreak { 34 | background: var(--long-break-bg); 35 | box-shadow: 2px 0 20px color-mix(in srgb, var(--long-break-color) 15%, transparent); 36 | } 37 | 38 | .sidebar-icons { 39 | display: flex; 40 | flex-direction: column; 41 | gap: 1.5rem; 42 | margin-bottom: 2rem; 43 | } 44 | 45 | .sidebar-bottom { 46 | margin-top: auto; 47 | margin-bottom: 1rem; 48 | display: flex; 49 | flex-direction: column; 50 | /* Verticale su desktop */ 51 | align-items: center; 52 | gap: 1rem; 53 | } 54 | 55 | .sidebar-icon { 56 | width: 48px; 57 | height: 48px; 58 | border: none; 59 | border-radius: 12px; 60 | background: transparent; 61 | color: var(--focus-timer-color); 62 | cursor: pointer; 63 | display: flex; 64 | align-items: center; 65 | justify-content: center; 66 | transition: all 0.3s ease; 67 | position: relative; 68 | } 69 | 70 | .sidebar-icon-large { 71 | width: 52px; 72 | height: 52px; 73 | border: none; 74 | border-radius: 14px; 75 | background: transparent; 76 | color: var(--focus-timer-color); 77 | cursor: pointer; 78 | display: flex; 79 | align-items: center; 80 | justify-content: center; 81 | transition: all 0.3s ease; 82 | position: relative; 83 | } 84 | 85 | .sidebar-icon svg { 86 | width: 24px; 87 | height: 24px; 88 | } 89 | 90 | .sidebar-icon-large svg { 91 | width: 26px; 92 | height: 26px; 93 | } 94 | 95 | /* Sidebar icon theme states */ 96 | .sidebar.focus .sidebar-icon { 97 | color: var(--focus-timer-color); 98 | } 99 | 100 | .sidebar.focus .sidebar-icon:hover { 101 | background: var(--focus-secondary-btn); 102 | color: var(--focus-timer-color); 103 | transform: translateY(-2px); 104 | box-shadow: 0 4px 12px color-mix(in srgb, var(--focus-timer-color) 20%, transparent); 105 | } 106 | 107 | .sidebar.focus .sidebar-icon.active { 108 | background: var(--focus-primary-btn); 109 | color: #000; 110 | box-shadow: 0 4px 16px color-mix(in srgb, var(--focus-timer-color) 30%, transparent); 111 | } 112 | 113 | .sidebar.break .sidebar-icon { 114 | color: var(--break-timer-color); 115 | } 116 | 117 | .sidebar.break .sidebar-icon:hover { 118 | background: var(--break-secondary-btn); 119 | color: var(--break-timer-color); 120 | transform: translateY(-2px); 121 | box-shadow: 0 4px 12px color-mix(in srgb, var(--break-timer-color) 20%, transparent); 122 | } 123 | 124 | .sidebar.break .sidebar-icon.active { 125 | background: var(--break-primary-btn); 126 | color: #000; 127 | box-shadow: 0 4px 16px color-mix(in srgb, var(--break-timer-color) 30%, transparent); 128 | } 129 | 130 | .sidebar.longBreak .sidebar-icon { 131 | color: var(--long-break-timer-color); 132 | } 133 | 134 | .sidebar.longBreak .sidebar-icon:hover { 135 | background: var(--long-break-secondary-btn); 136 | color: var(--long-break-timer-color); 137 | transform: translateY(-2px); 138 | box-shadow: 0 4px 12px color-mix(in srgb, var(--long-break-timer-color) 20%, transparent); 139 | } 140 | 141 | .sidebar.longBreak .sidebar-icon.active { 142 | background: var(--long-break-primary-btn); 143 | color: #000; 144 | box-shadow: 0 4px 16px color-mix(in srgb, var(--long-break-timer-color) 30%, transparent); 145 | } 146 | 147 | /* Large settings icon theme states */ 148 | .sidebar.focus .sidebar-icon-large { 149 | color: var(--focus-timer-color); 150 | } 151 | 152 | .sidebar.focus .sidebar-icon-large:hover { 153 | background: var(--focus-secondary-btn); 154 | color: var(--focus-timer-color); 155 | transform: translateY(-2px); 156 | box-shadow: 0 6px 16px color-mix(in srgb, var(--focus-timer-color) 20%, transparent); 157 | } 158 | 159 | .sidebar.focus .sidebar-icon-large.active { 160 | background: var(--focus-primary-btn); 161 | color: var(--focus-timer-color); 162 | box-shadow: 0 6px 20px color-mix(in srgb, var(--focus-timer-color) 30%, transparent); 163 | } 164 | 165 | .sidebar.break .sidebar-icon-large { 166 | color: var(--break-timer-color); 167 | } 168 | 169 | .sidebar.break .sidebar-icon-large:hover { 170 | background: var(--break-secondary-btn); 171 | color: var(--break-timer-color); 172 | transform: translateY(-2px); 173 | box-shadow: 0 6px 16px color-mix(in srgb, var(--break-timer-color) 20%, transparent); 174 | } 175 | 176 | .sidebar.break .sidebar-icon-large.active { 177 | background: var(--break-primary-btn); 178 | color: var(--break-timer-color); 179 | box-shadow: 0 6px 20px color-mix(in srgb, var(--break-timer-color) 30%, transparent); 180 | } 181 | 182 | .sidebar.longBreak .sidebar-icon-large { 183 | color: var(--long-break-timer-color); 184 | } 185 | 186 | .sidebar.longBreak .sidebar-icon-large:hover { 187 | background: var(--long-break-secondary-btn); 188 | color: var(--long-break-timer-color); 189 | transform: translateY(-2px); 190 | box-shadow: 0 6px 16px color-mix(in srgb, var(--long-break-timer-color) 20%, transparent); 191 | } 192 | 193 | .sidebar.longBreak .sidebar-icon-large.active { 194 | background: var(--long-break-primary-btn); 195 | color: var(--long-break-timer-color); 196 | box-shadow: 0 6px 20px color-mix(in srgb, var(--long-break-timer-color) 30%, transparent); 197 | } 198 | 199 | /* User Avatar Styles */ 200 | .user-avatar-container { 201 | position: relative; 202 | display: flex; 203 | justify-content: center; 204 | } 205 | 206 | .user-avatar-btn { 207 | width: 42px; 208 | height: 42px; 209 | border-radius: 50%; 210 | overflow: hidden; 211 | border: 2px solid transparent; 212 | background: var(--focus-secondary-btn); 213 | display: flex; 214 | align-items: center; 215 | justify-content: center; 216 | transition: all 0.3s ease; 217 | cursor: pointer; 218 | padding: 0; 219 | } 220 | 221 | .user-avatar-btn:hover { 222 | transform: scale(1.05); 223 | border-color: var(--focus-timer-color); 224 | box-shadow: 0 4px 12px color-mix(in srgb, var(--focus-timer-color) 20%, transparent); 225 | } 226 | 227 | .user-avatar-btn img { 228 | width: 100%; 229 | height: 100%; 230 | object-fit: cover; 231 | } 232 | 233 | .avatar-fallback { 234 | width: 100%; 235 | height: 100%; 236 | display: flex; 237 | align-items: center; 238 | justify-content: center; 239 | color: var(--focus-timer-color); 240 | background: var(--focus-secondary-btn); 241 | } 242 | 243 | .avatar-fallback i { 244 | font-size: 24px; 245 | } 246 | 247 | .avatar-fallback span { 248 | font-size: 18px; 249 | font-weight: 600; 250 | text-transform: uppercase; 251 | } 252 | 253 | /* User Dropdown Menu */ 254 | .user-dropdown { 255 | position: absolute; 256 | bottom: 0; 257 | left: 70px; 258 | /* Desktop: appare a destra */ 259 | width: 200px; 260 | background: var(--focus-bg); 261 | border: 1px solid color-mix(in srgb, var(--focus-timer-color) 20%, transparent); 262 | border-radius: 12px; 263 | box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); 264 | backdrop-filter: blur(20px); 265 | z-index: 1000; 266 | animation: dropdownFadeInRight 0.2s ease-out; 267 | } 268 | 269 | @keyframes dropdownFadeInRight { 270 | from { 271 | opacity: 0; 272 | transform: translateX(-10px) scale(0.95); 273 | } 274 | 275 | to { 276 | opacity: 1; 277 | transform: translateX(0) scale(1); 278 | } 279 | } 280 | 281 | /* Mobile responsiveness */ 282 | @media (max-width: 768px) { 283 | .user-dropdown { 284 | bottom: 60px; 285 | /* Mobile: sopra l'avatar */ 286 | left: 50%; 287 | transform: translateX(-50%); 288 | animation: dropdownFadeInUp 0.2s ease-out; 289 | } 290 | 291 | @keyframes dropdownFadeInUp { 292 | from { 293 | opacity: 0; 294 | transform: translateX(-50%) translateY(10px) scale(0.95); 295 | } 296 | 297 | to { 298 | opacity: 1; 299 | transform: translateX(-50%) translateY(0) scale(1); 300 | } 301 | } 302 | } 303 | 304 | .user-dropdown-header { 305 | padding: 16px; 306 | border-bottom: 1px solid color-mix(in srgb, var(--focus-timer-color) 10%, transparent); 307 | } 308 | 309 | .user-name { 310 | display: block; 311 | font-size: 14px; 312 | font-weight: 600; 313 | color: var(--focus-timer-color); 314 | margin-bottom: 4px; 315 | } 316 | 317 | .user-status { 318 | display: block; 319 | font-size: 12px; 320 | color: var(--focus-timer-color); 321 | opacity: 0.7; 322 | } 323 | 324 | .user-dropdown-actions { 325 | padding: 8px; 326 | } 327 | 328 | .user-dropdown-action { 329 | width: 100%; 330 | display: flex; 331 | align-items: center; 332 | gap: 12px; 333 | padding: 12px 16px; 334 | border: none; 335 | border-radius: 8px; 336 | background: transparent; 337 | color: var(--focus-timer-color); 338 | font-size: 14px; 339 | cursor: pointer; 340 | transition: all 0.2s ease; 341 | } 342 | 343 | .user-dropdown-action:hover { 344 | background: var(--focus-secondary-btn); 345 | transform: translateX(2px); 346 | } 347 | 348 | .user-dropdown-action svg { 349 | width: 16px; 350 | height: 16px; 351 | opacity: 0.8; 352 | } 353 | 354 | /* Theme states for user avatar and dropdown */ 355 | .sidebar.break .user-avatar-btn { 356 | background: var(--break-secondary-btn); 357 | border-color: var(--break-timer-color); 358 | } 359 | 360 | .sidebar.break .avatar-fallback { 361 | background: var(--break-secondary-btn); 362 | color: var(--break-timer-color); 363 | } 364 | 365 | .sidebar.break .user-dropdown { 366 | background: var(--break-bg); 367 | border-color: color-mix(in srgb, var(--break-timer-color) 20%, transparent); 368 | } 369 | 370 | .sidebar.break .user-name, 371 | .sidebar.break .user-status, 372 | .sidebar.break .user-dropdown-action { 373 | color: var(--break-timer-color); 374 | } 375 | 376 | .sidebar.break .user-dropdown-action:hover { 377 | background: var(--break-secondary-btn); 378 | } 379 | 380 | .sidebar.longBreak .user-avatar-btn { 381 | background: var(--long-break-secondary-btn); 382 | border-color: var(--long-break-timer-color); 383 | } 384 | 385 | .sidebar.longBreak .avatar-fallback { 386 | background: var(--long-break-secondary-btn); 387 | color: var(--long-break-timer-color); 388 | } 389 | 390 | .sidebar.longBreak .user-dropdown { 391 | background: var(--long-break-bg); 392 | border-color: color-mix(in srgb, var(--long-break-timer-color) 20%, transparent); 393 | } 394 | 395 | .sidebar.longBreak .user-name, 396 | .sidebar.longBreak .user-status, 397 | .sidebar.longBreak .user-dropdown-action { 398 | color: var(--long-break-timer-color); 399 | } 400 | 401 | .sidebar.longBreak .user-dropdown-action:hover { 402 | background: var(--long-break-secondary-btn); 403 | } -------------------------------------------------------------------------------- /src/styles/smart-indicator.css: -------------------------------------------------------------------------------- 1 | /* Settings Indicators Container */ 2 | .settings-indicators { 3 | position: absolute; 4 | bottom: 50%; 5 | right: 20px; 6 | transform: translateY(50%); 7 | display: flex; 8 | flex-direction: column; 9 | gap: 8px; 10 | z-index: 100; 11 | } 12 | 13 | /* Smart Pause Container for indicator and countdown */ 14 | .smart-pause-container { 15 | position: relative; 16 | display: inline-block; 17 | } 18 | 19 | /* Smart Pause Countdown Number */ 20 | .countdown-number { 21 | position: absolute; 22 | top: 0; 23 | right: 0; 24 | color: inherit; 25 | font-size: 10px; 26 | font-weight: bold; 27 | min-width: 12px; 28 | height: 12px; 29 | display: flex; 30 | align-items: center; 31 | justify-content: center; 32 | z-index: 101; 33 | } 34 | 35 | /* Base styles for all setting indicators */ 36 | .settings-indicators i { 37 | font-size: 20px; 38 | cursor: pointer; 39 | border-radius: 50%; 40 | padding: 8px; 41 | backdrop-filter: blur(5px); 42 | transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 43 | opacity: 0.7; 44 | transform: scale(1); 45 | 46 | /* Default color (focus state) */ 47 | color: var(--focus-timer-color); 48 | } 49 | 50 | /* Smart Pause Indicator Styles */ 51 | #smart-indicator { 52 | /* Initially hidden */ 53 | display: none; 54 | } 55 | 56 | /* Auto-start Indicator Styles */ 57 | #auto-start-indicator { 58 | /* Initially visible */ 59 | display: block; 60 | } 61 | 62 | /* Continuous Session Indicator Styles */ 63 | #continuous-session-indicator { 64 | /* Initially visible */ 65 | display: block; 66 | } 67 | 68 | /* Theme-based coloring for all indicators */ 69 | .container.focus .settings-indicators i { 70 | color: var(--focus-timer-color); 71 | } 72 | 73 | .container.break .settings-indicators i { 74 | color: var(--break-timer-color); 75 | } 76 | 77 | .container.longBreak .settings-indicators i { 78 | color: var(--long-break-timer-color); 79 | } 80 | 81 | /* Theme-based coloring for countdown number */ 82 | .container.focus .countdown-number { 83 | color: var(--focus-timer-color); 84 | } 85 | 86 | .container.break .countdown-number { 87 | color: var(--break-timer-color); 88 | } 89 | 90 | .container.longBreak .countdown-number { 91 | color: var(--long-break-timer-color); 92 | } 93 | 94 | /* Hover effects for all indicators */ 95 | .settings-indicators i:hover { 96 | opacity: 1; 97 | transform: scale(1.1); 98 | } 99 | 100 | /* Active state (when setting is enabled) */ 101 | .settings-indicators i.active { 102 | opacity: 1; 103 | } 104 | 105 | /* Theme-specific active states */ 106 | .container.focus .settings-indicators i.active { 107 | color: var(--focus-timer-color); 108 | } 109 | 110 | .container.break .settings-indicators i.active { 111 | color: var(--break-timer-color); 112 | } 113 | 114 | .container.longBreak .settings-indicators i.active { 115 | color: var(--long-break-timer-color); 116 | } 117 | 118 | /* Click animation for all indicators */ 119 | .settings-indicators i:active { 120 | transform: scale(0.95); 121 | transition: transform 0.1s ease; 122 | } 123 | 124 | /* Responsive adjustments */ 125 | @media (max-width: 768px) { 126 | .settings-indicators { 127 | bottom: 50%; 128 | right: 15px; 129 | transform: translateY(50%); 130 | gap: 6px; 131 | } 132 | 133 | .settings-indicators i { 134 | font-size: 16px; 135 | padding: 6px; 136 | } 137 | } 138 | 139 | /* When indicators are hidden */ 140 | .settings-indicators i[style*="display: none"] { 141 | display: none !important; 142 | } -------------------------------------------------------------------------------- /src/styles/statistics.css: -------------------------------------------------------------------------------- 1 | /* Statistics Section Styles - Simplified using shared components */ 2 | 3 | /* Statistics uses base-card styling from shared-components.css */ 4 | 5 | /* Day stat specific styling - extends day-stat-base */ 6 | .day-label { 7 | font-size: 0.8rem; 8 | font-weight: 600; 9 | margin-bottom: 0.25rem; 10 | } 11 | 12 | .day-count { 13 | font-size: 1.2rem; 14 | font-weight: 700; 15 | } 16 | 17 | .day-time { 18 | font-size: 0.7rem; 19 | opacity: 0.8; 20 | } 21 | 22 | /* Dark theme styles for statistics */ 23 | [data-theme="dark"] .stats-section { 24 | background: #1e293b; 25 | border-color: #334155; 26 | } 27 | 28 | [data-theme="dark"] .day-stat { 29 | background: #334155; 30 | border-color: #475569; 31 | } 32 | 33 | [data-theme="dark"] .day-stat.today { 34 | border-color: var(--accent-color); 35 | background: rgba(155, 89, 182, 0.2); 36 | } 37 | 38 | [data-theme="dark"] .day-stat.completed { 39 | background: var(--focus-color); 40 | color: white; 41 | } 42 | 43 | [data-theme="dark"] .stat-item { 44 | background: #334155; 45 | border-color: #475569; 46 | } 47 | 48 | /* Auto theme dark styles for statistics */ 49 | @media (prefers-color-scheme: dark) { 50 | [data-theme="auto"] .stats-section { 51 | background: #1e293b; 52 | border-color: #334155; 53 | } 54 | 55 | [data-theme="auto"] .day-stat { 56 | background: #334155; 57 | border-color: #475569; 58 | } 59 | 60 | [data-theme="auto"] .day-stat.today { 61 | border-color: var(--accent-color); 62 | background: rgba(155, 89, 182, 0.2); 63 | } 64 | 65 | [data-theme="auto"] .day-stat.completed { 66 | background: var(--focus-color); 67 | color: white; 68 | } 69 | 70 | [data-theme="auto"] .stat-item { 71 | background: #334155; 72 | border-color: #475569; 73 | } 74 | } 75 | 76 | /* Statistics inherit user theme colors */ 77 | .week-day-bar, 78 | .hour-bar-focus, 79 | .calendar-dot, 80 | .day-stat.completed, 81 | .day-stat-base.completed { 82 | background: var(--focus-color) !important; 83 | } 84 | 85 | .hour-bar-break { 86 | background: var(--break-color) !important; 87 | } 88 | 89 | .legend-color.focus { 90 | background: var(--focus-color) !important; 91 | } 92 | 93 | .legend-color.break { 94 | background: var(--break-color) !important; 95 | } 96 | 97 | /* Calendar day with sessions uses focus color */ 98 | .calendar-day.has-sessions { 99 | border-color: var(--focus-color) !important; 100 | } 101 | 102 | [data-theme="light"] .calendar-day.has-sessions { 103 | background: color-mix(in srgb, var(--focus-color) 15%, white) !important; 104 | } 105 | 106 | [data-theme="dark"] .calendar-day.has-sessions { 107 | background: color-mix(in srgb, var(--focus-color) 20%, #334155) !important; 108 | } 109 | 110 | @media (prefers-color-scheme: dark) { 111 | [data-theme="auto"] .calendar-day.has-sessions { 112 | background: color-mix(in srgb, var(--focus-color) 20%, #334155) !important; 113 | } 114 | } 115 | 116 | /* Weekly stats colors inherit from timer theme */ 117 | .week-bar { 118 | background: var(--focus-color) !important; 119 | } 120 | 121 | .week-bar:hover { 122 | box-shadow: 0 4px 12px color-mix(in srgb, var(--focus-color) 30%, transparent) !important; 123 | } 124 | 125 | /* Metric values use timer colors */ 126 | .metric-value { 127 | color: var(--focus-timer-color) !important; 128 | } 129 | 130 | /* Focus metric active state */ 131 | .focus-metric .metric-change.positive { 132 | color: var(--break-color) !important; 133 | background: color-mix(in srgb, var(--break-color) 10%, transparent) !important; 134 | } 135 | 136 | .focus-metric .metric-change.negative { 137 | color: var(--focus-color) !important; 138 | background: color-mix(in srgb, var(--focus-color) 10%, transparent) !important; 139 | } 140 | 141 | /* Tag Usage Pie Chart Styles */ 142 | .tag-usage-card { 143 | background: white; 144 | border-radius: 12px; 145 | padding: 24px; 146 | margin-bottom: 24px; 147 | box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05); 148 | border: 1px solid #e5e7eb; 149 | } 150 | 151 | .tag-usage-card h3 { 152 | margin: 0 0 20px 0; 153 | color: #374151; 154 | font-size: 1.1rem; 155 | font-weight: 600; 156 | } 157 | 158 | .tag-chart-container { 159 | display: flex; 160 | align-items: center; 161 | gap: 24px; 162 | min-height: 200px; 163 | } 164 | 165 | .tag-pie-chart { 166 | position: relative; 167 | width: 200px; 168 | height: 200px; 169 | border-radius: 50%; 170 | flex-shrink: 0; 171 | } 172 | 173 | .pie-chart-placeholder { 174 | display: flex; 175 | flex-direction: column; 176 | align-items: center; 177 | justify-content: center; 178 | width: 100%; 179 | height: 100%; 180 | color: #9ca3af; 181 | border: 2px dashed #d1d5db; 182 | border-radius: 50%; 183 | } 184 | 185 | .pie-chart-placeholder i { 186 | font-size: 48px; 187 | margin-bottom: 8px; 188 | } 189 | 190 | .pie-chart-placeholder span { 191 | font-size: 14px; 192 | text-align: center; 193 | } 194 | 195 | .tag-legend { 196 | flex: 1; 197 | display: flex; 198 | flex-direction: column; 199 | gap: 12px; 200 | } 201 | 202 | .tag-legend-item { 203 | display: flex; 204 | align-items: center; 205 | gap: 12px; 206 | } 207 | 208 | .tag-legend-color { 209 | width: 16px; 210 | height: 16px; 211 | border-radius: 4px; 212 | flex-shrink: 0; 213 | } 214 | 215 | .tag-legend-info { 216 | flex: 1; 217 | } 218 | 219 | .tag-legend-name { 220 | font-weight: 500; 221 | color: #374151; 222 | font-size: 14px; 223 | } 224 | 225 | .tag-legend-stats { 226 | display: flex; 227 | align-items: center; 228 | gap: 8px; 229 | margin-top: 2px; 230 | } 231 | 232 | .tag-legend-time { 233 | color: #6b7280; 234 | font-size: 13px; 235 | } 236 | 237 | .tag-legend-percent { 238 | color: #9ca3af; 239 | font-size: 12px; 240 | font-weight: 500; 241 | } 242 | 243 | .tag-legend-others { 244 | opacity: 0.8; 245 | border-top: 1px solid #f3f4f6; 246 | padding-top: 8px; 247 | margin-top: 4px; 248 | } 249 | 250 | .tag-legend-others .tag-legend-name { 251 | font-style: italic; 252 | font-size: 13px; 253 | } 254 | 255 | /* Pie chart segments */ 256 | .pie-segment { 257 | position: absolute; 258 | width: 100%; 259 | height: 100%; 260 | border-radius: 50%; 261 | clip-path: polygon(50% 50%, 50% 0%, 100% 0%, 100% 100%, 0% 100%, 0% 0%, 50% 0%); 262 | } 263 | 264 | /* Dark theme styles for tag usage chart */ 265 | [data-theme="dark"] .tag-usage-card { 266 | background: #1e293b; 267 | border-color: #334155; 268 | color: #f1f5f9; 269 | } 270 | 271 | [data-theme="dark"] .tag-usage-card h3 { 272 | color: #f1f5f9; 273 | } 274 | 275 | [data-theme="dark"] .pie-chart-placeholder { 276 | color: #64748b; 277 | border-color: #475569; 278 | } 279 | 280 | [data-theme="dark"] .tag-legend-name { 281 | color: #f1f5f9; 282 | } 283 | 284 | [data-theme="dark"] .tag-legend-time { 285 | color: #94a3b8; 286 | } 287 | 288 | [data-theme="dark"] .tag-legend-percent { 289 | color: #64748b; 290 | } 291 | 292 | /* Auto theme dark styles for tag usage chart */ 293 | @media (prefers-color-scheme: dark) { 294 | [data-theme="auto"] .tag-usage-card { 295 | background: #1e293b; 296 | border-color: #334155; 297 | color: #f1f5f9; 298 | } 299 | 300 | [data-theme="auto"] .tag-usage-card h3 { 301 | color: #f1f5f9; 302 | } 303 | 304 | [data-theme="auto"] .pie-chart-placeholder { 305 | color: #64748b; 306 | border-color: #475569; 307 | } 308 | 309 | [data-theme="auto"] .tag-legend-name { 310 | color: #f1f5f9; 311 | } 312 | 313 | [data-theme="auto"] .tag-legend-time { 314 | color: #94a3b8; 315 | } 316 | 317 | [data-theme="auto"] .tag-legend-percent { 318 | color: #64748b; 319 | } 320 | } -------------------------------------------------------------------------------- /src/styles/tasks.css: -------------------------------------------------------------------------------- 1 | /* Task Section Styles */ 2 | 3 | .task-section { 4 | width: 100%; 5 | background: white; 6 | padding: 1.5rem; 7 | border-radius: 15px; 8 | box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); 9 | } 10 | 11 | .task-section h3 { 12 | margin-bottom: 1rem; 13 | color: var(--accent-color); 14 | text-align: center; 15 | } 16 | 17 | #task-input { 18 | width: 100%; 19 | padding: 1rem; 20 | border: 2px solid #ecf0f1; 21 | border-radius: 10px; 22 | font-size: 1rem; 23 | margin-bottom: 1rem; 24 | box-sizing: border-box; 25 | } 26 | 27 | #task-input:focus { 28 | outline: none; 29 | border-color: var(--accent-color); 30 | } 31 | 32 | .task-list { 33 | max-height: 200px; 34 | overflow-y: auto; 35 | } 36 | 37 | .task-item { 38 | padding: 0.75rem; 39 | margin-bottom: 0.5rem; 40 | background: #f8f9fa; 41 | border-radius: 8px; 42 | display: flex; 43 | justify-content: space-between; 44 | align-items: center; 45 | animation: slideIn 0.3s ease-out; 46 | } 47 | 48 | .task-item.completed { 49 | opacity: 0.6; 50 | text-decoration: line-through; 51 | } 52 | 53 | .task-delete { 54 | background: var(--focus-color); 55 | color: white; 56 | border: none; 57 | border-radius: 50%; 58 | width: 24px; 59 | height: 24px; 60 | cursor: pointer; 61 | font-size: 12px; 62 | } 63 | -------------------------------------------------------------------------------- /src/styles/team.css: -------------------------------------------------------------------------------- 1 | /* Team View Styles */ 2 | 3 | #team-view { 4 | background-color: #f6f6f6 !important; 5 | min-height: 100vh; 6 | max-width: 1200px; 7 | padding: 2rem !important; 8 | box-sizing: border-box; 9 | } 10 | 11 | /* Team specific subtitle */ 12 | .team-subtitle { 13 | text-align: center; 14 | color: var(--text-light); 15 | margin-bottom: 2rem; 16 | font-size: 1rem; 17 | } 18 | 19 | /* Team Members Section */ 20 | .team-members-container h2 { 21 | margin-bottom: 1.5rem; 22 | color: var(--accent-color); 23 | font-size: 1.5rem; 24 | } 25 | 26 | .team-members-grid { 27 | display: flex; 28 | flex-direction: column; 29 | gap: 2rem; 30 | } 31 | 32 | /* Team Section - extends base-card-compact */ 33 | .team-header h3 { 34 | margin: 0 0 0.25rem 0; 35 | color: var(--accent-color); 36 | font-size: 1.2rem; 37 | font-weight: 600; 38 | } 39 | 40 | .team-description { 41 | margin: 0 0 1rem 0; 42 | color: var(--text-light); 43 | font-size: 0.85rem; 44 | } 45 | 46 | /* Compact Member Rows */ 47 | .team-members-table { 48 | display: flex; 49 | flex-direction: column; 50 | gap: 1px; 51 | } 52 | 53 | /* Member Basic Info */ 54 | .member-basic-info { 55 | display: flex; 56 | align-items: center; 57 | gap: 0.5rem; 58 | } 59 | 60 | .member-avatar-small { 61 | width: 28px; 62 | height: 28px; 63 | border-radius: 50%; 64 | background: linear-gradient(135deg, var(--accent-color), #e74c3c); 65 | display: flex; 66 | align-items: center; 67 | justify-content: center; 68 | color: white; 69 | font-weight: 600; 70 | font-size: 0.75rem; 71 | position: relative; 72 | flex-shrink: 0; 73 | } 74 | 75 | .member-details { 76 | display: flex; 77 | flex-direction: column; 78 | min-width: 0; 79 | } 80 | 81 | .member-name { 82 | font-weight: 600; 83 | font-size: 0.85rem; 84 | color: #2c3e50; 85 | white-space: nowrap; 86 | overflow: hidden; 87 | text-overflow: ellipsis; 88 | } 89 | 90 | .member-role-small { 91 | font-size: 0.7rem; 92 | color: var(--text-light); 93 | white-space: nowrap; 94 | overflow: hidden; 95 | text-overflow: ellipsis; 96 | } 97 | 98 | /* Status Info */ 99 | .member-status-info { 100 | display: flex; 101 | flex-direction: column; 102 | align-items: center; 103 | gap: 0.15rem; 104 | } 105 | 106 | /* Timer styling - extends badge-base variants */ 107 | .member-timer-small { 108 | font-size: 0.8rem; 109 | font-weight: 600; 110 | padding: 0.1rem 0.3rem; 111 | border-radius: 2px; 112 | background: rgba(0, 0, 0, 0.05); 113 | } 114 | 115 | .member-timer-small.focus { 116 | color: var(--focus-color); 117 | background: rgba(231, 76, 60, 0.1); 118 | } 119 | 120 | .member-timer-small.break { 121 | color: var(--break-color); 122 | background: rgba(46, 204, 113, 0.1); 123 | } 124 | 125 | .member-timer-small.long-break { 126 | color: var(--long-break-color); 127 | background: rgba(52, 152, 219, 0.1); 128 | } 129 | 130 | .member-timer-small.offline, 131 | .member-timer-small.privacy { 132 | color: var(--text-light); 133 | background: rgba(149, 165, 166, 0.1); 134 | } 135 | 136 | /* Activity Info */ 137 | .member-activity-small { 138 | font-size: 0.75rem; 139 | color: var(--text-light); 140 | text-align: right; 141 | white-space: nowrap; 142 | overflow: hidden; 143 | text-overflow: ellipsis; 144 | } 145 | 146 | /* Online Status Indicator - Smaller version */ 147 | .online-indicator-small { 148 | position: absolute; 149 | bottom: -1px; 150 | right: -1px; 151 | width: 8px; 152 | height: 8px; 153 | border-radius: 50%; 154 | border: 1px solid white; 155 | } 156 | 157 | .online-indicator-small.online { 158 | background: var(--break-color); 159 | } 160 | 161 | .online-indicator-small.offline { 162 | background: #95a5a6; 163 | } 164 | 165 | .online-indicator-small.privacy { 166 | background: #e67e22; 167 | } 168 | 169 | /* Status-specific row styling - extends row-base */ 170 | .member-row.status-focus { 171 | border-left-color: var(--focus-color); 172 | } 173 | 174 | .member-row.status-break { 175 | border-left-color: var(--break-color); 176 | } 177 | 178 | .member-row.status-long-break { 179 | border-left-color: var(--long-break-color); 180 | } 181 | 182 | .member-row.status-privacy { 183 | border-left-color: #e67e22; 184 | } 185 | 186 | .member-row.status-offline { 187 | border-left-color: #95a5a6; 188 | background: rgba(149, 165, 166, 0.05); 189 | opacity: 0.6; 190 | } 191 | 192 | /* Dark theme styles for team */ 193 | [data-theme="dark"] .team-stat-card { 194 | background: #1e293b; 195 | border-color: #334155; 196 | } 197 | 198 | [data-theme="dark"] .team-member-card { 199 | background: #1e293b; 200 | border-color: #334155; 201 | } 202 | 203 | [data-theme="dark"] .member-row { 204 | background: rgba(255, 255, 255, 0.05); 205 | } 206 | 207 | [data-theme="dark"] .member-row:hover { 208 | background: rgba(255, 255, 255, 0.1); 209 | } 210 | 211 | /* Auto theme dark styles for team */ 212 | @media (prefers-color-scheme: dark) { 213 | [data-theme="auto"] .team-stat-card { 214 | background: #1e293b; 215 | border-color: #334155; 216 | } 217 | 218 | [data-theme="auto"] .team-member-card { 219 | background: #1e293b; 220 | border-color: #334155; 221 | } 222 | 223 | [data-theme="auto"] .member-row { 224 | background: rgba(255, 255, 255, 0.05); 225 | } 226 | 227 | [data-theme="auto"] .member-row:hover { 228 | background: rgba(255, 255, 255, 0.1); 229 | } 230 | } 231 | 232 | /* Team specific responsive overrides */ 233 | @media (max-width: 768px) { 234 | .member-row { 235 | grid-template-columns: 1fr; 236 | gap: 0.5rem; 237 | text-align: center; 238 | } 239 | 240 | .member-basic-info { 241 | justify-content: center; 242 | } 243 | 244 | .member-activity-small { 245 | text-align: center; 246 | } 247 | } -------------------------------------------------------------------------------- /src/styles/themes/espresso.css: -------------------------------------------------------------------------------- 1 | /* Timer Theme: Espresso (Default) 2 | * Author: Stefano Novelli (murdercode@gmail.com) 3 | * Description: Warm, coffee-inspired colors with rich earth tones 4 | * Supports: Light + Dark mode 5 | */ 6 | 7 | /* IMMEDIATE DEFAULT COLORS - Applied without any conditions */ 8 | /* These provide instant fallbacks before any theme is applied */ 9 | :root { 10 | /* Timer Colors */ 11 | --focus-color: #e74c3c; 12 | --break-color: #2ecc71; 13 | --long-break-color: #3498db; 14 | 15 | /* Background Colors */ 16 | --focus-bg: #FFF2F2; 17 | --break-bg: #F0FAF0; 18 | --long-break-bg: #E8F4FF; 19 | 20 | /* Timer Text Colors */ 21 | --focus-timer-color: #471515; 22 | --break-timer-color: #14401D; 23 | --long-break-timer-color: #153047; 24 | 25 | /* Button Colors */ 26 | --focus-primary-btn: #FF7c7c; 27 | --break-primary-btn: #8CE8A1; 28 | --long-break-primary-btn: #8BCAFF; 29 | 30 | --focus-secondary-btn: #FFD9D9; 31 | --break-secondary-btn: #DAFAE0; 32 | --long-break-secondary-btn: #D9EEFF; 33 | 34 | /* Shared Component Colors */ 35 | --shared-bg: #f6f6f6; 36 | --shared-border: #e0e0e0; 37 | --shared-text: #333333; 38 | 39 | /* Additional theme colors */ 40 | --card-bg: white; 41 | --card-border: #f0f0f0; 42 | --stat-border: #e9ecef; 43 | --day-stat-bg: #f8f9fa; 44 | --hover-bg: #f8f9fa; 45 | --row-bg: rgba(0, 0, 0, 0.02); 46 | --row-hover-bg: rgba(0, 0, 0, 0.05); 47 | --focus-bg-alpha: rgba(231, 76, 60, 0.1); 48 | --text-on-focus: white; 49 | 50 | /* Settings components */ 51 | --setting-item-bg: #f8f9fa; 52 | --setting-item-hover-bg: #f5f5f5; 53 | --setting-item-hover-border: rgba(155, 89, 182, 0.2); 54 | --input-border: #e9ecef; 55 | --input-focus-shadow: rgba(155, 89, 182, 0.1); 56 | } 57 | 58 | /* Default Fallback Colors for Dark Mode (before theme is applied) */ 59 | [data-theme="dark"] { 60 | /* Timer Colors - darker versions */ 61 | --focus-color: #ef4444; 62 | --break-color: #22c55e; 63 | --long-break-color: #3b82f6; 64 | 65 | /* Background Colors - dark versions */ 66 | --focus-bg: #1e1b1b; 67 | --break-bg: #0f1f13; 68 | --long-break-bg: #0f1419; 69 | 70 | /* Timer Text Colors - light for dark background */ 71 | --focus-timer-color: #fca5a5; 72 | --break-timer-color: #86efac; 73 | --long-break-timer-color: #93c5fd; 74 | 75 | /* Button Colors - adjusted for dark theme */ 76 | --focus-primary-btn: #dc2626; 77 | --break-primary-btn: #16a34a; 78 | --long-break-primary-btn: #2563eb; 79 | 80 | --focus-secondary-btn: #450a0a; 81 | --break-secondary-btn: #052e16; 82 | --long-break-secondary-btn: #1e3a8a; 83 | 84 | /* Shared Component Colors for Dark Mode */ 85 | --shared-bg: #1e1b1b; 86 | --shared-border: #334155; 87 | --shared-text: #e2e8f0; 88 | 89 | /* Additional dark theme colors */ 90 | --card-bg: #222; 91 | --card-border: #334155; 92 | --stat-border: #334155; 93 | --day-stat-bg: #334155; 94 | --day-stat-border: #475569; 95 | --hover-bg: #3f4246; 96 | --row-bg: rgba(255, 255, 255, 0.05); 97 | --row-hover-bg: rgba(255, 255, 255, 0.1); 98 | --focus-bg-alpha: rgba(239, 68, 68, 0.2); 99 | --text-on-focus: white; 100 | 101 | /* Settings components for dark mode */ 102 | --setting-item-bg: #262323; 103 | --setting-item-hover-bg: #2e2b2b; 104 | --setting-item-hover-border: rgba(239, 68, 68, 0.2); 105 | --input-border: #334155; 106 | --input-focus-shadow: rgba(239, 68, 68, 0.1); 107 | } 108 | 109 | /* === SHARED COMPONENTS STYLES === */ 110 | /* Stili per statistiche e impostazioni nel tema Espresso */ 111 | 112 | /* Light mode shared components */ 113 | :root[data-timer-theme="espresso"] .view-section { 114 | background-color: var(--shared-bg) !important; 115 | } 116 | 117 | :root[data-timer-theme="espresso"] .base-card, 118 | :root[data-timer-theme="espresso"] .base-card-compact, 119 | :root[data-timer-theme="espresso"] .base-section { 120 | background: var(--card-bg); 121 | border-color: var(--card-border); 122 | } 123 | 124 | :root[data-timer-theme="espresso"] .stat-card { 125 | background: var(--card-bg); 126 | border-color: var(--stat-border); 127 | } 128 | 129 | :root[data-timer-theme="espresso"] .setting-item { 130 | background: var(--setting-item-bg); 131 | } 132 | 133 | :root[data-timer-theme="espresso"] .setting-item:hover { 134 | background: var(--setting-item-hover-bg); 135 | border-color: var(--setting-item-hover-border); 136 | } 137 | 138 | :root[data-timer-theme="espresso"] .setting-item input[type="number"] { 139 | border-color: var(--input-border); 140 | } 141 | 142 | :root[data-timer-theme="espresso"] .setting-item input[type="number"]:focus { 143 | border-color: var(--focus-color); 144 | box-shadow: 0 0 0 3px var(--input-focus-shadow); 145 | } 146 | 147 | :root[data-timer-theme="espresso"] .day-stat-base { 148 | background: var(--day-stat-bg); 149 | border-color: transparent; 150 | } 151 | 152 | :root[data-timer-theme="espresso"] .day-stat-base.today { 153 | border-color: var(--focus-color); 154 | background: var(--focus-bg-alpha); 155 | } 156 | 157 | :root[data-timer-theme="espresso"] .day-stat-base.completed { 158 | background: var(--focus-color); 159 | color: var(--text-on-focus); 160 | } 161 | 162 | :root[data-timer-theme="espresso"] .sidebar-base { 163 | background: var(--card-bg); 164 | border-color: var(--stat-border); 165 | } 166 | 167 | :root[data-timer-theme="espresso"] .row-base { 168 | background: var(--row-bg); 169 | } 170 | 171 | :root[data-timer-theme="espresso"] .row-base:hover { 172 | background: var(--row-hover-bg); 173 | } 174 | 175 | /* Dark mode shared components per tema Espresso */ 176 | :root[data-timer-theme="espresso"][data-theme="dark"] .view-section { 177 | background-color: var(--shared-bg) !important; 178 | color: var(--shared-text); 179 | } 180 | 181 | :root[data-timer-theme="espresso"][data-theme="dark"] .base-card, 182 | :root[data-timer-theme="espresso"][data-theme="dark"] .base-card-compact, 183 | :root[data-timer-theme="espresso"][data-theme="dark"] .base-section { 184 | background: var(--shared-bg); 185 | border-color: var(--shared-border); 186 | color: var(--shared-text); 187 | } 188 | 189 | :root[data-timer-theme="espresso"][data-theme="dark"] .stat-card { 190 | background: var(--card-bg); 191 | border-color: var(--stat-border); 192 | } 193 | 194 | :root[data-timer-theme="espresso"][data-theme="dark"] .setting-item { 195 | background: var(--setting-item-bg); 196 | } 197 | 198 | :root[data-timer-theme="espresso"][data-theme="dark"] .setting-item:hover { 199 | background: var(--setting-item-hover-bg); 200 | border-color: var(--setting-item-hover-border); 201 | } 202 | 203 | :root[data-timer-theme="espresso"][data-theme="dark"] .setting-item input[type="number"] { 204 | border-color: var(--input-border); 205 | background: var(--card-bg); 206 | color: var(--shared-text); 207 | } 208 | 209 | :root[data-timer-theme="espresso"][data-theme="dark"] .setting-item input[type="number"]:focus { 210 | border-color: var(--focus-color); 211 | box-shadow: 0 0 0 3px var(--input-focus-shadow); 212 | } 213 | 214 | :root[data-timer-theme="espresso"][data-theme="dark"] .day-stat-base { 215 | background: var(--day-stat-bg); 216 | border-color: var(--day-stat-border); 217 | } 218 | 219 | :root[data-timer-theme="espresso"][data-theme="dark"] .day-stat-base.today { 220 | border-color: var(--focus-color); 221 | background: var(--focus-bg-alpha); 222 | } 223 | 224 | :root[data-timer-theme="espresso"][data-theme="dark"] .day-stat-base.completed { 225 | background: var(--focus-color); 226 | color: var(--text-on-focus); 227 | } 228 | 229 | :root[data-timer-theme="espresso"][data-theme="dark"] .nav-item-base:hover { 230 | background: var(--hover-bg); 231 | } 232 | 233 | :root[data-timer-theme="espresso"][data-theme="dark"] .nav-item-base.active:hover { 234 | background: var(--focus-color) !important; 235 | color: var(--text-on-focus) !important; 236 | } 237 | 238 | :root[data-timer-theme="espresso"][data-theme="dark"] .sidebar-base { 239 | background: var(--card-bg); 240 | border-color: var(--stat-border); 241 | } 242 | 243 | :root[data-timer-theme="espresso"][data-theme="dark"] .row-base { 244 | background: var(--row-bg); 245 | } 246 | 247 | :root[data-timer-theme="espresso"][data-theme="dark"] .row-base:hover { 248 | background: var(--row-hover-bg); 249 | } -------------------------------------------------------------------------------- /src/styles/themes/pommodore64.css: -------------------------------------------------------------------------------- 1 | /* Timer Theme: Pommodore64 2 | * Author: (murdercode@gmail.com) 3 | * Description: Un tema retrò ispirato al Commodore 64 con color:root[data-timer-theme="pommodore64"] .progress-indicator { 4 | border-radius: 2px !important; 5 | border: 1px solid rgba(45, 52, 54, 0.3) !important;stalgici e font pixelato 6 | * Supports: Light mode only 7 | */ 8 | 9 | /* Import del font Orbitron per C64 */ 10 | @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@700;800;900&display=swap'); 11 | 12 | /* Light Mode Colors - Commodore 64 Style */ 13 | :root[data-timer-theme="pommodore64"] { 14 | /* Timer Colors - Ispirate al C64 */ 15 | --focus-color: #6c5ce7; 16 | /* Viola C64 */ 17 | --break-color: #0984e3; 18 | /* Blu C64 */ 19 | --long-break-color: #00b894; 20 | /* Verde acqua C64 */ 21 | 22 | /* Background Colors - Sfondo chiaro con accenti retro */ 23 | --focus-bg: #f8f7ff; 24 | /* Sfondo viola molto chiaro */ 25 | --break-bg: #f0f8ff; 26 | /* Sfondo blu molto chiaro */ 27 | --long-break-bg: #f0fff9; 28 | /* Sfondo verde molto chiaro */ 29 | 30 | /* Timer Text Colors - Contrastanti su sfondo chiaro */ 31 | --focus-timer-color: #2d3436; 32 | --break-timer-color: #2d3436; 33 | --long-break-timer-color: #2d3436; 34 | 35 | --focus-primary-btn: #a29bfe; 36 | /* Viola chiaro */ 37 | --break-primary-btn: #74b9ff; 38 | /* Blu chiaro */ 39 | --long-break-primary-btn: #55efc4; 40 | /* Verde chiaro */ 41 | 42 | --focus-secondary-btn: #e8e7ff; 43 | /* Viola molto chiaro */ 44 | --break-secondary-btn: #e8f4ff; 45 | /* Blu molto chiaro */ 46 | --long-break-secondary-btn: #e8fff5; 47 | /* Verde molto chiaro */ 48 | 49 | /* Shared Component Colors */ 50 | --shared-bg: #f5f6fa; 51 | --shared-border: #ddd; 52 | --shared-text: #2d3436; 53 | 54 | /* Additional theme colors */ 55 | --card-bg: #ffffff; 56 | --card-border: #e1e5e9; 57 | --stat-border: #e1e5e9; 58 | --day-stat-bg: #f1f2f6; 59 | --hover-bg: #f1f2f6; 60 | --row-bg: rgba(108, 92, 231, 0.02); 61 | --row-hover-bg: rgba(108, 92, 231, 0.05); 62 | --focus-bg-alpha: rgba(108, 92, 231, 0.1); 63 | --text-on-focus: white; 64 | 65 | /* Settings components */ 66 | --setting-item-bg: #f1f2f6; 67 | --setting-item-hover-bg: #e9ecef; 68 | --setting-item-hover-border: rgba(108, 92, 231, 0.2); 69 | --input-border: #ddd; 70 | --input-focus-shadow: rgba(108, 92, 231, 0.1); 71 | } 72 | 73 | /* Font Orbitron per il tema Pommodore64 */ 74 | :root[data-timer-theme="pommodore64"] .timer-minutes, 75 | :root[data-timer-theme="pommodore64"] .timer-seconds { 76 | font-family: 'Orbitron', monospace !important; 77 | font-weight: 900 !important; 78 | letter-spacing: 0.1em; 79 | text-shadow: 80 | 2px 2px 0px rgba(108, 92, 231, 0.3), 81 | 4px 4px 0px rgba(108, 92, 231, 0.2); 82 | font-size: 8em; 83 | } 84 | 85 | :root[data-timer-theme="pommodore64"] .timer-status { 86 | font-family: 'Orbitron', monospace !important; 87 | font-weight: 700 !important; 88 | font-size: 18px !important; 89 | letter-spacing: 0.15em; 90 | text-transform: uppercase; 91 | text-shadow: 1px 1px 0px rgba(108, 92, 231, 0.4); 92 | } 93 | 94 | :root[data-timer-theme="pommodore64"] .control-btn { 95 | border: 2px solid currentColor !important; 96 | border-radius: 4px !important; 97 | box-shadow: 3px 3px 0px rgba(45, 52, 54, 0.3) !important; 98 | transition: all 0.1s ease !important; 99 | } 100 | 101 | :root[data-timer-theme="pommodore64"] .control-btn:hover { 102 | transform: translate(-1px, -1px) !important; 103 | box-shadow: 4px 4px 0px rgba(45, 52, 54, 0.4) !important; 104 | } 105 | 106 | :root[data-timer-theme="pommodore64"] .control-btn:active { 107 | transform: translate(1px, 1px) !important; 108 | box-shadow: 1px 1px 0px rgba(45, 52, 54, 0.3) !important; 109 | } 110 | 111 | /* Status icons stile C64 */ 112 | :root[data-timer-theme="pommodore64"] #status-icon { 113 | filter: drop-shadow(1px 1px 0px rgba(108, 92, 231, 0.3)); 114 | } 115 | 116 | /* Progress dots stile pixel art */ 117 | :root[data-timer-theme="pommodore64"] .progress-dot { 118 | border-radius: 2px !important; 119 | border: 1px solid rgba(45, 52, 54, 0.3) !important; 120 | } 121 | 122 | :root[data-timer-theme="pommodore64"] .timer-container::before { 123 | content: ''; 124 | position: absolute; 125 | top: 0; 126 | left: 0; 127 | right: 0; 128 | bottom: 0; 129 | background: repeating-linear-gradient(0deg, 130 | transparent, 131 | transparent 2px, 132 | rgba(108, 92, 231, 0.03) 2px, 133 | rgba(108, 92, 231, 0.03) 4px); 134 | pointer-events: none; 135 | z-index: 1; 136 | } 137 | 138 | :root[data-timer-theme="pommodore64"] .timer-display { 139 | position: relative; 140 | z-index: 2; 141 | } 142 | 143 | /* Sidebar styling per coordinare con il tema */ 144 | :root[data-timer-theme="pommodore64"] .sidebar.focus { 145 | background: linear-gradient(135deg, var(--focus-color), #a29bfe) !important; 146 | } 147 | 148 | :root[data-timer-theme="pommodore64"] .sidebar.break { 149 | background: linear-gradient(135deg, var(--break-color), #74b9ff) !important; 150 | } 151 | 152 | :root[data-timer-theme="pommodore64"] .sidebar.longBreak { 153 | background: linear-gradient(135deg, var(--long-break-color), #55efc4) !important; 154 | } 155 | 156 | /* === SHARED COMPONENTS STYLES === */ 157 | /* Stili per statistiche e impostazioni nel tema Pommodore64 */ 158 | 159 | :root[data-timer-theme="pommodore64"] .view-section { 160 | background-color: #f0f0f0 !important; 161 | } 162 | 163 | :root[data-timer-theme="pommodore64"] .base-card, 164 | :root[data-timer-theme="pommodore64"] .base-card-compact, 165 | :root[data-timer-theme="pommodore64"] .base-section { 166 | background: white; 167 | border: 2px solid #ddd; 168 | border-radius: 4px; 169 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 170 | } 171 | 172 | :root[data-timer-theme="pommodore64"] .stat-card { 173 | background: white; 174 | border: 2px solid #ddd; 175 | border-radius: 4px; 176 | box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1); 177 | } 178 | 179 | :root[data-timer-theme="pommodore64"] .day-stat-base { 180 | background: #e8e8e8; 181 | border: 2px solid transparent; 182 | border-radius: 4px; 183 | font-family: 'Orbitron', monospace; 184 | font-weight: 500; 185 | } 186 | 187 | :root[data-timer-theme="pommodore64"] .day-stat-base.today { 188 | border-color: var(--focus-color); 189 | background: var(--focus-bg); 190 | box-shadow: 0 0 5px rgba(108, 92, 231, 0.3); 191 | } 192 | 193 | :root[data-timer-theme="pommodore64"] .day-stat-base.completed { 194 | background: var(--focus-color); 195 | color: white; 196 | border-color: var(--focus-color); 197 | } 198 | 199 | :root[data-timer-theme="pommodore64"] .nav-item-base { 200 | font-family: 'Orbitron', monospace; 201 | font-weight: 500; 202 | border-radius: 4px; 203 | } 204 | 205 | :root[data-timer-theme="pommodore64"] .nav-item-base:hover { 206 | background: #e8e8e8; 207 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 208 | } 209 | 210 | :root[data-timer-theme="pommodore64"] .nav-item-base.active { 211 | background: var(--focus-color); 212 | color: white; 213 | box-shadow: 0 2px 8px rgba(108, 92, 231, 0.3); 214 | } 215 | 216 | :root[data-timer-theme="pommodore64"] .nav-item-base.active:hover { 217 | background: var(--focus-color) !important; 218 | color: white !important; 219 | } 220 | 221 | :root[data-timer-theme="pommodore64"] .sidebar-base { 222 | background: white; 223 | border-color: #ddd; 224 | box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1); 225 | } 226 | 227 | :root[data-timer-theme="pommodore64"] .row-base { 228 | background: #f8f8f8; 229 | border-radius: 4px; 230 | } 231 | 232 | :root[data-timer-theme="pommodore64"] .row-base:hover { 233 | background: #e8e8e8; 234 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 235 | } 236 | 237 | :root[data-timer-theme="pommodore64"] .section-header, 238 | :root[data-timer-theme="pommodore64"] .page-header, 239 | :root[data-timer-theme="pommodore64"] .card-header { 240 | color: var(--focus-color); 241 | font-family: 'Orbitron', monospace; 242 | font-weight: 600; 243 | } 244 | 245 | :root[data-timer-theme="pommodore64"] .stat-number { 246 | color: var(--focus-color); 247 | font-family: 'Orbitron', monospace; 248 | font-weight: 600; 249 | } 250 | 251 | :root[data-timer-theme="pommodore64"] .stat-label { 252 | color: #666; 253 | font-family: 'Orbitron', monospace; 254 | font-weight: 400; 255 | } 256 | 257 | :root[data-timer-theme="pommodore64"] .badge-focus { 258 | background: var(--focus-bg); 259 | color: var(--focus-color); 260 | font-family: 'Orbitron', monospace; 261 | font-size: 0.6rem; 262 | font-weight: 500; 263 | } 264 | 265 | :root[data-timer-theme="pommodore64"] .badge-break { 266 | background: var(--break-bg); 267 | color: var(--break-color); 268 | font-family: 'Orbitron', monospace; 269 | font-size: 0.6rem; 270 | font-weight: 500; 271 | } 272 | 273 | :root[data-timer-theme="pommodore64"] .badge-long-break { 274 | background: var(--long-break-bg); 275 | color: var(--long-break-color); 276 | font-family: 'Orbitron', monospace; 277 | font-size: 0.6rem; 278 | font-weight: 500; 279 | } 280 | 281 | /* === SHARED COMPONENTS STYLES === */ 282 | /* Stili per statistiche e impostazioni nel tema Pommodore64 */ 283 | 284 | :root[data-timer-theme="pommodore64"] .view-section { 285 | background-color: var(--shared-bg) !important; 286 | } 287 | 288 | :root[data-timer-theme="pommodore64"] .base-card, 289 | :root[data-timer-theme="pommodore64"] .base-card-compact, 290 | :root[data-timer-theme="pommodore64"] .base-section { 291 | background: var(--card-bg); 292 | border-color: var(--card-border); 293 | } 294 | 295 | :root[data-timer-theme="pommodore64"] .stat-card { 296 | background: var(--card-bg); 297 | border-color: var(--stat-border); 298 | } 299 | 300 | :root[data-timer-theme="pommodore64"] .setting-item { 301 | background: var(--setting-item-bg); 302 | } 303 | 304 | :root[data-timer-theme="pommodore64"] .setting-item:hover { 305 | background: var(--setting-item-hover-bg); 306 | border-color: var(--setting-item-hover-border); 307 | } 308 | 309 | :root[data-timer-theme="pommodore64"] .setting-item input[type="number"] { 310 | border-color: var(--input-border); 311 | } 312 | 313 | :root[data-timer-theme="pommodore64"] .setting-item input[type="number"]:focus { 314 | border-color: var(--focus-color); 315 | box-shadow: 0 0 0 3px var(--input-focus-shadow); 316 | } 317 | 318 | :root[data-timer-theme="pommodore64"] .day-stat-base { 319 | background: var(--day-stat-bg); 320 | border-color: transparent; 321 | } -------------------------------------------------------------------------------- /src/styles/timeline.css: -------------------------------------------------------------------------------- 1 | /* Timeline and Session Management Styles - Simplified */ 2 | 3 | .sessions-timeline { 4 | margin-top: 1rem; 5 | background: #f8f9fa; 6 | border-radius: 6px; 7 | padding: 0.5rem; 8 | min-height: 80px; 9 | height: auto; 10 | position: relative; 11 | } 12 | 13 | .timeline-hours { 14 | position: relative; 15 | margin-bottom: 0.2rem; 16 | height: 12px; 17 | padding: 0 5px; 18 | } 19 | 20 | .timeline-hour { 21 | font-size: 9px; 22 | color: #9ca3af; 23 | position: absolute; 24 | text-align: center; 25 | display: flex; 26 | align-items: center; 27 | justify-content: center; 28 | height: 12px; 29 | font-weight: 400; 30 | letter-spacing: 0.5px; 31 | transform: translateX(-50%); 32 | } 33 | 34 | .timeline-hour::after { 35 | display: none; 36 | } 37 | 38 | .timeline-track { 39 | position: relative; 40 | height: 40px; 41 | min-height: 40px; 42 | background: white; 43 | border-radius: 4px; 44 | border: 1px solid #e0e0e0; 45 | overflow: visible; 46 | transition: height 0.3s ease; 47 | } 48 | 49 | .timeline-grid-line { 50 | position: absolute; 51 | top: 0; 52 | bottom: 0; 53 | width: 1px; 54 | background: #e5e7eb; 55 | z-index: 1; 56 | pointer-events: none; 57 | } 58 | 59 | .timeline-session { 60 | position: absolute; 61 | height: 15px; 62 | border-radius: 4px; 63 | cursor: move; 64 | display: flex; 65 | align-items: center; 66 | justify-content: center; 67 | font-size: 0.6rem; 68 | font-weight: 500; 69 | color: white; 70 | min-width: 30px; 71 | user-select: none; 72 | transition: all 0.2s ease; 73 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 74 | z-index: 10; 75 | } 76 | 77 | .timeline-session-content { 78 | cursor: move; 79 | pointer-events: none; 80 | flex: 1; 81 | display: flex; 82 | flex-direction: column; 83 | align-items: center; 84 | justify-content: center; 85 | } 86 | 87 | .timeline-session-content-minimal { 88 | cursor: move; 89 | pointer-events: none; 90 | } 91 | 92 | .timeline-session:hover { 93 | transform: translateY(-1px); 94 | box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); 95 | } 96 | 97 | .timeline-session.focus { 98 | background: #e74c3c; 99 | } 100 | 101 | .timeline-session.break { 102 | background: #2ecc71; 103 | } 104 | 105 | .timeline-session.longBreak { 106 | background: #3498db; 107 | } 108 | 109 | .timeline-session.custom { 110 | background: #9b59b6; 111 | } 112 | 113 | 114 | /* Today's Sessions - Minimal Display */ 115 | .timeline-session.today-session { 116 | height: 15px; 117 | z-index: 10; 118 | } 119 | 120 | .timeline-session.today-session .timeline-session-content-minimal { 121 | width: 100%; 122 | height: 100%; 123 | pointer-events: none; 124 | } 125 | 126 | .timeline-session.today-session .timeline-session-type, 127 | .timeline-session.today-session .timeline-session-time { 128 | display: none; 129 | } 130 | 131 | .timeline-session.today-session .session-handle { 132 | opacity: 0; 133 | transition: opacity 0.2s ease; 134 | z-index: 15; 135 | position: absolute; 136 | top: 0; 137 | bottom: 0; 138 | width: 8px; 139 | cursor: ew-resize; 140 | } 141 | 142 | .timeline-session.today-session .session-handle.left { 143 | left: -4px; 144 | } 145 | 146 | .timeline-session.today-session .session-handle.right { 147 | right: -4px; 148 | } 149 | 150 | .timeline-session.today-session:hover .session-handle { 151 | opacity: 1; 152 | } 153 | 154 | /* Enhanced tooltip styling for today's sessions */ 155 | .timeline-session.today-session { 156 | position: relative; 157 | } 158 | 159 | .timeline-session.today-session:hover { 160 | transform: translateY(-3px); 161 | box-shadow: 0 6px 15px rgba(0, 0, 0, 0.3); 162 | z-index: 20; 163 | } 164 | 165 | .timeline-session.time-constraint { 166 | border: 2px solid #ffa500; 167 | } 168 | 169 | .timeline-track:empty::before { 170 | content: 'Click "+" to add a session or drag existing sessions here'; 171 | position: absolute; 172 | top: 50%; 173 | left: 50%; 174 | transform: translate(-50%, -50%); 175 | color: var(--text-light); 176 | font-size: 0.8rem; 177 | text-align: center; 178 | pointer-events: none; 179 | } 180 | 181 | /* Session Handle Visual Indicators */ 182 | .session-handle { 183 | position: absolute; 184 | top: 0; 185 | bottom: 0; 186 | width: 8px; 187 | cursor: ew-resize; 188 | z-index: 20; 189 | opacity: 0; 190 | transition: opacity 0.2s ease; 191 | } 192 | 193 | .session-handle.left { 194 | left: -4px; 195 | } 196 | 197 | .session-handle.right { 198 | right: -4px; 199 | } 200 | 201 | .timeline-session:hover .session-handle { 202 | opacity: 1; 203 | } 204 | 205 | .session-handle::before { 206 | content: ''; 207 | position: absolute; 208 | top: 50%; 209 | left: 50%; 210 | transform: translate(-50%, -50%); 211 | width: 2px; 212 | height: 12px; 213 | background: rgba(255, 255, 255, 0.8); 214 | border-radius: 1px; 215 | } 216 | 217 | .session-handle::after { 218 | content: ''; 219 | position: absolute; 220 | top: 50%; 221 | left: 50%; 222 | transform: translate(-50%, -50%); 223 | width: 2px; 224 | height: 12px; 225 | background: rgba(255, 255, 255, 0.8); 226 | border-radius: 1px; 227 | margin-left: 3px; 228 | } 229 | 230 | .session-handle.left::after { 231 | margin-left: -3px; 232 | } 233 | 234 | /* Dragging States */ 235 | .timeline-session.dragging { 236 | z-index: 10; 237 | transform: scale(1.02); 238 | box-shadow: 0 3px 8px rgba(0, 0, 0, 0.2); 239 | } 240 | 241 | .timeline-session.resizing { 242 | z-index: 10; 243 | } 244 | 245 | .timeline-track.drop-zone { 246 | border-color: var(--accent-color); 247 | background: rgba(155, 89, 182, 0.05); 248 | } 249 | 250 | .session-context-menu { 251 | position: fixed; 252 | background: white; 253 | border-radius: 6px; 254 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); 255 | padding: 0.3rem 0; 256 | z-index: 1000; 257 | display: none; 258 | min-width: 100px; 259 | } 260 | 261 | .context-menu-item { 262 | padding: 0.4rem 0.8rem; 263 | cursor: pointer; 264 | font-size: 0.75rem; 265 | transition: background 0.2s ease; 266 | } 267 | 268 | .context-menu-item:hover { 269 | background: #f8f9fa; 270 | } 271 | 272 | .context-menu-item.danger { 273 | color: #dc3545; 274 | } 275 | 276 | .context-menu-item.danger:hover { 277 | background: #ffe6e6; 278 | } 279 | 280 | .week-average-line { 281 | position: absolute; 282 | height: 2px; 283 | z-index: 10; 284 | opacity: 0.8; 285 | border-radius: 1px; 286 | pointer-events: none; 287 | } 288 | 289 | .week-average-line::before { 290 | content: ''; 291 | position: absolute; 292 | left: 0; 293 | top: -2px; 294 | width: 4px; 295 | height: 4px; 296 | background: #3498db; 297 | border-radius: 50%; 298 | box-shadow: 0 0 0 1px white; 299 | } 300 | 301 | .week-average-line::after { 302 | content: ''; 303 | position: absolute; 304 | right: 0; 305 | top: -2px; 306 | width: 4px; 307 | height: 4px; 308 | background: #3498db; 309 | border-radius: 50%; 310 | box-shadow: 0 0 0 1px white; 311 | } 312 | 313 | .week-average-label { 314 | position: absolute; 315 | right: 5px; 316 | top: -15px; 317 | font-size: 9px; 318 | color: #3498db; 319 | font-weight: 500; 320 | background: white; 321 | padding: 1px 3px; 322 | border-radius: 2px; 323 | white-space: nowrap; 324 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); 325 | border: 1px solid rgba(52, 152, 219, 0.2); 326 | } 327 | 328 | .timeline-session.session-stacked { 329 | box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); 330 | border: 1px solid rgba(255, 255, 255, 0.5); 331 | } 332 | 333 | .timeline-session.session-stacked:hover { 334 | box-shadow: 0 3px 8px rgba(0, 0, 0, 0.2); 335 | border-color: rgba(255, 255, 255, 0.8); 336 | } 337 | 338 | /* Session Hover Tooltip */ 339 | .session-hover-tooltip { 340 | background: rgba(0, 0, 0, 0.9); 341 | color: white; 342 | border-radius: 6px; 343 | padding: 8px 12px; 344 | font-size: 12px; 345 | line-height: 1.4; 346 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); 347 | max-width: 250px; 348 | } 349 | 350 | .session-hover-tooltip .tooltip-content { 351 | display: flex; 352 | flex-direction: column; 353 | gap: 2px; 354 | } 355 | 356 | .session-hover-tooltip .tooltip-type { 357 | font-weight: 600; 358 | color: #fff; 359 | } 360 | 361 | .session-hover-tooltip .tooltip-time { 362 | font-weight: 500; 363 | color: #e0e0e0; 364 | } 365 | 366 | .session-hover-tooltip .tooltip-duration { 367 | color: #b0b0b0; 368 | font-size: 11px; 369 | } 370 | 371 | .session-hover-tooltip .tooltip-notes { 372 | color: #d0d0d0; 373 | font-style: italic; 374 | margin-top: 2px; 375 | padding-top: 4px; 376 | border-top: 1px solid rgba(255, 255, 255, 0.2); 377 | } -------------------------------------------------------------------------------- /src/styles/timer.css: -------------------------------------------------------------------------------- 1 | /* Timer Display Styles */ 2 | 3 | .timer-container { 4 | text-align: center; 5 | padding: 3rem 2rem; 6 | background: transparent; 7 | } 8 | 9 | /* Timer Status Container with Adjust Buttons */ 10 | .timer-status-container { 11 | display: flex; 12 | align-items: start; 13 | justify-content: center; 14 | gap: 0.25rem; 15 | margin-bottom: 0.5rem; 16 | } 17 | 18 | .timer-adjust-btn { 19 | display: flex; 20 | align-items: center; 21 | justify-content: center; 22 | height: 32px; 23 | border: none; 24 | background-color: transparent; 25 | cursor: pointer; 26 | transition: all 0.2s ease; 27 | font-size: 12px; 28 | font-weight: 600; 29 | color: var(--focus-timer-color); 30 | padding: 0; 31 | } 32 | 33 | .timer-adjust-btn:hover { 34 | transform: scale(1.05); 35 | } 36 | 37 | .timer-adjust-btn:active { 38 | transform: scale(0.95); 39 | } 40 | 41 | .timer-adjust-btn i { 42 | font-size: 14px; 43 | line-height: 1; 44 | } 45 | 46 | .timer-adjust-btn span { 47 | font-size: 14px; 48 | font-weight: 700; 49 | line-height: 1; 50 | } 51 | 52 | /* Timer status label - positioned outside timer-container in HTML */ 53 | #timer-view .timer-status { 54 | font-size: 14px; 55 | margin-bottom: 0.5rem; 56 | color: var(--focus-timer-color); 57 | font-family: 'Roboto Flex', sans-serif; 58 | font-weight: 800; 59 | text-align: center; 60 | display: inline-flex; 61 | align-items: center; 62 | justify-content: center; 63 | padding: 0.2rem 1rem; 64 | border-radius: 2rem; 65 | background-color: var(--focus-secondary-btn); 66 | border: 2px solid var(--focus-timer-color); 67 | } 68 | 69 | .timer-status { 70 | font-size: 14px; 71 | margin-bottom: 0.5rem; 72 | color: var(--focus-timer-color); 73 | font-family: 'Roboto Flex', sans-serif; 74 | font-weight: 800; 75 | text-align: center; 76 | display: flex; 77 | align-items: center; 78 | justify-content: center; 79 | position: relative; 80 | } 81 | 82 | #status-icon { 83 | font-size: 24px; 84 | line-height: 1; 85 | margin-right: 4px; 86 | color: inherit; 87 | font-family: "remixicon"; 88 | width: 24px; 89 | height: 24px; 90 | vertical-align: middle; 91 | } 92 | 93 | /* Tag Dropdown Styles */ 94 | .timer-status.clickable { 95 | cursor: pointer; 96 | position: relative; 97 | transition: background-color 0.2s ease; 98 | gap: 4px; 99 | } 100 | 101 | 102 | .timer-status.clickable:hover { 103 | background-color: var(--focus-primary-btn); 104 | } 105 | 106 | .tag-dropdown-arrow { 107 | font-size: 18px; 108 | transition: transform 0.2s ease, opacity 0.2s ease; 109 | } 110 | 111 | .timer-status.active .tag-dropdown-arrow { 112 | transform: rotate(180deg); 113 | } 114 | 115 | .tag-dropdown-menu { 116 | position: absolute; 117 | top: calc(100% + 8px); 118 | left: 50%; 119 | transform: translateX(-50%); 120 | background: var(--bg-color); 121 | border: 2px solid var(--focus-timer-color); 122 | border-radius: 1rem; 123 | width: 320px; 124 | z-index: 1000; 125 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); 126 | display: none; 127 | } 128 | 129 | .tag-dropdown-menu.active { 130 | display: block; 131 | animation: slideDown 0.2s ease; 132 | background-color: var(--focus-bg); 133 | } 134 | 135 | @keyframes slideDown { 136 | from { 137 | opacity: 0; 138 | transform: translateX(-50%) translateY(-8px); 139 | } 140 | 141 | to { 142 | opacity: 1; 143 | transform: translateX(-50%) translateY(0); 144 | } 145 | } 146 | 147 | .tag-dropdown-header { 148 | padding: 1rem; 149 | border-bottom: 1px solid var(--sidebar-item-border); 150 | font-size: 14px; 151 | font-weight: 600; 152 | color: var(--text-color); 153 | } 154 | 155 | .tag-list { 156 | /* Natural height, no scroll */ 157 | } 158 | 159 | .tag-item { 160 | display: flex; 161 | align-items: center; 162 | padding: 0.75rem 1rem; 163 | cursor: pointer; 164 | transition: background-color 0.2s ease; 165 | gap: 8px; 166 | border-bottom: 1px solid var(--sidebar-item-border); 167 | } 168 | 169 | .tag-item:hover { 170 | background-color: var(--sidebar-item-hover); 171 | } 172 | 173 | .tag-item.selected { 174 | background-color: var(--focus-primary-btn); 175 | color: var(--focus-timer-color); 176 | } 177 | 178 | .tag-item-icon { 179 | font-size: 18px; 180 | width: 20px; 181 | height: 20px; 182 | display: flex; 183 | align-items: center; 184 | justify-content: center; 185 | } 186 | 187 | .tag-item-name { 188 | flex: 1; 189 | font-size: 14px; 190 | font-weight: 500; 191 | } 192 | 193 | .tag-item-delete { 194 | font-size: 16px; 195 | opacity: 0; 196 | transition: opacity 0.2s ease; 197 | cursor: pointer; 198 | padding: 2px; 199 | border-radius: 4px; 200 | } 201 | 202 | .tag-item:hover .tag-item-delete { 203 | opacity: 0.7; 204 | } 205 | 206 | .tag-item-delete:hover { 207 | opacity: 1; 208 | background-color: rgba(255, 77, 77, 0.1); 209 | color: #ff4d4d; 210 | } 211 | 212 | .tag-dropdown-footer { 213 | border-top: 2px solid var(--focus-timer-color); 214 | padding: 1rem; 215 | background: var(--sidebar-bg); 216 | border-bottom-left-radius: 1rem; 217 | border-bottom-right-radius: 1rem; 218 | } 219 | 220 | .new-tag-input { 221 | padding: 0; 222 | } 223 | 224 | .tag-input-row { 225 | display: flex; 226 | align-items: center; 227 | gap: 0.5rem; 228 | } 229 | 230 | .new-tag-input input { 231 | flex: 1; 232 | padding: 0.5rem; 233 | border: 1px solid var(--sidebar-item-border); 234 | border-radius: 0.5rem; 235 | background: var(--sidebar-bg); 236 | color: var(--text-color); 237 | font-size: 14px; 238 | } 239 | 240 | .new-tag-input input:focus { 241 | outline: none; 242 | border-color: var(--focus-timer-color); 243 | } 244 | 245 | /* Icon Selector Container */ 246 | .icon-selector-container { 247 | position: relative; 248 | } 249 | 250 | .selected-icon-btn { 251 | display: flex; 252 | align-items: center; 253 | gap: 4px; 254 | padding: 0.5rem; 255 | background: var(--sidebar-bg); 256 | border: 1px solid var(--sidebar-item-border); 257 | border-radius: 0.5rem; 258 | cursor: pointer; 259 | transition: all 0.2s ease; 260 | min-width: 48px; 261 | justify-content: center; 262 | } 263 | 264 | .selected-icon-btn:hover { 265 | border-color: var(--focus-timer-color); 266 | background-color: var(--focus-secondary-btn); 267 | } 268 | 269 | .selected-icon-btn .dropdown-arrow { 270 | font-size: 14px; 271 | opacity: 0.7; 272 | transition: transform 0.2s ease; 273 | } 274 | 275 | .selected-icon-btn.active .dropdown-arrow { 276 | transform: rotate(180deg); 277 | } 278 | 279 | .icon-selector-dropdown { 280 | position: absolute; 281 | top: calc(100% + 4px); 282 | left: 0; 283 | background: var(--bg-color); 284 | border: 2px solid var(--focus-timer-color); 285 | border-radius: 0.5rem; 286 | padding: 0.5rem; 287 | display: none; 288 | z-index: 1001; 289 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); 290 | min-width: 200px; 291 | } 292 | 293 | .icon-selector-dropdown.active { 294 | display: block; 295 | animation: fadeIn 0.2s ease; 296 | background-color: var(--focus-bg); 297 | } 298 | 299 | @keyframes fadeIn { 300 | from { 301 | opacity: 0; 302 | transform: translateY(-4px); 303 | } 304 | 305 | to { 306 | opacity: 1; 307 | transform: translateY(0); 308 | } 309 | } 310 | 311 | .icon-selector-dropdown { 312 | display: none; 313 | grid-template-columns: repeat(4, 1fr); 314 | gap: 0.5rem; 315 | } 316 | 317 | .icon-selector-dropdown.active { 318 | display: grid; 319 | } 320 | 321 | .icon-option, 322 | .emoji-option { 323 | width: 32px; 324 | height: 32px; 325 | display: flex; 326 | align-items: center; 327 | justify-content: center; 328 | border: 1px solid var(--sidebar-item-border); 329 | border-radius: 0.5rem; 330 | cursor: pointer; 331 | transition: all 0.2s ease; 332 | font-size: 16px; 333 | } 334 | 335 | .icon-option:hover, 336 | .emoji-option:hover { 337 | border-color: var(--focus-timer-color); 338 | background-color: var(--focus-secondary-btn); 339 | } 340 | 341 | .icon-option.selected, 342 | .emoji-option.selected { 343 | border-color: var(--focus-timer-color); 344 | background-color: var(--focus-primary-btn); 345 | color: var(--focus-timer-color); 346 | } 347 | 348 | .create-tag-btn { 349 | padding: 0.5rem 1rem; 350 | background: var(--focus-primary-btn); 351 | color: var(--focus-timer-color); 352 | border: none; 353 | border-radius: 0.5rem; 354 | cursor: pointer; 355 | font-size: 14px; 356 | font-weight: 600; 357 | transition: background-color 0.2s ease; 358 | } 359 | 360 | .create-tag-btn:hover { 361 | background: var(--focus-timer-color); 362 | color: var(--bg-color); 363 | } 364 | 365 | .create-tag-btn:disabled { 366 | opacity: 0.5; 367 | cursor: not-allowed; 368 | } 369 | 370 | .timer-minutes { 371 | font-size: 10rem; 372 | font-weight: 800; 373 | color: var(--focus-timer-color); 374 | font-family: 'Roboto Flex', sans-serif; 375 | line-height: 0.4; 376 | margin-bottom: 0.5rem; 377 | margin-top: 2.5rem; 378 | } 379 | 380 | .timer-seconds { 381 | font-size: 10rem; 382 | font-weight: 800; 383 | color: var(--text-light); 384 | font-family: 'Roboto Flex', sans-serif; 385 | line-height: 1; 386 | } 387 | 388 | /* Timer State Styles - Base Class */ 389 | .timer-container.focus .timer-status, 390 | .timer-container.focus .timer-minutes, 391 | .timer-container.focus .timer-seconds { 392 | color: var(--focus-timer-color); 393 | } 394 | 395 | .timer-container.break .timer-status, 396 | .timer-container.break .timer-minutes, 397 | .timer-container.break .timer-seconds { 398 | color: var(--break-timer-color); 399 | } 400 | 401 | .timer-container.longBreak .timer-status, 402 | .timer-container.longBreak .timer-minutes, 403 | .timer-container.longBreak .timer-seconds { 404 | color: var(--long-break-timer-color); 405 | } 406 | 407 | /* Timer status background styles */ 408 | .timer-container.focus .timer-status { 409 | background-color: var(--focus-secondary-btn); 410 | border: 2px solid var(--focus-timer-color); 411 | } 412 | 413 | .timer-container.break .timer-status { 414 | background-color: var(--break-secondary-btn); 415 | border: 2px solid var(--break-timer-color); 416 | } 417 | 418 | .timer-container.longBreak .timer-status { 419 | background-color: var(--long-break-secondary-btn); 420 | border: 2px solid var(--long-break-timer-color); 421 | } 422 | 423 | /* Timer status for container states (when timer-status is outside timer-container) */ 424 | .container.focus #timer-view .timer-status { 425 | background-color: var(--focus-secondary-btn); 426 | border: 2px solid var(--focus-timer-color); 427 | color: var(--focus-timer-color); 428 | } 429 | 430 | .container.break #timer-view .timer-status { 431 | background-color: var(--break-secondary-btn); 432 | border: 2px solid var(--break-timer-color); 433 | color: var(--break-timer-color); 434 | } 435 | 436 | .container.longBreak #timer-view .timer-status { 437 | background-color: var(--long-break-secondary-btn); 438 | border: 2px solid var(--long-break-timer-color); 439 | color: var(--long-break-timer-color); 440 | } 441 | 442 | /* Timer adjust buttons for different timer modes */ 443 | .container.focus .timer-adjust-btn { 444 | color: var(--focus-timer-color); 445 | } 446 | 447 | .container.break .timer-adjust-btn { 448 | color: var(--break-timer-color); 449 | } 450 | 451 | 452 | .container.longBreak .timer-adjust-btn { 453 | color: var(--long-break-timer-color); 454 | } 455 | 456 | /* Overtime styles for continuous sessions */ 457 | .container.overtime .timer-minutes, 458 | .container.overtime .timer-seconds { 459 | color: var(--warning-color); 460 | text-shadow: 0 0 10px rgba(230, 126, 34, 0.3); 461 | animation: overtime-pulse 2s infinite ease-in-out; 462 | } 463 | 464 | .container.overtime .timer-status { 465 | color: var(--warning-color); 466 | } 467 | 468 | .container.overtime #status-icon { 469 | color: var(--warning-color); 470 | } 471 | 472 | .sidebar.overtime { 473 | background: linear-gradient(135deg, var(--warning-color), #d35400); 474 | } 475 | 476 | @keyframes overtime-pulse { 477 | 478 | 0%, 479 | 100% { 480 | opacity: 0.8; 481 | transform: scale(1); 482 | } 483 | 484 | 50% { 485 | opacity: 1; 486 | transform: scale(1.02); 487 | } 488 | } -------------------------------------------------------------------------------- /src/styles/variables.css: -------------------------------------------------------------------------------- 1 | /* CSS Variables and Base Styles */ 2 | 3 | /* Default Light Theme Variables (Base Application) */ 4 | :root { 5 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 6 | font-size: 16px; 7 | line-height: 24px; 8 | font-weight: 400; 9 | 10 | color: #0f0f0f; 11 | background-color: #f6f6f6; 12 | 13 | font-synthesis: none; 14 | text-rendering: optimizeLegibility; 15 | -webkit-font-smoothing: antialiased; 16 | -moz-osx-font-smoothing: grayscale; 17 | -webkit-text-size-adjust: 100%; 18 | 19 | /* Base Application Colors (non-timer specific) */ 20 | --accent-color: #2c3e50; 21 | --text-light: #7f8c8d; 22 | --warning-color: #e67e22; 23 | 24 | /* Smart indicator colors */ 25 | --smart-indicator-active: #3b82f6; 26 | --smart-indicator-inactive: #94a3b8; 27 | --smart-indicator-bg: rgba(255, 255, 255, 0.1); 28 | --smart-indicator-bg-hover: rgba(255, 255, 255, 0.2); 29 | } 30 | 31 | /* Dark Theme Variables */ 32 | [data-theme="dark"] { 33 | color: #e2e8f0; 34 | background-color: #1b1b1b; 35 | 36 | /* Base Application Colors - dark versions */ 37 | --accent-color: #f8fafc; 38 | --text-light: #94a3b8; 39 | --warning-color: #f59e0b; 40 | 41 | /* Smart indicator colors for dark theme */ 42 | --smart-indicator-active: #60a5fa; 43 | --smart-indicator-inactive: #64748b; 44 | --smart-indicator-bg: rgba(0, 0, 0, 0.2); 45 | --smart-indicator-bg-hover: rgba(0, 0, 0, 0.3); 46 | } 47 | 48 | /* Auto Theme - follows system preference */ 49 | @media (prefers-color-scheme: dark) { 50 | [data-theme="auto"] { 51 | color: #e2e8f0; 52 | background-color: #0f172a; 53 | 54 | /* Base Application Colors - dark versions for auto mode */ 55 | --accent-color: #f8fafc; 56 | --text-light: #94a3b8; 57 | --warning-color: #f59e0b; 58 | 59 | /* Smart indicator colors for auto dark theme */ 60 | --smart-indicator-active: #60a5fa; 61 | --smart-indicator-inactive: #64748b; 62 | --smart-indicator-bg: rgba(0, 0, 0, 0.2); 63 | --smart-indicator-bg-hover: rgba(0, 0, 0, 0.3); 64 | } 65 | } 66 | 67 | /* Dark Theme Styles for forced dark theme */ 68 | [data-theme="dark"] { 69 | 70 | /* Card backgrounds in dark theme */ 71 | .base-card, 72 | .base-card-compact, 73 | .base-section { 74 | background: #1e293b; 75 | border-color: #334155; 76 | } 77 | 78 | /* Modal backgrounds */ 79 | .modal-overlay { 80 | background: rgba(0, 0, 0, 0.8); 81 | } 82 | 83 | .modal-content { 84 | background: #1e293b; 85 | border-color: #334155; 86 | } 87 | 88 | /* Input styles in dark theme */ 89 | input, 90 | select, 91 | textarea { 92 | background: #334155; 93 | border-color: #475569; 94 | color: #e2e8f0; 95 | } 96 | 97 | input:focus, 98 | select:focus, 99 | textarea:focus { 100 | border-color: #60a5fa; 101 | box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.1); 102 | } 103 | 104 | /* Button styles in dark theme */ 105 | .btn-secondary { 106 | background: #374151; 107 | border-color: #4b5563; 108 | color: #e2e8f0; 109 | } 110 | 111 | .btn-secondary:hover { 112 | background: #4b5563; 113 | border-color: #6b7280; 114 | } 115 | 116 | /* Text colors in dark theme */ 117 | .text-muted { 118 | color: #94a3b8; 119 | } 120 | 121 | .text-primary { 122 | color: #60a5fa; 123 | } 124 | 125 | /* Border colors in dark theme */ 126 | hr { 127 | border-color: #334155; 128 | } 129 | 130 | .border { 131 | border-color: #334155; 132 | } 133 | } 134 | 135 | /* Auto theme dark mode styles */ 136 | @media (prefers-color-scheme: dark) { 137 | [data-theme="auto"] { 138 | 139 | /* Card backgrounds in auto dark theme */ 140 | .base-card, 141 | .base-card-compact, 142 | .base-section { 143 | background: #1e293b; 144 | border-color: #334155; 145 | } 146 | 147 | /* Modal backgrounds */ 148 | .modal-overlay { 149 | background: rgba(0, 0, 0, 0.8); 150 | } 151 | 152 | .modal-content { 153 | background: #1e293b; 154 | border-color: #334155; 155 | } 156 | 157 | /* Input styles in auto dark theme */ 158 | input, 159 | select, 160 | textarea { 161 | background: #334155; 162 | border-color: #475569; 163 | color: #e2e8f0; 164 | } 165 | 166 | input:focus, 167 | select:focus, 168 | textarea:focus { 169 | border-color: #60a5fa; 170 | box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.1); 171 | } 172 | 173 | /* Button styles in auto dark theme */ 174 | .btn-secondary { 175 | background: #374151; 176 | border-color: #4b5563; 177 | color: #e2e8f0; 178 | } 179 | 180 | .btn-secondary:hover { 181 | background: #4b5563; 182 | border-color: #6b7280; 183 | } 184 | 185 | /* Text colors in auto dark theme */ 186 | .text-muted { 187 | color: #94a3b8; 188 | } 189 | 190 | .text-primary { 191 | color: #60a5fa; 192 | } 193 | 194 | /* Border colors in auto dark theme */ 195 | hr { 196 | border-color: #334155; 197 | } 198 | 199 | .border { 200 | border-color: #334155; 201 | } 202 | } 203 | } 204 | 205 | /* Base utility classes */ 206 | .text-center { 207 | text-align: center; 208 | } 209 | 210 | .text-left { 211 | text-align: left; 212 | } 213 | 214 | .text-right { 215 | text-align: right; 216 | } 217 | 218 | .hidden { 219 | display: none !important; 220 | } 221 | 222 | .visible { 223 | display: block !important; 224 | } 225 | 226 | .flex { 227 | display: flex; 228 | } 229 | 230 | .flex-column { 231 | flex-direction: column; 232 | } 233 | 234 | .flex-row { 235 | flex-direction: row; 236 | } 237 | 238 | .justify-center { 239 | justify-content: center; 240 | } 241 | 242 | .align-center { 243 | align-items: center; 244 | } 245 | 246 | .mt-1 { 247 | margin-top: 0.25rem; 248 | } 249 | 250 | .mt-2 { 251 | margin-top: 0.5rem; 252 | } 253 | 254 | .mt-3 { 255 | margin-top: 0.75rem; 256 | } 257 | 258 | .mt-4 { 259 | margin-top: 1rem; 260 | } 261 | 262 | .mb-1 { 263 | margin-bottom: 0.25rem; 264 | } 265 | 266 | .mb-2 { 267 | margin-bottom: 0.5rem; 268 | } 269 | 270 | .mb-3 { 271 | margin-bottom: 0.75rem; 272 | } 273 | 274 | .mb-4 { 275 | margin-bottom: 1rem; 276 | } 277 | 278 | .ml-1 { 279 | margin-left: 0.25rem; 280 | } 281 | 282 | .ml-2 { 283 | margin-left: 0.5rem; 284 | } 285 | 286 | .ml-3 { 287 | margin-left: 0.75rem; 288 | } 289 | 290 | .ml-4 { 291 | margin-left: 1rem; 292 | } 293 | 294 | .mr-1 { 295 | margin-right: 0.25rem; 296 | } 297 | 298 | .mr-2 { 299 | margin-right: 0.5rem; 300 | } 301 | 302 | .mr-3 { 303 | margin-right: 0.75rem; 304 | } 305 | 306 | .mr-4 { 307 | margin-right: 1rem; 308 | } 309 | 310 | .p-1 { 311 | padding: 0.25rem; 312 | } 313 | 314 | .p-2 { 315 | padding: 0.5rem; 316 | } 317 | 318 | .p-3 { 319 | padding: 0.75rem; 320 | } 321 | 322 | .p-4 { 323 | padding: 1rem; 324 | } 325 | 326 | .w-full { 327 | width: 100%; 328 | } 329 | 330 | .h-full { 331 | height: 100%; 332 | } 333 | 334 | /* Common animations */ 335 | .fade-in { 336 | animation: fadeIn 0.3s ease-in-out; 337 | } 338 | 339 | .fade-out { 340 | animation: fadeOut 0.3s ease-in-out; 341 | } 342 | 343 | @keyframes fadeIn { 344 | from { 345 | opacity: 0; 346 | } 347 | 348 | to { 349 | opacity: 1; 350 | } 351 | } 352 | 353 | @keyframes fadeOut { 354 | from { 355 | opacity: 1; 356 | } 357 | 358 | to { 359 | opacity: 0; 360 | } 361 | } 362 | 363 | .slide-in-up { 364 | animation: slideInUp 0.3s ease-out; 365 | } 366 | 367 | @keyframes slideInUp { 368 | from { 369 | transform: translateY(100%); 370 | opacity: 0; 371 | } 372 | 373 | to { 374 | transform: translateY(0); 375 | opacity: 1; 376 | } 377 | } 378 | 379 | .bounce-in { 380 | animation: bounceIn 0.5s ease-out; 381 | } 382 | 383 | @keyframes bounceIn { 384 | 0% { 385 | transform: scale(0.3); 386 | opacity: 0; 387 | } 388 | 389 | 50% { 390 | transform: scale(1.05); 391 | } 392 | 393 | 70% { 394 | transform: scale(0.9); 395 | } 396 | 397 | 100% { 398 | transform: scale(1); 399 | opacity: 1; 400 | } 401 | } -------------------------------------------------------------------------------- /src/utils/analytics.js: -------------------------------------------------------------------------------- 1 | // Analytics utility for tracking events with Aptabase 2 | import { trackEvent } from "@aptabase/tauri"; 3 | 4 | /** 5 | * Analytics utility class for tracking user events 6 | */ 7 | class Analytics { 8 | /** 9 | * Check if analytics are enabled in user settings 10 | * @returns {Promise} Whether analytics are enabled 11 | */ 12 | static async isEnabled() { 13 | try { 14 | // Import settings manager to check analytics preference 15 | // Note: This is a dynamic import to avoid circular dependencies 16 | const settingsManager = window.settingsManager; 17 | if (settingsManager && settingsManager.settings) { 18 | return settingsManager.settings.analytics_enabled !== false; 19 | } 20 | // Default to enabled if we can't check settings 21 | return true; 22 | } catch (error) { 23 | console.warn('Could not check analytics settings, defaulting to enabled:', error); 24 | return true; 25 | } 26 | } 27 | 28 | /** 29 | * Track an event only if analytics are enabled 30 | * @param {string} eventName - The event name 31 | * @param {object} properties - Event properties 32 | */ 33 | static async track(eventName, properties = {}) { 34 | if (await this.isEnabled()) { 35 | try { 36 | trackEvent(eventName, properties); 37 | } catch (error) { 38 | console.warn('Failed to track analytics event:', error); 39 | } 40 | } 41 | } 42 | 43 | /** 44 | * Track timer-related events 45 | */ 46 | static timer = { 47 | /** 48 | * Track when a timer is started 49 | * @param {string} mode - The timer mode (focus, break, longBreak) 50 | * @param {number} duration - Duration in minutes 51 | */ 52 | async started(mode, duration) { 53 | await Analytics.track("timer_started", { 54 | mode, 55 | duration_minutes: duration 56 | }); 57 | }, 58 | 59 | /** 60 | * Track when a timer is paused 61 | * @param {string} mode - The timer mode 62 | * @param {number} remainingTime - Remaining time in seconds 63 | */ 64 | async paused(mode, remainingTime) { 65 | await Analytics.track("timer_paused", { 66 | mode, 67 | remaining_seconds: remainingTime 68 | }); 69 | }, 70 | 71 | /** 72 | * Track when a timer is completed 73 | * @param {string} mode - The timer mode 74 | * @param {number} duration - Duration in minutes 75 | */ 76 | async completed(mode, duration) { 77 | await Analytics.track("timer_completed", { 78 | mode, 79 | duration_minutes: duration 80 | }); 81 | }, 82 | 83 | /** 84 | * Track when a timer is skipped 85 | * @param {string} mode - The timer mode 86 | * @param {number} remainingTime - Remaining time in seconds 87 | */ 88 | async skipped(mode, remainingTime) { 89 | await Analytics.track("timer_skipped", { 90 | mode, 91 | remaining_seconds: remainingTime 92 | }); 93 | }, 94 | 95 | /** 96 | * Track when a timer is reset 97 | * @param {string} mode - The timer mode 98 | */ 99 | async reset(mode) { 100 | await Analytics.track("timer_reset", { mode }); 101 | } 102 | }; 103 | 104 | /** 105 | * Track task-related events 106 | */ 107 | static tasks = { 108 | /** 109 | * Track when a task is created 110 | */ 111 | async created() { 112 | await Analytics.track("task_created"); 113 | }, 114 | 115 | /** 116 | * Track when a task is completed 117 | */ 118 | async completed() { 119 | await Analytics.track("task_completed"); 120 | }, 121 | 122 | /** 123 | * Track when a task is deleted 124 | */ 125 | async deleted() { 126 | await Analytics.track("task_deleted"); 127 | }, 128 | 129 | /** 130 | * Track when tasks are bulk imported/exported 131 | * @param {number} count - Number of tasks 132 | * @param {string} action - 'import' or 'export' 133 | */ 134 | async bulkAction(count, action) { 135 | await Analytics.track("tasks_bulk_action", { 136 | count, 137 | action 138 | }); 139 | } 140 | }; 141 | 142 | /** 143 | * Track feature usage 144 | */ 145 | static features = { 146 | /** 147 | * Track when a specific feature is used 148 | * @param {string} feature - Feature name 149 | * @param {Object} properties - Additional properties 150 | */ 151 | async used(feature, properties = {}) { 152 | await Analytics.track("feature_used", { 153 | feature, 154 | ...properties 155 | }); 156 | }, 157 | 158 | /** 159 | * Track shortcuts usage 160 | * @param {string} shortcut - Shortcut name (start-stop, reset, skip) 161 | */ 162 | async shortcutUsed(shortcut) { 163 | await Analytics.track("shortcut_used", { shortcut }); 164 | }, 165 | 166 | /** 167 | * Track when smart pause is triggered 168 | * @param {number} inactiveTime - Time inactive in seconds 169 | */ 170 | async smartPauseTriggered(inactiveTime) { 171 | await Analytics.track("smart_pause_triggered", { 172 | inactive_seconds: inactiveTime 173 | }); 174 | }, 175 | 176 | /** 177 | * Track view changes 178 | * @param {string} view - View name (timer, tasks, statistics, settings) 179 | */ 180 | async viewChanged(view) { 181 | await Analytics.track("view_changed", { view }); 182 | } 183 | }; 184 | 185 | /** 186 | * Track session-related events 187 | */ 188 | static sessions = { 189 | /** 190 | * Track session completion 191 | * @param {number} completedPomodoros - Number of completed pomodoros 192 | * @param {number} totalFocusTime - Total focus time in minutes 193 | */ 194 | async completed(completedPomodoros, totalFocusTime) { 195 | await Analytics.track("session_completed", { 196 | completed_pomodoros: completedPomodoros, 197 | total_focus_minutes: Math.round(totalFocusTime / 60) 198 | }); 199 | }, 200 | 201 | /** 202 | * Track daily goal achievement 203 | * @param {number} goalMinutes - Goal in minutes 204 | * @param {number} achievedMinutes - Achieved minutes 205 | */ 206 | async goalProgress(goalMinutes, achievedMinutes) { 207 | const percentage = Math.round((achievedMinutes / goalMinutes) * 100); 208 | await Analytics.track("daily_goal_progress", { 209 | goal_minutes: goalMinutes, 210 | achieved_minutes: achievedMinutes, 211 | percentage 212 | }); 213 | } 214 | }; 215 | 216 | /** 217 | * Track errors and issues 218 | */ 219 | /** 220 | * Track errors and issues 221 | */ 222 | static errors = { 223 | /** 224 | * Track when an error occurs 225 | * @param {string} error - Error message or type 226 | * @param {string} context - Where the error occurred 227 | */ 228 | async occurred(error, context) { 229 | await Analytics.track("error_occurred", { 230 | error: String(error), 231 | context 232 | }); 233 | } 234 | }; 235 | } 236 | 237 | export default Analytics; 238 | -------------------------------------------------------------------------------- /src/utils/tag-statistics.js: -------------------------------------------------------------------------------- 1 | // Tag Statistics Utility for generating pie chart data and visualization 2 | export class TagStatistics { 3 | constructor() { 4 | this.tagColors = [ 5 | '#3b82f6', // Blue 6 | '#10b981', // Green 7 | '#f59e0b', // Yellow 8 | '#ef4444', // Red 9 | '#8b5cf6', // Purple 10 | '#06b6d4', // Cyan 11 | '#f97316', // Orange 12 | '#84cc16', // Lime 13 | '#ec4899', // Pink 14 | '#6b7280', // Gray 15 | '#14b8a6', // Teal 16 | '#a855f7', // Violet 17 | '#eab308', // Amber 18 | '#22c55e', // Emerald 19 | '#3b82f6', // Blue (repeat) 20 | ]; 21 | } 22 | 23 | /** 24 | * Get tag usage statistics for a specific time period 25 | * @param {Array} sessions - Array of session objects 26 | * @param {Array} tags - Array of available tags 27 | * @param {Date} startDate - Start date for filtering 28 | * @param {Date} endDate - End date for filtering 29 | * @returns {Object} Tag statistics with percentages and durations 30 | */ 31 | getTagUsageStatistics(sessions, tags, startDate, endDate) { 32 | // Filter sessions within the date range 33 | const filteredSessions = sessions.filter(session => { 34 | const sessionDate = new Date(session.date || session.created_at); 35 | return sessionDate >= startDate && sessionDate <= endDate; 36 | }); 37 | 38 | // Calculate tag usage from session data 39 | const tagUsage = new Map(); 40 | let totalDuration = 0; 41 | 42 | // Process sessions to calculate tag durations 43 | filteredSessions.forEach(session => { 44 | // All sessions are focus sessions now 45 | if (session.duration > 0) { 46 | // Handle various formats for tags field (backward compatibility) 47 | let sessionTags = []; 48 | if (Array.isArray(session.tags)) { 49 | sessionTags = session.tags; 50 | } else if (session.tags) { 51 | // In case tags is not an array but exists 52 | sessionTags = [session.tags]; 53 | } 54 | // sessionTags will be empty array if session.tags is null/undefined 55 | 56 | if (sessionTags.length === 0) { 57 | // Session without tags - assign to "Untagged" 58 | const untaggedKey = 'untagged'; 59 | const current = tagUsage.get(untaggedKey) || { duration: 0, sessions: 0 }; 60 | tagUsage.set(untaggedKey, { 61 | duration: current.duration + session.duration, 62 | sessions: current.sessions + 1, 63 | tag: { id: 'untagged', name: 'Untagged', icon: 'ri-price-tag-line', color: '#6b7280' } 64 | }); 65 | totalDuration += session.duration; 66 | } else { 67 | // Distribute session duration equally among all tags 68 | const durationPerTag = session.duration / sessionTags.length; 69 | sessionTags.forEach(sessionTag => { 70 | // Handle both tag objects and tag IDs 71 | const tagId = typeof sessionTag === 'string' ? sessionTag : sessionTag.id; 72 | const tag = typeof sessionTag === 'object' ? sessionTag : tags.find(t => t.id === tagId); 73 | 74 | if (tag) { 75 | const current = tagUsage.get(tagId) || { duration: 0, sessions: 0 }; 76 | tagUsage.set(tagId, { 77 | duration: current.duration + durationPerTag, 78 | sessions: current.sessions + 1, 79 | tag: tag 80 | }); 81 | } 82 | }); 83 | totalDuration += session.duration; 84 | } 85 | } 86 | }); 87 | 88 | // Convert to array and calculate percentages 89 | const tagStats = Array.from(tagUsage.entries()).map(([tagId, data], index) => ({ 90 | tagId, 91 | tag: data.tag, 92 | duration: Math.round(data.duration * 60), // Convert to seconds 93 | sessions: data.sessions, 94 | percentage: totalDuration > 0 ? (data.duration / totalDuration) * 100 : 0, 95 | color: this.tagColors[index % this.tagColors.length] 96 | })); 97 | 98 | // Sort by duration (highest first) 99 | tagStats.sort((a, b) => b.duration - a.duration); 100 | 101 | return { 102 | stats: tagStats, 103 | totalDuration: Math.round(totalDuration * 60), // Convert to seconds 104 | totalSessions: filteredSessions.length // All sessions are focus sessions now 105 | }; 106 | } 107 | 108 | /** 109 | * Get current week tag statistics 110 | * @param {Array} sessions - Array of session objects 111 | * @param {Array} tags - Array of available tags 112 | * @returns {Object} Tag statistics for current week 113 | */ 114 | getCurrentWeekTagStats(sessions, tags) { 115 | const now = new Date(); 116 | const startOfWeek = new Date(now); 117 | startOfWeek.setDate(now.getDate() - now.getDay()); // Start of week (Sunday) 118 | startOfWeek.setHours(0, 0, 0, 0); 119 | 120 | const endOfWeek = new Date(startOfWeek); 121 | endOfWeek.setDate(startOfWeek.getDate() + 6); // End of week (Saturday) 122 | endOfWeek.setHours(23, 59, 59, 999); 123 | 124 | return this.getTagUsageStatistics(sessions, tags, startOfWeek, endOfWeek); 125 | } 126 | 127 | /** 128 | * Format duration in seconds to human readable format 129 | * @param {number} seconds - Duration in seconds 130 | * @returns {string} Formatted duration (e.g., "2h 30m", "45m", "30s") 131 | */ 132 | formatDuration(seconds) { 133 | if (seconds < 60) { 134 | return `${seconds}s`; 135 | } 136 | 137 | const minutes = Math.floor(seconds / 60); 138 | if (minutes < 60) { 139 | return `${minutes}m`; 140 | } 141 | 142 | const hours = Math.floor(minutes / 60); 143 | const remainingMinutes = minutes % 60; 144 | 145 | if (remainingMinutes === 0) { 146 | return `${hours}h`; 147 | } 148 | return `${hours}h ${remainingMinutes}m`; 149 | } 150 | 151 | /** 152 | * Generate CSS conic-gradient for pie chart 153 | * @param {Array} tagStats - Array of tag statistics 154 | * @returns {string} CSS conic-gradient string 155 | */ 156 | generatePieChartGradient(tagStats) { 157 | if (!tagStats || tagStats.length === 0) { 158 | return 'conic-gradient(#e5e7eb 0deg 360deg)'; 159 | } 160 | 161 | let currentAngle = 0; 162 | const gradientStops = []; 163 | 164 | tagStats.forEach((stat, index) => { 165 | const angle = (stat.percentage / 100) * 360; 166 | const nextAngle = currentAngle + angle; 167 | 168 | gradientStops.push(`${stat.color} ${currentAngle}deg ${nextAngle}deg`); 169 | 170 | currentAngle = nextAngle; 171 | }); 172 | 173 | return `conic-gradient(${gradientStops.join(', ')})`; 174 | } 175 | 176 | /** 177 | * Render pie chart and legend 178 | * @param {string} chartContainerId - ID of the chart container element 179 | * @param {string} legendContainerId - ID of the legend container element 180 | * @param {Object} tagStatsData - Tag statistics data 181 | */ 182 | renderTagPieChart(chartContainerId, legendContainerId, tagStatsData) { 183 | const chartContainer = document.getElementById(chartContainerId); 184 | const legendContainer = document.getElementById(legendContainerId); 185 | 186 | if (!chartContainer || !legendContainer) { 187 | console.error('Tag pie chart containers not found'); 188 | return; 189 | } 190 | 191 | const { stats, totalDuration } = tagStatsData; 192 | 193 | // Clear containers 194 | chartContainer.innerHTML = ''; 195 | legendContainer.innerHTML = ''; 196 | 197 | if (!stats || stats.length === 0 || totalDuration === 0) { 198 | // Show placeholder 199 | chartContainer.innerHTML = ` 200 |
201 | 202 | No data available 203 |
204 | `; 205 | return; 206 | } 207 | 208 | // Create pie chart with CSS conic-gradient 209 | const gradient = this.generatePieChartGradient(stats); 210 | chartContainer.style.background = gradient; 211 | chartContainer.style.border = '1px solid #e5e7eb'; 212 | 213 | // Show only top 5 tags in legend for better space utilization 214 | const topStats = stats.slice(0, 5); 215 | const remainingStats = stats.slice(5); 216 | 217 | // Create legend for top tags 218 | topStats.forEach(stat => { 219 | const legendItem = document.createElement('div'); 220 | legendItem.className = 'tag-legend-item'; 221 | 222 | const iconHtml = stat.tag.icon.startsWith('ri-') 223 | ? `` 224 | : stat.tag.icon; 225 | 226 | legendItem.innerHTML = ` 227 |
228 |
229 |
230 | ${iconHtml} ${stat.tag.name} 231 |
232 |
233 | ${this.formatDuration(stat.duration)} 234 | ${stat.percentage.toFixed(1)}% 235 |
236 |
237 | `; 238 | 239 | legendContainer.appendChild(legendItem); 240 | }); 241 | 242 | // If there are more tags, show a summary 243 | if (remainingStats.length > 0) { 244 | const remainingDuration = remainingStats.reduce((sum, stat) => sum + stat.duration, 0); 245 | const remainingPercentage = remainingStats.reduce((sum, stat) => sum + stat.percentage, 0); 246 | 247 | const othersItem = document.createElement('div'); 248 | othersItem.className = 'tag-legend-item tag-legend-others'; 249 | 250 | othersItem.innerHTML = ` 251 |
252 |
253 |
254 | ${remainingStats.length} others 255 |
256 |
257 | ${this.formatDuration(remainingDuration)} 258 | ${remainingPercentage.toFixed(1)}% 259 |
260 |
261 | `; 262 | 263 | legendContainer.appendChild(othersItem); 264 | } 265 | } 266 | } -------------------------------------------------------------------------------- /src/utils/theme-loader.js: -------------------------------------------------------------------------------- 1 | // Auto Theme Loader 2 | // This utility automatically discovers and loads all theme CSS files from the themes folder 3 | 4 | class ThemeLoader { 5 | constructor() { 6 | this.loadedThemes = new Set(); 7 | this.themeStyles = new Map(); 8 | } 9 | 10 | /** 11 | * Automatically discover and load all theme CSS files 12 | */ 13 | async loadAllThemes() { 14 | try { 15 | // Get all CSS files from the themes directory 16 | const themeFiles = await this.discoverThemeFiles(); 17 | 18 | console.log(`🎨 Discovered ${themeFiles.length} theme files:`, themeFiles); 19 | 20 | // Load each theme file 21 | for (const themeFile of themeFiles) { 22 | await this.loadThemeFile(themeFile); 23 | } 24 | 25 | console.log(`🎨 Auto-loaded ${this.loadedThemes.size} themes successfully`); 26 | return Array.from(this.loadedThemes); 27 | } catch (error) { 28 | console.error('❌ Failed to auto-load themes:', error); 29 | return []; 30 | } 31 | } 32 | 33 | /** 34 | * Discover all CSS files in the themes directory 35 | */ 36 | async discoverThemeFiles() { 37 | // In a browser environment, we need to use a different approach 38 | // Since we can't directly read the filesystem, we'll use a predefined list 39 | // that gets updated by the build process or manually maintained 40 | 41 | // This could be enhanced to use a build-time script that generates this list 42 | const knownThemes = [ 43 | 'espresso.css', 44 | 'pipboy.css', 45 | 'pommodore64.css' 46 | ]; 47 | 48 | return knownThemes; 49 | } 50 | 51 | /** 52 | * Load a specific theme CSS file 53 | */ 54 | async loadThemeFile(filename) { 55 | const themeId = filename.replace('.css', ''); 56 | 57 | if (this.loadedThemes.has(themeId)) { 58 | console.log(`🎨 Theme ${themeId} already loaded, skipping`); 59 | return; 60 | } 61 | 62 | try { 63 | // Since CSS is already imported statically in main.css, 64 | // we just need to register the theme in our loaded themes 65 | console.log(`✅ Theme registered: ${themeId}`); 66 | this.loadedThemes.add(themeId); 67 | 68 | // Extract theme metadata from CSS file 69 | await this.extractThemeMetadata(themeId); 70 | 71 | } catch (error) { 72 | console.error(`❌ Error registering theme ${themeId}:`, error); 73 | } 74 | } 75 | 76 | /** 77 | * Extract theme metadata from CSS comments 78 | */ 79 | async extractThemeMetadata(themeId) { 80 | try { 81 | // Fetch the CSS file to read metadata from comments 82 | const response = await fetch(`./src/styles/themes/${themeId}.css`); 83 | const cssContent = await response.text(); 84 | 85 | // Parse metadata from CSS comments 86 | const metadata = this.parseThemeMetadata(cssContent); 87 | 88 | if (metadata) { 89 | // Add to TIMER_THEMES dynamically 90 | const { TIMER_THEMES } = await import('./timer-themes.js'); 91 | 92 | if (!TIMER_THEMES[themeId]) { 93 | TIMER_THEMES[themeId] = { 94 | name: metadata.name || this.capitalizeFirst(themeId), 95 | description: metadata.description || `Auto-discovered theme: ${themeId}`, 96 | supports: metadata.supports || ['light', 'dark'], 97 | isDefault: false, 98 | preview: metadata.preview || { 99 | focus: '#e74c3c', 100 | break: '#2ecc71', 101 | longBreak: '#3498db' 102 | } 103 | }; 104 | 105 | console.log(`📝 Auto-registered theme: ${themeId}`, TIMER_THEMES[themeId]); 106 | } 107 | } 108 | } catch (error) { 109 | console.warn(`⚠️ Could not extract metadata for theme ${themeId}:`, error); 110 | } 111 | } 112 | 113 | /** 114 | * Parse theme metadata from CSS comments 115 | */ 116 | parseThemeMetadata(cssContent) { 117 | try { 118 | // Look for theme metadata in CSS comments 119 | const metadataRegex = /\/\*\s*Timer Theme:\s*(.+?)\s*\*\s*Author:\s*(.+?)\s*\*\s*Description:\s*(.+?)\s*\*\s*Supports:\s*(.+?)\s*\*\//s; 120 | const match = cssContent.match(metadataRegex); 121 | 122 | if (match) { 123 | const [, name, author, description, supports] = match; 124 | 125 | // Parse supports field 126 | const supportsModes = supports.toLowerCase().includes('light') && supports.toLowerCase().includes('dark') 127 | ? ['light', 'dark'] 128 | : supports.toLowerCase().includes('dark') 129 | ? ['dark'] 130 | : ['light']; 131 | 132 | // Try to extract color values for preview 133 | const preview = this.extractPreviewColors(cssContent); 134 | 135 | return { 136 | name: name.trim(), 137 | author: author.trim(), 138 | description: description.trim(), 139 | supports: supportsModes, 140 | preview 141 | }; 142 | } 143 | } catch (error) { 144 | console.warn('Could not parse theme metadata:', error); 145 | } 146 | 147 | return null; 148 | } 149 | 150 | /** 151 | * Extract preview colors from CSS variables 152 | */ 153 | extractPreviewColors(cssContent) { 154 | const colors = { 155 | focus: '#e74c3c', 156 | break: '#2ecc71', 157 | longBreak: '#3498db' 158 | }; 159 | 160 | try { 161 | // Extract --focus-color, --break-color, --long-break-color 162 | const focusMatch = cssContent.match(/--focus-color:\s*([^;]+);/); 163 | const breakMatch = cssContent.match(/--break-color:\s*([^;]+);/); 164 | const longBreakMatch = cssContent.match(/--long-break-color:\s*([^;]+);/); 165 | 166 | if (focusMatch) colors.focus = focusMatch[1].trim(); 167 | if (breakMatch) colors.break = breakMatch[1].trim(); 168 | if (longBreakMatch) colors.longBreak = longBreakMatch[1].trim(); 169 | } catch (error) { 170 | console.warn('Could not extract preview colors:', error); 171 | } 172 | 173 | return colors; 174 | } 175 | 176 | /** 177 | * Capitalize first letter of a string 178 | */ 179 | capitalizeFirst(str) { 180 | return str.charAt(0).toUpperCase() + str.slice(1); 181 | } 182 | 183 | /** 184 | * Remove a loaded theme 185 | */ 186 | unloadTheme(themeId) { 187 | const linkElement = this.themeStyles.get(themeId); 188 | if (linkElement) { 189 | document.head.removeChild(linkElement); 190 | this.loadedThemes.delete(themeId); 191 | this.themeStyles.delete(themeId); 192 | console.log(`🗑️ Unloaded theme: ${themeId}`); 193 | } 194 | } 195 | 196 | /** 197 | * Get list of loaded themes 198 | */ 199 | getLoadedThemes() { 200 | return Array.from(this.loadedThemes); 201 | } 202 | 203 | /** 204 | * Check if a theme is loaded 205 | */ 206 | isThemeLoaded(themeId) { 207 | return this.loadedThemes.has(themeId); 208 | } 209 | } 210 | 211 | // Create and export a singleton instance 212 | export const themeLoader = new ThemeLoader(); 213 | 214 | // Auto-load themes when this module is imported 215 | export async function initializeAutoThemeLoader() { 216 | console.log('🎨 Initializing auto theme loader...'); 217 | const loadedThemes = await themeLoader.loadAllThemes(); 218 | return loadedThemes; 219 | } 220 | 221 | export default themeLoader; 222 | -------------------------------------------------------------------------------- /src/utils/timer-themes.js: -------------------------------------------------------------------------------- 1 | // Timer Themes Configuration 2 | // This file defines all available timer themes and their properties 3 | 4 | export const TIMER_THEMES = { 5 | espresso: { 6 | name: 'Espresso', 7 | description: 'Warm, coffee-inspired colors with rich earth tones', 8 | supports: ['light', 'dark'], 9 | isDefault: true, 10 | preview: { 11 | focus: '#e74c3c', 12 | break: '#2ecc71', 13 | longBreak: '#3498db' 14 | } 15 | }, 16 | pommodore64: { 17 | name: 'Pommodore64', 18 | description: 'Un tema retrò ispirato al Commodore 64 con colori nostalgici e font pixelato', 19 | supports: ['light'], 20 | isDefault: false, 21 | preview: { 22 | focus: '#6c5ce7', 23 | break: '#0984e3', 24 | longBreak: '#00b894' 25 | } 26 | }, 27 | pipboy: { 28 | name: 'PipBoy', 29 | description: 'A retro-futuristic theme inspired by Fallout\'s PipBoy interface with green terminal colors and digital effects', 30 | supports: ['dark'], 31 | isDefault: false, 32 | preview: { 33 | focus: '#00ff41', 34 | break: '#39ff14', 35 | longBreak: '#00cc33' 36 | } 37 | }, 38 | }; 39 | 40 | // Function to dynamically register a new theme 41 | export function registerTheme(themeId, themeConfig) { 42 | if (TIMER_THEMES[themeId]) { 43 | console.warn(`🎨 Theme ${themeId} already exists, overriding...`); 44 | } 45 | 46 | TIMER_THEMES[themeId] = { 47 | name: themeConfig.name || themeId, 48 | description: themeConfig.description || `Theme: ${themeId}`, 49 | supports: themeConfig.supports || ['light', 'dark'], 50 | isDefault: themeConfig.isDefault || false, 51 | preview: themeConfig.preview || { 52 | focus: '#e74c3c', 53 | break: '#2ecc71', 54 | longBreak: '#3498db' 55 | } 56 | }; 57 | 58 | console.log(`✅ Registered theme: ${themeId}`, TIMER_THEMES[themeId]); 59 | return TIMER_THEMES[themeId]; 60 | } 61 | 62 | // Function to unregister a theme 63 | export function unregisterTheme(themeId) { 64 | if (TIMER_THEMES[themeId] && !TIMER_THEMES[themeId].isDefault) { 65 | delete TIMER_THEMES[themeId]; 66 | console.log(`🗑️ Unregistered theme: ${themeId}`); 67 | return true; 68 | } 69 | return false; 70 | } 71 | 72 | // Get theme by ID 73 | export function getThemeById(themeId) { 74 | return TIMER_THEMES[themeId] || TIMER_THEMES.espresso; 75 | } 76 | 77 | // Get all available themes 78 | export function getAllThemes() { 79 | return Object.entries(TIMER_THEMES).map(([id, theme]) => ({ 80 | id, 81 | ...theme 82 | })); 83 | } 84 | 85 | // Get compatible themes for current color mode 86 | export function getCompatibleThemes(colorMode = 'light') { 87 | return getAllThemes().filter(theme => 88 | theme.supports.includes(colorMode) 89 | ); 90 | } 91 | 92 | // Check if theme is compatible with color mode 93 | export function isThemeCompatible(themeId, colorMode = 'light') { 94 | const theme = getThemeById(themeId); 95 | return theme.supports.includes(colorMode); 96 | } 97 | 98 | // Get default theme 99 | export function getDefaultTheme() { 100 | const defaultTheme = getAllThemes().find(theme => theme.isDefault); 101 | return defaultTheme || getAllThemes()[0]; 102 | } 103 | -------------------------------------------------------------------------------- /src/version.js: -------------------------------------------------------------------------------- 1 | // Questo file viene generato automaticamente durante il build 2 | // Contiene la versione corrente dell'applicazione 3 | export const APP_VERSION = '0.4.4'; 4 | -------------------------------------------------------------------------------- /verify-updates.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Verification script for Tempo update system configuration 4 | # This script checks if everything is properly set up for automatic updates 5 | 6 | echo "🔍 Tempo Update System Verification" 7 | echo "==================================" 8 | echo "" 9 | 10 | # Colors for output 11 | RED='\033[0;31m' 12 | GREEN='\033[0;32m' 13 | YELLOW='\033[1;33m' 14 | BLUE='\033[0;34m' 15 | NC='\033[0m' # No Color 16 | 17 | # Function to print status 18 | print_status() { 19 | local status=$1 20 | local message=$2 21 | if [ "$status" = "ok" ]; then 22 | echo -e "${GREEN}✅${NC} $message" 23 | elif [ "$status" = "warning" ]; then 24 | echo -e "${YELLOW}⚠️${NC} $message" 25 | else 26 | echo -e "${RED}❌${NC} $message" 27 | fi 28 | } 29 | 30 | # Function to check if a file exists 31 | check_file() { 32 | local file=$1 33 | local description=$2 34 | if [ -f "$file" ]; then 35 | print_status "ok" "$description found" 36 | return 0 37 | else 38 | print_status "error" "$description not found at $file" 39 | return 1 40 | fi 41 | } 42 | 43 | # Function to check if a command exists 44 | check_command() { 45 | local cmd=$1 46 | local description=$2 47 | if command -v "$cmd" &> /dev/null; then 48 | print_status "ok" "$description available" 49 | return 0 50 | else 51 | print_status "error" "$description not found" 52 | return 1 53 | fi 54 | } 55 | 56 | echo "📋 Checking Dependencies" 57 | echo "------------------------" 58 | 59 | # Check for required commands 60 | check_command "npm" "npm" 61 | check_command "npx" "npx" 62 | check_command "git" "git" 63 | 64 | # Check if tauri CLI is available 65 | if npx tauri --version &> /dev/null; then 66 | print_status "ok" "Tauri CLI available" 67 | else 68 | print_status "error" "Tauri CLI not available" 69 | fi 70 | 71 | echo "" 72 | echo "📁 Checking Project Files" 73 | echo "-------------------------" 74 | 75 | # Check critical files 76 | check_file "src-tauri/tauri.conf.json" "Tauri configuration" 77 | check_file ".github/workflows/release.yml" "GitHub Actions workflow" 78 | check_file "src-tauri/capabilities/default.json" "Tauri capabilities" 79 | check_file "src/managers/update-manager.js" "Update manager" 80 | check_file "src/components/update-notification.js" "Update notification component" 81 | 82 | echo "" 83 | echo "🔧 Checking Configuration" 84 | echo "-------------------------" 85 | 86 | # Check tauri.conf.json for updater configuration 87 | if [ -f "src-tauri/tauri.conf.json" ]; then 88 | if grep -q '"updater"' src-tauri/tauri.conf.json; then 89 | print_status "ok" "Updater plugin configured in tauri.conf.json" 90 | 91 | # Check if pubkey is set 92 | if grep -q '"pubkey": ""' src-tauri/tauri.conf.json; then 93 | print_status "warning" "Public key is empty in tauri.conf.json" 94 | elif grep -q '"pubkey":' src-tauri/tauri.conf.json; then 95 | print_status "ok" "Public key is set in tauri.conf.json" 96 | fi 97 | 98 | # Check endpoint 99 | if grep -q 'YOUR_USERNAME' src-tauri/tauri.conf.json; then 100 | print_status "warning" "GitHub repository placeholders not replaced" 101 | elif grep -q 'github.com' src-tauri/tauri.conf.json; then 102 | print_status "ok" "GitHub endpoint configured" 103 | fi 104 | else 105 | print_status "error" "Updater plugin not configured in tauri.conf.json" 106 | fi 107 | fi 108 | 109 | # Check capabilities 110 | if [ -f "src-tauri/capabilities/default.json" ]; then 111 | if grep -q 'updater:allow-check' src-tauri/capabilities/default.json; then 112 | print_status "ok" "Updater permissions configured" 113 | else 114 | print_status "error" "Updater permissions missing in capabilities" 115 | fi 116 | fi 117 | 118 | echo "" 119 | echo "🔑 Checking Signing Keys" 120 | echo "------------------------" 121 | 122 | # Check for signing keys 123 | KEY_PATH="$HOME/.tauri/tempo_signing_key" 124 | PUB_KEY_PATH="$HOME/.tauri/tempo_signing_key.pub" 125 | 126 | if [ -f "$KEY_PATH" ]; then 127 | print_status "ok" "Private signing key found" 128 | else 129 | print_status "error" "Private signing key not found at $KEY_PATH" 130 | fi 131 | 132 | if [ -f "$PUB_KEY_PATH" ]; then 133 | print_status "ok" "Public signing key found" 134 | echo -e "${BLUE}📄 Public key content:${NC}" 135 | cat "$PUB_KEY_PATH" 136 | echo "" 137 | else 138 | print_status "error" "Public signing key not found at $PUB_KEY_PATH" 139 | fi 140 | 141 | echo "" 142 | echo "📦 Checking Dependencies" 143 | echo "------------------------" 144 | 145 | # Check package.json for required dependencies 146 | if [ -f "package.json" ]; then 147 | if grep -q '@tauri-apps/plugin-updater' package.json; then 148 | print_status "ok" "Updater plugin dependency found" 149 | else 150 | print_status "warning" "Updater plugin dependency not found in package.json" 151 | fi 152 | 153 | if grep -q '@tauri-apps/plugin-dialog' package.json; then 154 | print_status "ok" "Dialog plugin dependency found" 155 | else 156 | print_status "warning" "Dialog plugin dependency not found in package.json" 157 | fi 158 | fi 159 | 160 | echo "" 161 | echo "🚀 Next Steps" 162 | echo "-------------" 163 | 164 | if [ -f "$KEY_PATH" ] && [ -f "$PUB_KEY_PATH" ]; then 165 | echo -e "${GREEN}1.${NC} Add these secrets to your GitHub repository:" 166 | echo " - TAURI_SIGNING_PRIVATE_KEY: (content of $KEY_PATH)" 167 | echo " - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: (your key password)" 168 | echo "" 169 | fi 170 | 171 | echo -e "${GREEN}2.${NC} Test the setup by creating a release:" 172 | echo " git tag v0.1.1" 173 | echo " git push origin v0.1.1" 174 | echo "" 175 | 176 | echo -e "${GREEN}3.${NC} Monitor the GitHub Actions workflow" 177 | echo "" 178 | 179 | echo -e "${GREEN}4.${NC} Test update checking in the compiled app" 180 | echo "" 181 | 182 | echo "🎉 Verification complete!" 183 | echo "For detailed setup instructions, see: UPDATES.md" 184 | --------------------------------------------------------------------------------