Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f7aa074bc | ||
|
|
d26b73288d | ||
|
|
8a04edc2bc | ||
|
|
aadc1eaac0 | ||
|
|
acf06c8d32 | ||
|
|
95f684450c | ||
|
|
3ae9db3be0 | ||
| 51541c9b66 | |||
|
|
743477cd4e | ||
|
|
666b2418b9 |
125
README.md
125
README.md
@@ -21,7 +21,7 @@
|
|||||||
<img src="https://img.shields.io/badge/svelte-5-FF3E00?style=flat-square&logo=svelte&logoColor=white" alt="Svelte 5" />
|
<img src="https://img.shields.io/badge/svelte-5-FF3E00?style=flat-square&logo=svelte&logoColor=white" alt="Svelte 5" />
|
||||||
<img src="https://img.shields.io/badge/rust-2021-000000?style=flat-square&logo=rust&logoColor=white" alt="Rust" />
|
<img src="https://img.shields.io/badge/rust-2021-000000?style=flat-square&logo=rust&logoColor=white" alt="Rust" />
|
||||||
<img src="https://img.shields.io/badge/tailwind-v4-06B6D4?style=flat-square&logo=tailwindcss&logoColor=white" alt="Tailwind v4" />
|
<img src="https://img.shields.io/badge/tailwind-v4-06B6D4?style=flat-square&logo=tailwindcss&logoColor=white" alt="Tailwind v4" />
|
||||||
<img src="https://img.shields.io/badge/WCAG_2.1-AA-228B22?style=flat-square" alt="WCAG 2.1 AA" />
|
<img src="https://img.shields.io/badge/WCAG_2.2-AAA-228B22?style=flat-square&logo=w3c&logoColor=white" alt="WCAG 2.2 AAA" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Why does this exist?
|
## 💡 Why does this exist?
|
||||||
|
|
||||||
Repetitive strain injury and eye strain are not personal failings. They are the predictable result of systems that treat human attention as an extractable resource. Every person at a screen deserves a tool that gently interrupts the grind - one that serves *them*, not a subscription model, not an analytics dashboard, not a corporate wellness KPI.
|
Repetitive strain injury and eye strain are not personal failings. They are the predictable result of systems that treat human attention as an extractable resource. Every person at a screen deserves a tool that gently interrupts the grind - one that serves *them*, not a subscription model, not an analytics dashboard, not a corporate wellness KPI.
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ Core Cooldown is a single portable `.exe`. No installer, no account, no telemetr
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Philosophy
|
## 🧭 Philosophy
|
||||||
|
|
||||||
Core Cooldown exists because rest is not a luxury. It is a fundamental need that no productivity framework, no employer, and no software platform should gatekeep behind a paywall or a subscription.
|
Core Cooldown exists because rest is not a luxury. It is a fundamental need that no productivity framework, no employer, and no software platform should gatekeep behind a paywall or a subscription.
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ Tools for human wellbeing should never be enclosed, never be scarce, and never s
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Screenshots
|
## 📸 Screenshots
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="screenshots/01-dashboard.png" alt="Dashboard - Main Timer" width="420" /><br />
|
<img src="screenshots/01-dashboard.png" alt="Dashboard - Main Timer" width="420" /><br />
|
||||||
@@ -98,9 +98,9 @@ Tools for human wellbeing should never be enclosed, never be scarce, and never s
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Features
|
## ✨ Features
|
||||||
|
|
||||||
### Timer & Breaks
|
### ⏱️ Timer & Breaks
|
||||||
|
|
||||||
| | Feature | Description |
|
| | Feature | Description |
|
||||||
|:--|:--------|:------------|
|
|:--|:--------|:------------|
|
||||||
@@ -116,7 +116,7 @@ Tools for human wellbeing should never be enclosed, never be scarce, and never s
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
### Pomodoro Mode
|
### 🍅 Pomodoro Mode
|
||||||
|
|
||||||
A full Pomodoro timer built in. Alternates short breaks with a longer recovery break after a configurable number of focus sessions.
|
A full Pomodoro timer built in. Alternates short breaks with a longer recovery break after a configurable number of focus sessions.
|
||||||
|
|
||||||
@@ -128,7 +128,7 @@ A full Pomodoro timer built in. Alternates short breaks with a longer recovery b
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
### Microbreaks (20-20-20 Rule)
|
### 👁️ Microbreaks (20-20-20 Rule)
|
||||||
|
|
||||||
Quick eye rest reminders between full breaks. The 20-20-20 rule: every 20 minutes, look at something 20 feet away for 20 seconds.
|
Quick eye rest reminders between full breaks. The 20-20-20 rule: every 20 minutes, look at something 20 feet away for 20 seconds.
|
||||||
|
|
||||||
@@ -141,7 +141,7 @@ Quick eye rest reminders between full breaks. The 20-20-20 rule: every 20 minute
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
### Breathing Guide
|
### 🌬️ Breathing Guide
|
||||||
|
|
||||||
A visual breathing exercise during breaks. The breathing text pulses with the rhythm - scaling up on inhale, holding on hold, scaling down on exhale - with a color gradient that interpolates between your accent and break colors.
|
A visual breathing exercise during breaks. The breathing text pulses with the rhythm - scaling up on inhale, holding on hold, scaling down on exhale - with a color gradient that interpolates between your accent and break colors.
|
||||||
|
|
||||||
@@ -155,7 +155,7 @@ A visual breathing exercise during breaks. The breathing text pulses with the rh
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
### Idle Detection & Smart Breaks
|
### 💤 Idle Detection & Smart Breaks
|
||||||
|
|
||||||
| | Feature | Description |
|
| | Feature | Description |
|
||||||
|:--|:--------|:------------|
|
|:--|:--------|:------------|
|
||||||
@@ -166,7 +166,7 @@ A visual breathing exercise during breaks. The breathing text pulses with the rh
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
### Presentation Mode
|
### 🎬 Presentation Mode
|
||||||
|
|
||||||
Detects fullscreen applications (presentations, video calls, games) and defers breaks until you exit.
|
Detects fullscreen applications (presentations, video calls, games) and defers breaks until you exit.
|
||||||
|
|
||||||
@@ -177,7 +177,7 @@ Detects fullscreen applications (presentations, video calls, games) and defers b
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
### Break Activities
|
### 🤸 Break Activities
|
||||||
|
|
||||||
Each break shows a randomized suggestion from a curated library of **71 activities** across four categories:
|
Each break shows a randomized suggestion from a curated library of **71 activities** across four categories:
|
||||||
|
|
||||||
@@ -198,7 +198,7 @@ Activities cycle every 30 seconds and never repeat consecutively.
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
### Goals & Streaks
|
### 🏆 Goals & Streaks
|
||||||
|
|
||||||
| | Feature | Description |
|
| | Feature | Description |
|
||||||
|:--|:--------|:------------|
|
|:--|:--------|:------------|
|
||||||
@@ -209,7 +209,7 @@ Activities cycle every 30 seconds and never repeat consecutively.
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
### Screen Dimming
|
### 🌑 Screen Dimming
|
||||||
|
|
||||||
A gentle pre-break nudge that gradually dims your screen before the break starts.
|
A gentle pre-break nudge that gradually dims your screen before the break starts.
|
||||||
|
|
||||||
@@ -219,7 +219,7 @@ A gentle pre-break nudge that gradually dims your screen before the break starts
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
### Working Hours Schedule
|
### 📅 Working Hours Schedule
|
||||||
|
|
||||||
A per-day schedule with multiple time ranges per day. The timer only runs during your configured hours - outside those hours, it pauses automatically.
|
A per-day schedule with multiple time ranges per day. The timer only runs during your configured hours - outside those hours, it pauses automatically.
|
||||||
|
|
||||||
@@ -229,7 +229,7 @@ A per-day schedule with multiple time ranges per day. The timer only runs during
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
### Statistics & History
|
### 📊 Statistics & History
|
||||||
|
|
||||||
| | Metric | Description |
|
| | Metric | Description |
|
||||||
|:--|:-------|:------------|
|
|:--|:-------|:------------|
|
||||||
@@ -243,7 +243,7 @@ All statistics stored locally in a plain JSON file next to the executable.
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
### Sound Effects
|
### 🔊 Sound Effects
|
||||||
|
|
||||||
Synthesized notification sounds via Web Audio API - no bundled audio files, no network requests.
|
Synthesized notification sounds via Web Audio API - no bundled audio files, no network requests.
|
||||||
|
|
||||||
@@ -253,7 +253,7 @@ Sounds play on break start, pre-break warning, and break completion. Volume conf
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
### Global Keyboard Shortcuts
|
### ⌨️ Global Keyboard Shortcuts
|
||||||
|
|
||||||
| Shortcut | Action |
|
| Shortcut | Action |
|
||||||
|:---------|:-------|
|
|:---------|:-------|
|
||||||
@@ -265,7 +265,7 @@ Works system-wide, even when Core Cooldown is not focused.
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
### System Tray
|
### 🔵 System Tray
|
||||||
|
|
||||||
- **Dynamic icon** - 32x32 progress arc rendered in real-time (orange = focus, purple = break, dimmed = paused)
|
- **Dynamic icon** - 32x32 progress arc rendered in real-time (orange = focus, purple = break, dimmed = paused)
|
||||||
- **Countdown tooltip** - hover over tray icon to see time remaining
|
- **Countdown tooltip** - hover over tray icon to see time remaining
|
||||||
@@ -273,7 +273,7 @@ Works system-wide, even when Core Cooldown is not focused.
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
### Mini Mode
|
### 📌 Mini Mode
|
||||||
|
|
||||||
A compact floating timer (200x50px) that sits on top of your other windows.
|
A compact floating timer (200x50px) that sits on top of your other windows.
|
||||||
|
|
||||||
@@ -284,7 +284,7 @@ A compact floating timer (200x50px) that sits on top of your other windows.
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
### Appearance & Customization
|
### 🎨 Appearance & Customization
|
||||||
|
|
||||||
| Setting | Range |
|
| Setting | Range |
|
||||||
|:--------|:------|
|
|:--------|:------|
|
||||||
@@ -297,7 +297,7 @@ A compact floating timer (200x50px) that sits on top of your other windows.
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
### Notifications
|
### 🔔 Notifications
|
||||||
|
|
||||||
Native Windows toast notifications for:
|
Native Windows toast notifications for:
|
||||||
- Pre-break warnings (configurable seconds before break)
|
- Pre-break warnings (configurable seconds before break)
|
||||||
@@ -308,7 +308,7 @@ Native Windows toast notifications for:
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
### Window Behavior
|
### 🪟 Window Behavior
|
||||||
|
|
||||||
- **Frameless window** with custom titlebar and drag region
|
- **Frameless window** with custom titlebar and drag region
|
||||||
- **Transparent background** with frosted glass effects (backdrop-blur)
|
- **Transparent background** with frosted glass effects (backdrop-blur)
|
||||||
@@ -318,27 +318,60 @@ Native Windows toast notifications for:
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
### Accessibility
|
### ♿ Accessibility
|
||||||
|
|
||||||
Core Cooldown targets **WCAG 2.1 Level AA** compliance. A break timer for preventing repetitive strain injury should be usable by everyone - including those who already live with disabilities.
|
<p>
|
||||||
|
<img src="https://img.shields.io/badge/WCAG_2.2-AAA_Conformance-228B22?style=for-the-badge&logo=w3c&logoColor=white" alt="WCAG 2.2 AAA Conformance" />
|
||||||
|
<img src="https://img.shields.io/badge/since-v0.2.0-blue?style=for-the-badge" alt="Since v0.2.0" />
|
||||||
|
</p>
|
||||||
|
|
||||||
| | Feature | Description |
|
Core Cooldown targets **WCAG 2.2 Level AAA** conformance - the highest level of the Web Content Accessibility Guidelines. A break timer for preventing repetitive strain injury should be usable by everyone, including those who already live with disabilities.
|
||||||
|:--|:--------|:------------|
|
|
||||||
| ⌨️ | **Full keyboard navigation** | Every control reachable and operable via keyboard alone. Arrow keys navigate dropdowns, adjust color pickers, steppers, and time spinners. Tab/Shift+Tab cycles through all interactive elements. |
|
> **Why AAA?** Most applications stop at AA. We went further because the people who benefit most from a break timer - those with repetitive strain injuries, chronic pain, or vision impairments - are the same people who need the strongest accessibility support. AAA isn't a checkbox. It's the right thing to do.
|
||||||
| 🔍 | **Visible focus indicators** | Global `:focus-visible` outlines on all interactive elements - no hidden or suppressed focus rings |
|
|
||||||
| 🗣️ | **Screen reader support** | `aria-live` regions announce timer state, breathing phase, break activities, and status changes. Progress rings use `role="progressbar"` with value text. Accordion panels have `aria-controls` and `aria-expanded`. Custom dropdowns support `role="listbox"` with arrow key navigation. |
|
#### Contrast & Visual Design
|
||||||
| 🎯 | **Focus management** | View transitions move focus to the new view's heading. Break screen traps focus to prevent interaction with obscured content. Dropdown focus returns to trigger on close. |
|
|
||||||
| 🎨 | **Color contrast** | All text meets 4.5:1 minimum contrast ratio against dark backgrounds. Dynamic breathing text color interpolation validated against threshold. |
|
| | Feature | Standard | Description |
|
||||||
| 🖥️ | **Windows High Contrast** | `forced-colors: active` media query maps all theme tokens to system colors |
|
|:--|:--------|:---------|:------------|
|
||||||
| 🐢 | **Reduced motion** | `prefers-reduced-motion` disables all CSS animations/transitions, all JavaScript-driven Web Animations API effects, and momentum scroll physics. No functionality lost - just calmer. |
|
| 🎨 | **Enhanced text contrast** | AAA 7:1 | All body text meets 7:1 contrast ratio against dark backgrounds. Secondary text `#a8a8a8` on `#000` = 7.28:1. |
|
||||||
| 🏷️ | **Descriptive labels** | All toggle switches, steppers, buttons, dropdowns, and form controls have descriptive accessible names |
|
| 🔤 | **Large text contrast** | AAA 4.5:1 | Headings and large text (18px+) meet 4.5:1 minimum. Timer countdown, break titles validated. |
|
||||||
| 👆 | **Touch targets** | Interactive elements meet minimum 32x32px hit areas for comfortable interaction |
|
| 🎯 | **Non-text contrast** | AA 3:1 | UI components (toggles, steppers, rings, chart bars, color swatches) all meet 3:1 against adjacent colors. |
|
||||||
|
| 🖥️ | **Windows High Contrast** | AAA | `forced-colors: active` maps all theme tokens to system colors. Full usability in all Windows contrast themes. |
|
||||||
|
| 🐢 | **Reduced motion** | AAA | `prefers-reduced-motion` disables all CSS animations, JS Web Animations API effects, and momentum scroll. Zero functionality loss. |
|
||||||
|
|
||||||
|
#### Keyboard & Navigation
|
||||||
|
|
||||||
|
| | Feature | Standard | Description |
|
||||||
|
|:--|:--------|:---------|:------------|
|
||||||
|
| ⌨️ | **Full keyboard access** | AAA 2.1.3 | Every control operable via keyboard alone - no exceptions. Arrow keys for color pickers, steppers, radio groups. Tab/Shift+Tab cycles all interactive elements. |
|
||||||
|
| 🔍 | **Visible focus indicators** | AAA 2.4.13 | 2px solid white outline with dark shadow fallback on every interactive element. No hidden or suppressed focus rings. |
|
||||||
|
| ⏭️ | **Skip navigation** | AA 2.4.1 | Skip-to-content link bypasses the titlebar on Tab. |
|
||||||
|
| 🏠 | **Focus management** | AA 2.4.3 | View transitions auto-focus the new view's heading. Break screen traps focus. Dropdown focus returns to trigger on close. |
|
||||||
|
| 🏷️ | **Heading structure** | AAA 2.4.10 | Semantic `h1` > `h2` hierarchy across all views. Settings sections use `h2` with `aria-labelledby`. |
|
||||||
|
|
||||||
|
#### Screen Readers & Assistive Technology
|
||||||
|
|
||||||
|
| | Feature | Standard | Description |
|
||||||
|
|:--|:--------|:---------|:------------|
|
||||||
|
| 🗣️ | **Live regions** | AA 4.1.3 | `aria-live` announces timer state, breathing phase, break activities, status changes, and celebration events. |
|
||||||
|
| 📊 | **Semantic roles** | AA 4.1.2 | `progressbar` on timer rings, `switch` on toggles, `tablist`/`tab`/`tabpanel` on stats view, `radiogroup`/`radio` on breathing patterns, `alertdialog` on overlays. |
|
||||||
|
| 📋 | **Data tables** | A 1.3.1 | Screen-reader-only data tables behind the 7-day chart provide the same information non-visually. |
|
||||||
|
| 🏷️ | **Accessible names** | AA 4.1.2 | Every toggle, stepper, button, swatch, and form control has a descriptive `aria-label` or visible label. Sound presets use `aria-pressed`. |
|
||||||
|
| 📝 | **Page titles** | AAA 2.4.2 | Dynamic `document.title` updates per view ("Core Cooldown - Dashboard", "- Settings", etc.). |
|
||||||
|
|
||||||
|
#### Target Sizes & Interaction
|
||||||
|
|
||||||
|
| | Feature | Standard | Description |
|
||||||
|
|:--|:--------|:---------|:------------|
|
||||||
|
| 👆 | **44px touch targets** | AAA 2.5.8 | All interactive elements (buttons, toggles, steppers, color swatches, titlebar controls) have minimum 44x44px hit areas. Visual size may be smaller - the clickable area extends invisibly. |
|
||||||
|
| 🎯 | **Celebration persistence** | - | Milestone/goal popups stay visible on hover or focus, with keyboard-accessible dismiss buttons and Escape key support. |
|
||||||
|
| ⏱️ | **Hold-to-repeat** | - | Stepper +/- buttons support press-and-hold for continuous increment, with keyboard Enter/Space triggering the same behavior. |
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Portability
|
## 📦 Portability
|
||||||
|
|
||||||
Core Cooldown is **fully portable**. The executable carries everything it needs and stores everything it creates right next to itself:
|
Core Cooldown is **fully portable**. The executable carries everything it needs and stores everything it creates right next to itself:
|
||||||
|
|
||||||
@@ -360,7 +393,7 @@ anywhere-you-want/
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Installation
|
## 🚀 Installation
|
||||||
|
|
||||||
```
|
```
|
||||||
1. Download core-cooldown.exe from the Releases page
|
1. Download core-cooldown.exe from the Releases page
|
||||||
@@ -376,7 +409,7 @@ That's it. No elevated permissions. No runtime dependencies. The first launch ma
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Building from Source
|
## 🔧 Building from Source
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><strong>Prerequisites</strong></summary>
|
<summary><strong>Prerequisites</strong></summary>
|
||||||
@@ -435,7 +468,7 @@ Output: `src-tauri/target/x86_64-pc-windows-gnu/release/core-cooldown.exe`
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Architecture
|
## 🏗️ Architecture
|
||||||
|
|
||||||
A split-architecture desktop app: Rust backend for system integration and timer logic, Svelte frontend rendered in a native WebView.
|
A split-architecture desktop app: Rust backend for system integration and timer logic, Svelte frontend rendered in a native WebView.
|
||||||
|
|
||||||
@@ -450,7 +483,7 @@ A split-architecture desktop app: Rust backend for system integration and timer
|
|||||||
│ │ (WebView) │ │ (WebView) │ │ (WebView) │ │
|
│ │ (WebView) │ │ (WebView) │ │ (WebView) │ │
|
||||||
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
|
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
|
||||||
│ │ │ │ │
|
│ │ │ │ │
|
||||||
│ └─────────┬───────┴──────────────────┘ │
|
│ └─────────┬────────┴──────────────────┘ │
|
||||||
│ │ Tauri IPC │
|
│ │ Tauri IPC │
|
||||||
│ ┌─────────┴─────────┐ │
|
│ ┌─────────┴─────────┐ │
|
||||||
│ │ Rust Backend │ │
|
│ │ Rust Backend │ │
|
||||||
@@ -510,7 +543,7 @@ A split-architecture desktop app: Rust backend for system integration and timer
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Configuration Reference
|
## ⚙️ Configuration Reference
|
||||||
|
|
||||||
All settings stored in `config.json` next to the executable. The settings panel exposes every option with live validation, but the file is plain JSON and can be edited by hand.
|
All settings stored in `config.json` next to the executable. The settings panel exposes every option with live validation, but the file is plain JSON and can be edited by hand.
|
||||||
|
|
||||||
@@ -677,7 +710,7 @@ All settings stored in `config.json` next to the executable. The settings panel
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Dependencies
|
## 📚 Dependencies
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><strong>Rust crates</strong></summary>
|
<summary><strong>Rust crates</strong></summary>
|
||||||
@@ -713,7 +746,7 @@ All settings stored in `config.json` next to the executable. The settings panel
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Contributing
|
## 🤝 Contributing
|
||||||
|
|
||||||
This project belongs to no one and everyone. If you find it useful and want to make it better, you are welcome.
|
This project belongs to no one and everyone. If you find it useful and want to make it better, you are welcome.
|
||||||
|
|
||||||
@@ -723,7 +756,7 @@ No contribution agreements to sign. No corporate CLAs. No licensing traps. Every
|
|||||||
|
|
||||||
- Report bugs or rough edges
|
- Report bugs or rough edges
|
||||||
- Suggest new break activities (especially with physiotherapy or ergonomics knowledge)
|
- Suggest new break activities (especially with physiotherapy or ergonomics knowledge)
|
||||||
- Improve accessibility (WCAG 2.1 AA foundation is in place - help us push further)
|
- Improve accessibility (WCAG 2.2 AAA foundation is in place - help us maintain it)
|
||||||
- Port idle detection to macOS/Linux
|
- Port idle detection to macOS/Linux
|
||||||
- Translate the interface
|
- Translate the interface
|
||||||
- Share it with someone who needs it
|
- Share it with someone who needs it
|
||||||
@@ -734,7 +767,7 @@ The best software is built through mutual aid - people helping people because it
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## License
|
## 📜 License
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://creativecommons.org/publicdomain/zero/1.0/">
|
<a href="https://creativecommons.org/publicdomain/zero/1.0/">
|
||||||
|
|||||||
218
docs/plans/2026-02-18-wcag-aaa-design.md
Normal file
218
docs/plans/2026-02-18-wcag-aaa-design.md
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
# WCAG 2.2 AAA Compliance Design
|
||||||
|
|
||||||
|
**Date:** 2026-02-18
|
||||||
|
**Scope:** `break-timer/` (frontend + CSS only, no Rust backend changes)
|
||||||
|
**Goal:** Achieve WCAG 2.2 AAA conformance while preserving the existing dark-theme visual identity.
|
||||||
|
|
||||||
|
## Audit Summary
|
||||||
|
|
||||||
|
42 issues found (8 Critical, 14 Major, 20 Minor) across 28 source files. The app already has solid AA foundations: focus indicators, reduced motion support, forced colors mode, ARIA roles on custom widgets, screen-reader text, and keyboard support for complex components.
|
||||||
|
|
||||||
|
## Design Decisions
|
||||||
|
|
||||||
|
1. **Secondary text color:** `#8a8a8a` -> `#a8a8a8` (7.28:1 ratio, minimal visual change)
|
||||||
|
2. **Target sizes:** Enlarge controls to 44px (visual + padding), including 20px traffic lights, 28px swatches, 36px steppers
|
||||||
|
3. **Auto-dismiss toasts:** Persist until dismissed when user hovers/focuses; auto-fade only if untouched
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Section 1: Color & Contrast System
|
||||||
|
|
||||||
|
### Theme Token Changes (`app.css`)
|
||||||
|
|
||||||
|
| Token | Current | New | Ratio on #000 |
|
||||||
|
|-------|---------|-----|---------------|
|
||||||
|
| `--color-text-sec` | `#8a8a8a` | `#a8a8a8` | 7.28:1 |
|
||||||
|
| `--color-text-dim` | `#3a3a3a` | `#5c5c5c` | 3.5:1 (decorative) |
|
||||||
|
| `--color-border` | `#222222` | `#3a3a3a` | 2.63:1 (non-text) |
|
||||||
|
| New: `--color-input-border` | — | `#444444` | 3.14:1 |
|
||||||
|
| New: `--color-surface-lt` | — | `#1e1e1e` | 1.28:1 (bg-to-bg) |
|
||||||
|
|
||||||
|
### Hardcoded Color Replacements
|
||||||
|
|
||||||
|
- All `text-[#8a8a8a]` -> `text-text-sec` (Tailwind theme token)
|
||||||
|
- All `border-[#222]` -> `border-border`
|
||||||
|
- All `border-[#161616]` -> `border-[#333]` (card dividers: 3:1)
|
||||||
|
- All `bg-[#141414]` (stepper bg) -> `bg-[#1a1a1a]` with `border border-[#3a3a3a]`
|
||||||
|
- Toggle OFF knob: `#444` -> `#666`
|
||||||
|
- Mini paused text: `#555` -> `#a8a8a8`
|
||||||
|
- Placeholder text: `#2a2a2a` -> `#555` (3.37:1)
|
||||||
|
- Danger color: `#f85149` -> `#ff6b6b` (7.41:1)
|
||||||
|
|
||||||
|
### Focus Indicator Safety
|
||||||
|
|
||||||
|
When accent color is too dark, add white outer shadow fallback:
|
||||||
|
|
||||||
|
```css
|
||||||
|
:focus-visible {
|
||||||
|
outline: 2px solid var(--color-accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
box-shadow: 0 0 0 4px rgba(255,255,255,0.3);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Section 2: Target Size Enlargement
|
||||||
|
|
||||||
|
| Component | Current | New Visual | Hit Area |
|
||||||
|
|-----------|---------|-----------|----------|
|
||||||
|
| Titlebar traffic lights | 15x15px | 20x20px | 44x44px (padding) |
|
||||||
|
| Color swatches | 22x22px | 28x28px | 44px spacing |
|
||||||
|
| Stepper +/- buttons | 28x28px | 36x36px | 44x44px (padding) |
|
||||||
|
| Toggle switch | 48x24px | 52x28px | 52x44px (padding) |
|
||||||
|
| Back button | 32x32px | 40x40px | 44x44px |
|
||||||
|
| Stats tab buttons | ~60x30px | ~60x40px | 44px height |
|
||||||
|
| Activity star/remove | 32x32px | 36x36px | 44x44px (padding) |
|
||||||
|
| Time range buttons | 32x32px | 36x36px | 44x44px (padding) |
|
||||||
|
|
||||||
|
Strategy: Use `min-h-[44px] min-w-[44px]` on interactive elements with padding for visual sizing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Section 3: Heading Hierarchy & Document Structure
|
||||||
|
|
||||||
|
### Heading Fixes
|
||||||
|
|
||||||
|
- Each view keeps `<h1 class="sr-only" tabindex="-1">`
|
||||||
|
- Settings/Stats: Change all `<h3>` to `<h2>`
|
||||||
|
- Working Hours section: Add missing heading
|
||||||
|
- BreakScreen `<h2>` stays correct
|
||||||
|
|
||||||
|
### Landmark Regions
|
||||||
|
|
||||||
|
- Titlebar: Wrap in `<header role="banner">`
|
||||||
|
- Dashboard bottom buttons: Wrap in `<nav aria-label="Main actions">`
|
||||||
|
- Stats tab bar: `<nav>` with `role="tablist"` / `role="tab"` / `role="tabpanel"`
|
||||||
|
- All sections: Add `aria-labelledby` pointing to heading `id`
|
||||||
|
|
||||||
|
### Document Title
|
||||||
|
|
||||||
|
Add `$effect` in `App.svelte` to set `document.title = "Core Cooldown - ${viewName}"` on view change.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Section 4: Keyboard & Focus Fixes
|
||||||
|
|
||||||
|
### C5: Strict Mode Keyboard Trap
|
||||||
|
|
||||||
|
When `strict_mode` is true and buttons are hidden, render a visually hidden focusable `<span tabindex="0">` with sr-only text "Break in progress, please wait" so focus trap always has one element.
|
||||||
|
|
||||||
|
### Stepper Keyboard Hold-to-Repeat
|
||||||
|
|
||||||
|
Add `onkeydown` handler that starts repeat on held ArrowUp/ArrowDown/Enter/Space.
|
||||||
|
|
||||||
|
### `pressable` Keyboard Feedback
|
||||||
|
|
||||||
|
Add `keydown`/`keyup` listeners for Enter/Space to trigger same scale animation.
|
||||||
|
|
||||||
|
### `glowHover` Keyboard Focus
|
||||||
|
|
||||||
|
Add `focusin`/`focusout` listeners alongside `mouseenter`/`mouseleave`.
|
||||||
|
|
||||||
|
### Missing ARIA States
|
||||||
|
|
||||||
|
- Sound presets: `aria-pressed={$config.sound_preset === preset}`
|
||||||
|
- Breathing patterns: `role="radiogroup"` wrapper, `role="radio"` + `aria-checked` on buttons
|
||||||
|
- Color swatches: `aria-pressed={value === color}`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Section 5: Timing & Notification Control
|
||||||
|
|
||||||
|
### Toast Persistence
|
||||||
|
|
||||||
|
All auto-dismissing elements (natural break toast, celebration overlay, goal badge):
|
||||||
|
|
||||||
|
1. Track `hovering` via `mouseenter/leave` + `focusin/focusout`
|
||||||
|
2. Auto-dismiss timer pauses while hovering
|
||||||
|
3. Close button appears (sr-only "Dismiss notification")
|
||||||
|
4. Escape key dismisses
|
||||||
|
5. `role="alert"` + `aria-live="assertive"` kept
|
||||||
|
|
||||||
|
### Breathing Guide Noise Fix
|
||||||
|
|
||||||
|
Change breathing `aria-live` span to only announce phase changes (Inhale/Hold/Exhale), not countdown ticks. Track `lastAnnouncedPhase` and update live text only on phase name change.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Section 6: ARIA Patterns & Semantic Enrichment
|
||||||
|
|
||||||
|
### Section Accessible Names
|
||||||
|
|
||||||
|
Add `id` to each heading, `aria-labelledby` on parent `<section>`.
|
||||||
|
|
||||||
|
### Stats Tab Pattern
|
||||||
|
|
||||||
|
- `role="tablist"` wrapper
|
||||||
|
- `role="tab"` + `aria-selected` + `aria-controls` on each tab
|
||||||
|
- `role="tabpanel"` + `id` + `aria-labelledby` on panels
|
||||||
|
|
||||||
|
### Missing Data Tables
|
||||||
|
|
||||||
|
Add sr-only `<table>` for 30-day chart and heatmap (matching existing 7-day pattern).
|
||||||
|
|
||||||
|
### Additional ARIA
|
||||||
|
|
||||||
|
- Daily goal bar: `role="progressbar"` + aria-value attributes
|
||||||
|
- BreakOverlay: `role="alertdialog"` + `aria-label` + `<h2>`
|
||||||
|
- MicrobreakOverlay: `role="alertdialog"` + `aria-label` + heading
|
||||||
|
- Reset button: `aria-live="polite"` for confirmation state
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Section 7: Visual Presentation (1.4.8)
|
||||||
|
|
||||||
|
### Line Spacing
|
||||||
|
|
||||||
|
Default `leading-relaxed` (1.625) on body. Override with `leading-none` only on countdown numerics.
|
||||||
|
|
||||||
|
### Text Selection
|
||||||
|
|
||||||
|
Remove global `user-select: none`. Keep only on drag regions and decorative elements.
|
||||||
|
|
||||||
|
### Skip Navigation
|
||||||
|
|
||||||
|
Add skip link as first child of `<main>`: `<a href="#main-content" class="sr-only focus:not-sr-only">Skip to content</a>`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Section 8: Supplementary
|
||||||
|
|
||||||
|
### Abbreviations
|
||||||
|
|
||||||
|
Add `<abbr>` tags for standalone "s", "min", "h", "m" units. Add `title` on first use of Pomodoro, 20-20-20 in Settings.
|
||||||
|
|
||||||
|
### Help Tooltips
|
||||||
|
|
||||||
|
Add `title` attributes on complex settings (Pomodoro, Smart breaks, Microbreaks, Breathing).
|
||||||
|
|
||||||
|
### Known Limitations
|
||||||
|
|
||||||
|
- Canvas chart text spacing: Not fixable (Canvas API limitation). Sr-only data tables provide equivalent access.
|
||||||
|
- Timer interruptions: Pause button effectively suppresses all breaks. No separate DND mode needed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files Affected
|
||||||
|
|
||||||
|
| File | Change Scope |
|
||||||
|
|------|-------------|
|
||||||
|
| `app.css` | Tokens, line-height, user-select, skip link, focus ring |
|
||||||
|
| `App.svelte` | Skip link, document title |
|
||||||
|
| `Titlebar.svelte` | Larger traffic lights, header landmark |
|
||||||
|
| `Dashboard.svelte` | Contrast, nav landmark, toast persistence, pomodoro alt text, goal progressbar |
|
||||||
|
| `BreakScreen.svelte` | Contrast, strict-mode focus, breathing aria-live fix |
|
||||||
|
| `Settings.svelte` | h2 headings, section labels, contrast, sizes, radio groups, abbr, title, reset aria-live |
|
||||||
|
| `StatsView.svelte` | h2 headings, tablist, data tables, contrast |
|
||||||
|
| `ToggleSwitch.svelte` | Larger (52x28), knob contrast |
|
||||||
|
| `Stepper.svelte` | Larger (36px), keyboard hold-repeat, contrast |
|
||||||
|
| `ColorPicker.svelte` | Larger swatches, aria-pressed, contrast |
|
||||||
|
| `FontSelector.svelte` | Contrast |
|
||||||
|
| `TimeSpinner.svelte` | Contrast |
|
||||||
|
| `ActivityManager.svelte` | Larger buttons, contrast, scroll fix |
|
||||||
|
| `MiniTimer.svelte` | Contrast |
|
||||||
|
| `MicrobreakOverlay.svelte` | Heading, role, label |
|
||||||
|
| `BreakOverlay.svelte` | Heading, role, label |
|
||||||
|
| `Celebration.svelte` | Toast persistence |
|
||||||
|
| `animate.ts` | pressable keyboard, glowHover focus |
|
||||||
967
docs/plans/2026-02-18-wcag-aaa-implementation.md
Normal file
967
docs/plans/2026-02-18-wcag-aaa-implementation.md
Normal file
@@ -0,0 +1,967 @@
|
|||||||
|
# WCAG 2.2 AAA Implementation Plan
|
||||||
|
|
||||||
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||||
|
|
||||||
|
**Goal:** Achieve WCAG 2.2 AAA conformance across all frontend components while preserving the existing dark-theme visual identity.
|
||||||
|
|
||||||
|
**Architecture:** All changes are CSS + Svelte template only — no Rust backend changes. Theme tokens in `app.css` propagate through Tailwind's `@theme` system. Components use Svelte 5 runes (`$state`, `$derived`, `$effect`, `$props`). Accessibility patterns follow WAI-ARIA 1.2 (tablist, radiogroup, alertdialog, progressbar).
|
||||||
|
|
||||||
|
**Tech Stack:** Svelte 5, Tailwind CSS v4 (`@theme` tokens in CSS, no config file), TypeScript, Web Animations API (`motion` library)
|
||||||
|
|
||||||
|
**Note:** This project has no frontend test suite. Verification is done via `npm run build` (Vite build) and manual inspection. Each task ends with a build check and a commit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Theme Tokens & Global Styles (`app.css`)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `break-timer/src/app.css` (entire file)
|
||||||
|
|
||||||
|
This is the foundation — all subsequent tasks depend on these token changes.
|
||||||
|
|
||||||
|
**Step 1: Update theme tokens**
|
||||||
|
|
||||||
|
In `app.css`, inside the `@theme { }` block (lines 3–20):
|
||||||
|
|
||||||
|
- Change `--color-text-sec: #8a8a8a` → `--color-text-sec: #a8a8a8` (7.28:1 on black)
|
||||||
|
- Change `--color-text-dim: #3a3a3a` → `--color-text-dim: #5c5c5c` (3.5:1 decorative)
|
||||||
|
- Change `--color-border: #222222` → `--color-border: #3a3a3a` (2.63:1 non-text)
|
||||||
|
- Change `--color-danger: #f85149` → `--color-danger: #ff6b6b` (7.41:1)
|
||||||
|
- Add new token: `--color-input-border: #444444;`
|
||||||
|
- Add new token: `--color-surface-lt: #1e1e1e;`
|
||||||
|
|
||||||
|
**Step 2: Update body styles**
|
||||||
|
|
||||||
|
In the `html, body` block (lines 22–35):
|
||||||
|
|
||||||
|
- Change `user-select: none` to only apply on drag regions:
|
||||||
|
- REMOVE `user-select: none;` and `-webkit-user-select: none;` from body
|
||||||
|
- ADD a new rule for drag regions only:
|
||||||
|
```css
|
||||||
|
[data-tauri-drag-region],
|
||||||
|
[data-tauri-drag-region] * {
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Add `line-height: 1.625;` (leading-relaxed) to body for AAA 1.4.8
|
||||||
|
|
||||||
|
**Step 3: Enhance focus indicators**
|
||||||
|
|
||||||
|
Replace the `:focus-visible` block (lines 73–76) with:
|
||||||
|
|
||||||
|
```css
|
||||||
|
:focus-visible {
|
||||||
|
outline: 2px solid var(--color-accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `box-shadow` provides a white fallback when the accent color has low contrast against dark backgrounds.
|
||||||
|
|
||||||
|
**Step 4: Add skip link styles**
|
||||||
|
|
||||||
|
After the `.sr-only` block, add:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.skip-link {
|
||||||
|
position: absolute;
|
||||||
|
top: -100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 10000;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: var(--color-accent);
|
||||||
|
color: #000;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 0 0 8px 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: top 0.15s ease;
|
||||||
|
}
|
||||||
|
.skip-link:focus {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 5: Verify build**
|
||||||
|
|
||||||
|
Run: `cd break-timer && npm run build`
|
||||||
|
Expected: Build succeeds with no errors.
|
||||||
|
|
||||||
|
**Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add break-timer/src/app.css
|
||||||
|
git commit -m "a11y: update theme tokens and global styles for WCAG AAA
|
||||||
|
|
||||||
|
- Bump --color-text-sec to #a8a8a8 (7.28:1 contrast)
|
||||||
|
- Bump --color-text-dim to #5c5c5c (3.5:1 decorative)
|
||||||
|
- Bump --color-border to #3a3a3a (2.63:1 non-text)
|
||||||
|
- Bump --color-danger to #ff6b6b (7.41:1)
|
||||||
|
- Add --color-input-border and --color-surface-lt tokens
|
||||||
|
- Add white shadow fallback on :focus-visible
|
||||||
|
- Add leading-relaxed default line-height
|
||||||
|
- Scope user-select:none to drag regions only
|
||||||
|
- Add skip-link focus styles"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: App Shell (`App.svelte`)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `break-timer/src/App.svelte`
|
||||||
|
|
||||||
|
**Step 1: Add skip link**
|
||||||
|
|
||||||
|
Inside the `<main>` element (line 87), add skip link as first child and an id target:
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<main class="relative h-full bg-black">
|
||||||
|
<a href="#main-content" class="skip-link">Skip to content</a>
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Then on the zoom container `<div>` (line 92), add `id="main-content"`:
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<div
|
||||||
|
id="main-content"
|
||||||
|
class="relative h-full overflow-hidden origin-top-left"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Add document title effect**
|
||||||
|
|
||||||
|
After the existing focus management `$effect` (after line 76), add:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// WCAG 2.4.2: Document title reflects current view
|
||||||
|
$effect(() => {
|
||||||
|
const viewNames: Record<string, string> = {
|
||||||
|
dashboard: "Dashboard",
|
||||||
|
breakScreen: "Break",
|
||||||
|
settings: "Settings",
|
||||||
|
stats: "Statistics",
|
||||||
|
};
|
||||||
|
document.title = `Core Cooldown — ${viewNames[effectiveView] ?? "Dashboard"}`;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Verify build**
|
||||||
|
|
||||||
|
Run: `cd break-timer && npm run build`
|
||||||
|
|
||||||
|
**Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add break-timer/src/App.svelte
|
||||||
|
git commit -m "a11y: add skip link and dynamic document title for WCAG AAA"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Titlebar (`Titlebar.svelte`)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `break-timer/src/lib/components/Titlebar.svelte`
|
||||||
|
|
||||||
|
**Step 1: Wrap in header landmark**
|
||||||
|
|
||||||
|
Change the outer `<div>` (line 8) to `<header role="banner">`.
|
||||||
|
|
||||||
|
**Step 2: Enlarge traffic lights to meet 44px target size**
|
||||||
|
|
||||||
|
Change each traffic light button from `h-[15px] w-[15px]` to `h-[20px] w-[20px]` visual size with `min-h-[44px] min-w-[44px]` hit area via padding. The trick: keep the visual circle at 20px but wrap in a 44px invisible tap area.
|
||||||
|
|
||||||
|
Replace each button's class. For example the Maximize button (line 27):
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<button
|
||||||
|
aria-label="Maximize"
|
||||||
|
class="traffic-btn group/btn relative flex h-[44px] w-[44px] items-center justify-center"
|
||||||
|
onclick={() => appWindow.toggleMaximize()}
|
||||||
|
>
|
||||||
|
<span class="flex h-[20px] w-[20px] items-center justify-center rounded-full bg-[#27C93F] transition-all duration-150 group-hover/btn:brightness-110">
|
||||||
|
<svg ...>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
Apply the same pattern to Minimize and Close buttons. The gap between buttons changes from `gap-[8px]` to `gap-0` since the 44px buttons provide their own spacing.
|
||||||
|
|
||||||
|
**Step 3: Verify build**
|
||||||
|
|
||||||
|
Run: `cd break-timer && npm run build`
|
||||||
|
|
||||||
|
**Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add break-timer/src/lib/components/Titlebar.svelte
|
||||||
|
git commit -m "a11y: add header landmark and enlarge traffic lights to 44px hit areas"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: ToggleSwitch (`ToggleSwitch.svelte`)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `break-timer/src/lib/components/ToggleSwitch.svelte`
|
||||||
|
|
||||||
|
**Step 1: Enlarge to 52x28 and fix knob contrast**
|
||||||
|
|
||||||
|
Change button dimensions from `h-[24px] w-[48px]` to `h-[28px] w-[52px]` with `min-h-[44px]` padding for hit area.
|
||||||
|
|
||||||
|
Change the knob span from `h-[19px] w-[19px]` to `h-[22px] w-[22px]`.
|
||||||
|
Change translate-x for ON state from `translate-x-[26px]` to `translate-x-[27px]`.
|
||||||
|
Change mt from `mt-[2.5px]` to `mt-[3px]`.
|
||||||
|
Change OFF knob color from `bg-[#444]` to `bg-[#666]` (better contrast).
|
||||||
|
|
||||||
|
The button should have `min-h-[44px]` via a wrapper approach or padding.
|
||||||
|
|
||||||
|
**Step 2: Verify build**
|
||||||
|
|
||||||
|
Run: `cd break-timer && npm run build`
|
||||||
|
|
||||||
|
**Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add break-timer/src/lib/components/ToggleSwitch.svelte
|
||||||
|
git commit -m "a11y: enlarge toggle switch and improve OFF knob contrast"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Stepper (`Stepper.svelte`)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `break-timer/src/lib/components/Stepper.svelte`
|
||||||
|
|
||||||
|
**Step 1: Enlarge buttons and fix contrast**
|
||||||
|
|
||||||
|
Change both +/- button dimensions from `h-7 w-7` (28px) to `h-9 w-9` (36px) with `min-h-[44px] min-w-[44px]` padding.
|
||||||
|
|
||||||
|
Change `bg-[#141414]` to `bg-[#1a1a1a] border border-[#3a3a3a]` for better non-text contrast.
|
||||||
|
|
||||||
|
Change `text-[#8a8a8a]` to `text-text-sec` (uses updated theme token).
|
||||||
|
|
||||||
|
**Step 2: Add keyboard hold-to-repeat**
|
||||||
|
|
||||||
|
Add `onkeydown` handlers to both buttons that trigger the same `startHold`/`stopHold` logic for Enter, Space, ArrowUp, ArrowDown keys:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function handleKeydown(fn: () => void, e: KeyboardEvent) {
|
||||||
|
if (["Enter", " ", "ArrowUp", "ArrowDown"].includes(e.key)) {
|
||||||
|
e.preventDefault();
|
||||||
|
startHold(e.key === "ArrowDown" || e.key === "ArrowUp" ? fn : fn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyup(e: KeyboardEvent) {
|
||||||
|
if (["Enter", " ", "ArrowUp", "ArrowDown"].includes(e.key)) {
|
||||||
|
stopHold();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `onkeydown` and `onkeyup` to both buttons. The decrease button uses ArrowDown for decrement, the increase button uses ArrowUp for increment.
|
||||||
|
|
||||||
|
**Step 3: Verify build**
|
||||||
|
|
||||||
|
Run: `cd break-timer && npm run build`
|
||||||
|
|
||||||
|
**Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add break-timer/src/lib/components/Stepper.svelte
|
||||||
|
git commit -m "a11y: enlarge stepper buttons and add keyboard hold-to-repeat"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: Animation Actions (`animate.ts`)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `break-timer/src/lib/utils/animate.ts`
|
||||||
|
|
||||||
|
**Step 1: Add keyboard support to `pressable`**
|
||||||
|
|
||||||
|
In the `pressable` function (line 117), after the mousedown/mouseup/mouseleave listeners (lines 143–145), add keydown/keyup for Enter and Space:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function onKeyDown(e: KeyboardEvent) {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
onDown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeyUp(e: KeyboardEvent) {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
onUp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
node.addEventListener("keydown", onKeyDown);
|
||||||
|
node.addEventListener("keyup", onKeyUp);
|
||||||
|
```
|
||||||
|
|
||||||
|
Update `destroy()` to remove these listeners too.
|
||||||
|
|
||||||
|
**Step 2: Add focus support to `glowHover`**
|
||||||
|
|
||||||
|
In the `glowHover` function (line 165), after mouseenter/mouseleave listeners (lines 201–202), add focusin/focusout:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
node.addEventListener("focusin", onEnter);
|
||||||
|
node.addEventListener("focusout", onLeave);
|
||||||
|
```
|
||||||
|
|
||||||
|
Update `destroy()` to remove these listeners too.
|
||||||
|
|
||||||
|
**Step 3: Verify build**
|
||||||
|
|
||||||
|
Run: `cd break-timer && npm run build`
|
||||||
|
|
||||||
|
**Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add break-timer/src/lib/utils/animate.ts
|
||||||
|
git commit -m "a11y: add keyboard feedback to pressable and focus glow to glowHover"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 7: Dashboard (`Dashboard.svelte`)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `break-timer/src/lib/components/Dashboard.svelte`
|
||||||
|
|
||||||
|
**Step 1: Replace hardcoded `#8a8a8a` with theme token**
|
||||||
|
|
||||||
|
Replace all `text-[#8a8a8a]` with `text-text-sec` throughout the file. There are ~10 instances on lines 180, 208, 216, 234, 241, 256, 301, 329, 361.
|
||||||
|
|
||||||
|
Replace `border-[#222]` with `border-border` on the three bottom buttons (lines 302, 328, 360).
|
||||||
|
|
||||||
|
**Step 2: Wrap bottom buttons in nav landmark**
|
||||||
|
|
||||||
|
Around the three bottom action buttons (lines 296–382), wrap in:
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<nav aria-label="Main actions">
|
||||||
|
<!-- Bottom left: start break now -->
|
||||||
|
...
|
||||||
|
<!-- Bottom center: stats -->
|
||||||
|
...
|
||||||
|
<!-- Bottom right: settings -->
|
||||||
|
...
|
||||||
|
</nav>
|
||||||
|
```
|
||||||
|
|
||||||
|
The three `<div class="absolute bottom-5 ...">` blocks move inside the `<nav>`.
|
||||||
|
|
||||||
|
**Step 3: Make natural break toast persistent on hover**
|
||||||
|
|
||||||
|
Replace the simple timeout logic (lines 118–126) with hover-aware persistence:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
let toastHovering = $state(false);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if ($timer.naturalBreakOccurred) {
|
||||||
|
showNaturalBreakToast = true;
|
||||||
|
if (naturalBreakToastTimeout) clearTimeout(naturalBreakToastTimeout);
|
||||||
|
naturalBreakToastTimeout = setTimeout(() => {
|
||||||
|
if (!toastHovering) showNaturalBreakToast = false;
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
On the toast div (line 266), add:
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
onmouseenter={() => toastHovering = true}
|
||||||
|
onmouseleave={() => { toastHovering = false; showNaturalBreakToast = false; }}
|
||||||
|
onfocusin={() => toastHovering = true}
|
||||||
|
onfocusout={() => { toastHovering = false; showNaturalBreakToast = false; }}
|
||||||
|
```
|
||||||
|
|
||||||
|
Add a close button inside the toast and an Escape key handler.
|
||||||
|
|
||||||
|
**Step 4: Add progressbar role to daily goal bar**
|
||||||
|
|
||||||
|
On the goal progress bar div (line 235), add ARIA:
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<div class="w-16 h-[2px] rounded-full overflow-hidden" style="background: #161616;"
|
||||||
|
role="progressbar"
|
||||||
|
aria-label="Daily goal progress"
|
||||||
|
aria-valuemin={0}
|
||||||
|
aria-valuemax={$config.daily_goal_breaks}
|
||||||
|
aria-valuenow={dailyGoalProgress}
|
||||||
|
>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 5: Add sr-only text for pomodoro dots**
|
||||||
|
|
||||||
|
After the pomodoro dots visual `<div>` (around line 192), add:
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<span class="sr-only">
|
||||||
|
Pomodoro cycle: session {$timer.pomodoroCyclePosition + 1} of {$timer.pomodoroTotalInCycle}
|
||||||
|
</span>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 6: Verify build**
|
||||||
|
|
||||||
|
Run: `cd break-timer && npm run build`
|
||||||
|
|
||||||
|
**Step 7: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add break-timer/src/lib/components/Dashboard.svelte
|
||||||
|
git commit -m "a11y: dashboard contrast, nav landmark, toast persistence, goal progressbar"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 8: Settings — Part 1: Headings & Structure (`Settings.svelte`)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `break-timer/src/lib/components/Settings.svelte`
|
||||||
|
|
||||||
|
This is a large file (1176 lines). Split into two commits for reviewability.
|
||||||
|
|
||||||
|
**Step 1: Change all `<h3>` to `<h2>`**
|
||||||
|
|
||||||
|
Replace every `<h3 class="mb-4 text-[11px] ...` with `<h2 class="mb-4 text-[11px] ...` and corresponding `</h3>` with `</h2>`. There are 17 instances at approximate lines: 163, 218, 280, 346, 435, 444, 498, 590, 668, 732, 825, 862, 906, 976(Working Hours has no heading — add one), 1078, 1119, 1138.
|
||||||
|
|
||||||
|
**Step 2: Add `id` to headings and `aria-labelledby` to sections**
|
||||||
|
|
||||||
|
Each `<section>` gets an `aria-labelledby` pointing to its heading's `id`. Pattern:
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<section aria-labelledby="settings-timer" class="rounded-2xl p-5 ...">
|
||||||
|
<h2 id="settings-timer" class="mb-4 ...">Timer</h2>
|
||||||
|
```
|
||||||
|
|
||||||
|
Do this for all 18 sections. Use ids: `settings-timer`, `settings-pomodoro`, `settings-microbreaks`, `settings-breakscreen`, `settings-activities`, `settings-breathing`, `settings-behavior`, `settings-alerts`, `settings-sound`, `settings-idle`, `settings-presentation`, `settings-goals`, `settings-appearance`, `settings-workinghours`, `settings-minimode`, `settings-general`, `settings-shortcuts`.
|
||||||
|
|
||||||
|
**Step 3: Add missing heading for Working Hours section**
|
||||||
|
|
||||||
|
The Working Hours section (line 975) has no `<h3>`/`<h2>`. Add:
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<h2 id="settings-workinghours" class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
|
||||||
|
Working Hours
|
||||||
|
</h2>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 4: Verify build**
|
||||||
|
|
||||||
|
Run: `cd break-timer && npm run build`
|
||||||
|
|
||||||
|
**Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add break-timer/src/lib/components/Settings.svelte
|
||||||
|
git commit -m "a11y: settings heading hierarchy (h3→h2) and section landmarks"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 9: Settings — Part 2: ARIA, Contrast, Sizes (`Settings.svelte`)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `break-timer/src/lib/components/Settings.svelte`
|
||||||
|
|
||||||
|
**Step 1: Breathing pattern — radiogroup/radio**
|
||||||
|
|
||||||
|
Wrap the breathing pattern buttons (line 464) in a `role="radiogroup"` container:
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<div role="radiogroup" aria-label="Breathing pattern" class="flex flex-col gap-1.5">
|
||||||
|
```
|
||||||
|
|
||||||
|
Each breathing pattern button gets `role="radio"` and `aria-checked`:
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<button
|
||||||
|
role="radio"
|
||||||
|
aria-checked={$config.breathing_pattern === bp.id}
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Sound preset — aria-pressed**
|
||||||
|
|
||||||
|
Each sound preset button (line 709) gets `aria-pressed`:
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<button
|
||||||
|
aria-pressed={$config.sound_preset === preset}
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Replace hardcoded colors**
|
||||||
|
|
||||||
|
- All `text-[#8a8a8a]` → `text-text-sec` (many instances throughout)
|
||||||
|
- All `bg-[#161616]` dividers → `bg-border` (use `--color-border` token)
|
||||||
|
- All `border-[#161616]` on inputs → `border-border`
|
||||||
|
- All `placeholder:text-[#2a2a2a]` → `placeholder:text-[#555]` (3.37:1)
|
||||||
|
- All `bg-[#141414]` → use updated stepper component (already handled in Task 5)
|
||||||
|
- Back button `h-8 w-8` → `h-10 w-10 min-h-[44px] min-w-[44px]` (line 127)
|
||||||
|
|
||||||
|
**Step 4: Reset button aria-live**
|
||||||
|
|
||||||
|
Wrap the reset button text in an `aria-live="polite"` region, or add `aria-live="polite"` to the button itself so screen readers announce the confirmation state change:
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<button
|
||||||
|
aria-live="polite"
|
||||||
|
...
|
||||||
|
>
|
||||||
|
{resetConfirming ? "Tap again to confirm reset" : "Reset to defaults"}
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 5: Add abbreviation tags**
|
||||||
|
|
||||||
|
For standalone unit abbreviations in setting descriptions, wrap with `<abbr>`:
|
||||||
|
|
||||||
|
- `"s"` (seconds) → `<abbr title="seconds">s</abbr>`
|
||||||
|
- `"min"` → `<abbr title="minutes">min</abbr>`
|
||||||
|
|
||||||
|
Apply on first occurrence in: alert timing description (line 613), idle timeout (line 755), snooze duration (line 537).
|
||||||
|
|
||||||
|
**Step 6: Add title tooltips on complex settings**
|
||||||
|
|
||||||
|
Add `title` attributes on the section-level labels for Pomodoro, Smart breaks, Microbreaks, Breathing:
|
||||||
|
|
||||||
|
- Pomodoro `<h2>`: `title="Pomodoro technique alternates focused work sessions with short and long breaks"`
|
||||||
|
- Microbreaks `<h2>`: `title="20-20-20 rule: every 20 minutes, look 20 feet away for 20 seconds"`
|
||||||
|
- Smart breaks `<div>`: `title="Automatically counts time away from computer as a break"`
|
||||||
|
- Breathing guide `<h2>`: `title="Visual breathing exercise during breaks to reduce stress"`
|
||||||
|
|
||||||
|
**Step 7: Verify build**
|
||||||
|
|
||||||
|
Run: `cd break-timer && npm run build`
|
||||||
|
|
||||||
|
**Step 8: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add break-timer/src/lib/components/Settings.svelte
|
||||||
|
git commit -m "a11y: settings ARIA patterns, contrast, abbreviations, and tooltips"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 10: StatsView — Tabs & Data Tables (`StatsView.svelte`)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `break-timer/src/lib/components/StatsView.svelte`
|
||||||
|
|
||||||
|
**Step 1: Change all `<h3>` to `<h2>`**
|
||||||
|
|
||||||
|
Replace all `<h3>` with `<h2>` and `</h3>` with `</h2>` (~12 instances).
|
||||||
|
|
||||||
|
**Step 2: Implement tablist pattern**
|
||||||
|
|
||||||
|
Replace the tab navigation (lines 297–310):
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<div role="tablist" aria-label="Statistics time range" class="flex gap-1 px-5 mb-3" use:fadeIn={{ duration: 0.3, y: 6 }}>
|
||||||
|
{#each [["today", "Today"], ["weekly", "Weekly"], ["monthly", "Monthly"]] as [tab, label]}
|
||||||
|
<button
|
||||||
|
role="tab"
|
||||||
|
aria-selected={activeTab === tab}
|
||||||
|
aria-controls="tabpanel-{tab}"
|
||||||
|
id="tab-{tab}"
|
||||||
|
use:pressable
|
||||||
|
class="rounded-lg px-4 py-2 min-h-[44px] text-[11px] tracking-wider uppercase transition-all duration-200
|
||||||
|
{activeTab === tab
|
||||||
|
? 'bg-[#1a1a1a] text-white'
|
||||||
|
: 'text-text-sec hover:text-white'}"
|
||||||
|
onclick={() => activeTab = tab as any}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Wrap each tab's content in a `role="tabpanel"`:
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
{#if activeTab === "today"}
|
||||||
|
<div role="tabpanel" id="tabpanel-today" aria-labelledby="tab-today">
|
||||||
|
...
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Same for `weekly` and `monthly`.
|
||||||
|
|
||||||
|
**Step 3: Add sr-only data tables for 30-day chart and heatmap**
|
||||||
|
|
||||||
|
After the 30-day chart canvas (around line 533), add:
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
{#if monthHistory.length > 0}
|
||||||
|
<table class="sr-only">
|
||||||
|
<caption>Break history for the last {monthHistory.length} days</caption>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Date</th><th>Completed</th><th>Skipped</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each monthHistory as day}
|
||||||
|
<tr><td>{day.date}</td><td>{day.breaksCompleted}</td><td>{day.breaksSkipped}</td></tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{/if}
|
||||||
|
```
|
||||||
|
|
||||||
|
After the heatmap canvas (around line 559), add:
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
{#if monthHistory.length > 0}
|
||||||
|
<table class="sr-only">
|
||||||
|
<caption>Activity heatmap for the last {monthHistory.length} days</caption>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Date</th><th>Breaks completed</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each monthHistory as day}
|
||||||
|
<tr><td>{day.date}</td><td>{day.breaksCompleted}</td></tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{/if}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 4: Replace hardcoded colors**
|
||||||
|
|
||||||
|
- All `text-[#8a8a8a]` → `text-text-sec`
|
||||||
|
- `bg-[#161616]` dividers → `bg-border`
|
||||||
|
- Back button: `h-8 w-8` → `h-10 w-10 min-h-[44px] min-w-[44px]`
|
||||||
|
|
||||||
|
**Step 5: Verify build**
|
||||||
|
|
||||||
|
Run: `cd break-timer && npm run build`
|
||||||
|
|
||||||
|
**Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add break-timer/src/lib/components/StatsView.svelte
|
||||||
|
git commit -m "a11y: stats tablist pattern, sr-only data tables, contrast updates"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 11: BreakScreen (`BreakScreen.svelte`)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `break-timer/src/lib/components/BreakScreen.svelte`
|
||||||
|
|
||||||
|
**Step 1: Add strict mode focus safety**
|
||||||
|
|
||||||
|
When `strict_mode` is true and buttons are hidden, the focus trap has zero focusable elements. After the `{#if showButtons}` block (around line 353 for in-app, line 203 for standalone), add an else block:
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
{:else}
|
||||||
|
<span tabindex="0" class="sr-only" aria-live="polite">
|
||||||
|
Break in progress, please wait
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
```
|
||||||
|
|
||||||
|
This ensures the focus trap always has at least one focusable element.
|
||||||
|
|
||||||
|
**Step 2: Fix breathing aria-live to only announce phase changes**
|
||||||
|
|
||||||
|
The breathing guide `<span aria-live="polite">` (lines 172 and 304) currently announces every countdown tick. Add a tracked variable that only updates on phase change:
|
||||||
|
|
||||||
|
In the script section, add:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
let lastAnnouncedPhase = $state("");
|
||||||
|
let breathAnnouncement = $derived(
|
||||||
|
breathPhase !== lastAnnouncedPhase
|
||||||
|
? (() => { lastAnnouncedPhase = breathPhase; return `${breathPhase}`; })()
|
||||||
|
: ""
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Actually, better approach — use a separate `$effect` and a dedicated announcement state:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
let breathAnnouncement = $state("");
|
||||||
|
$effect(() => {
|
||||||
|
// Only announce when the breathing phase name changes, not countdown ticks
|
||||||
|
breathAnnouncement = breathPhase;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Then change the aria-live span to use a separate invisible span for SR announcements:
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<span class="sr-only" aria-live="polite" aria-atomic="true">{breathAnnouncement}</span>
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep the visible breathing text without `aria-live` (remove `aria-live="polite"` from the visible span).
|
||||||
|
|
||||||
|
**Step 3: Replace hardcoded colors**
|
||||||
|
|
||||||
|
- `text-[#8a8a8a]` → `text-text-sec`
|
||||||
|
- `border-[#222]` → `border-border`
|
||||||
|
|
||||||
|
**Step 4: Verify build**
|
||||||
|
|
||||||
|
Run: `cd break-timer && npm run build`
|
||||||
|
|
||||||
|
**Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add break-timer/src/lib/components/BreakScreen.svelte
|
||||||
|
git commit -m "a11y: break screen strict-mode focus safety and breathing phase announcements"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 12: Celebration Toast Persistence (`Celebration.svelte`)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `break-timer/src/lib/components/Celebration.svelte`
|
||||||
|
|
||||||
|
**Step 1: Add hover/focus-aware auto-dismiss**
|
||||||
|
|
||||||
|
Replace the CSS-driven 3.5s fade with JS-controlled timing. Add state:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
let milestoneHovering = $state(false);
|
||||||
|
let goalHovering = $state(false);
|
||||||
|
```
|
||||||
|
|
||||||
|
For the `.celebration-overlay` div, add:
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
onmouseenter={() => milestoneHovering = true}
|
||||||
|
onmouseleave={() => milestoneHovering = false}
|
||||||
|
onfocusin={() => milestoneHovering = true}
|
||||||
|
onfocusout={() => milestoneHovering = false}
|
||||||
|
```
|
||||||
|
|
||||||
|
Remove `pointer-events: none` from `.celebration-overlay` and `.goal-overlay` styles.
|
||||||
|
|
||||||
|
Add `$effect` blocks that manage visibility based on hover state — when not hovering, set a timeout to add a CSS class that triggers fade-out.
|
||||||
|
|
||||||
|
Add a close button (sr-only label "Dismiss notification") to both overlays, and an Escape key handler.
|
||||||
|
|
||||||
|
**Step 2: Verify build**
|
||||||
|
|
||||||
|
Run: `cd break-timer && npm run build`
|
||||||
|
|
||||||
|
**Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add break-timer/src/lib/components/Celebration.svelte
|
||||||
|
git commit -m "a11y: celebration overlays persist on hover/focus with dismiss controls"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 13: MiniTimer Contrast (`MiniTimer.svelte`)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `break-timer/src/lib/components/MiniTimer.svelte`
|
||||||
|
|
||||||
|
**Step 1: Fix paused text contrast**
|
||||||
|
|
||||||
|
Change line 338: `color: {state === 'paused' ? '#555' : '#fff'}` to `color: {state === 'paused' ? '#a8a8a8' : '#fff'}`.
|
||||||
|
|
||||||
|
Replace `#8a8a8a` with the text-sec token value `#a8a8a8` on line 344 for pomodoro indicator.
|
||||||
|
|
||||||
|
**Step 2: Verify build**
|
||||||
|
|
||||||
|
Run: `cd break-timer && npm run build`
|
||||||
|
|
||||||
|
**Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add break-timer/src/lib/components/MiniTimer.svelte
|
||||||
|
git commit -m "a11y: fix mini timer paused text contrast for AAA"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 14: MicrobreakOverlay (`MicrobreakOverlay.svelte`)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `break-timer/src/lib/components/MicrobreakOverlay.svelte`
|
||||||
|
|
||||||
|
**Step 1: Add alertdialog role, heading, and label**
|
||||||
|
|
||||||
|
Change the outer `.microbreak-card` div (line 41):
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<div class="microbreak-card" role="alertdialog" aria-label="Microbreak" aria-describedby="microbreak-msg">
|
||||||
|
<h2 class="sr-only">Microbreak</h2>
|
||||||
|
<div class="flex items-center gap-3 mb-2">
|
||||||
|
...
|
||||||
|
<span id="microbreak-msg" class="text-[15px] font-medium text-white">
|
||||||
|
Look away — 20 feet for {timeRemaining}s
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `text-[#8a8a8a]` with `text-text-sec`.
|
||||||
|
|
||||||
|
**Step 2: Verify build**
|
||||||
|
|
||||||
|
Run: `cd break-timer && npm run build`
|
||||||
|
|
||||||
|
**Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add break-timer/src/lib/components/MicrobreakOverlay.svelte
|
||||||
|
git commit -m "a11y: microbreak overlay gets alertdialog role and heading"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 15: BreakOverlay (`BreakOverlay.svelte`)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `break-timer/src/lib/components/BreakOverlay.svelte`
|
||||||
|
|
||||||
|
**Step 1: Add alertdialog role, heading, and label**
|
||||||
|
|
||||||
|
Change the outer div (line 39):
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<div
|
||||||
|
role="alertdialog"
|
||||||
|
aria-label="Break in progress"
|
||||||
|
class="fixed inset-0 flex flex-col items-center justify-center"
|
||||||
|
style="background: rgba(0, 0, 0, {$config.backdrop_opacity});"
|
||||||
|
>
|
||||||
|
<h2 class="sr-only">Break in Progress</h2>
|
||||||
|
<p class="text-[16px] font-medium text-white mb-4">Break in progress</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Verify build**
|
||||||
|
|
||||||
|
Run: `cd break-timer && npm run build`
|
||||||
|
|
||||||
|
**Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add break-timer/src/lib/components/BreakOverlay.svelte
|
||||||
|
git commit -m "a11y: break overlay gets alertdialog role and heading"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 16: ColorPicker (`ColorPicker.svelte`)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `break-timer/src/lib/components/ColorPicker.svelte`
|
||||||
|
|
||||||
|
**Step 1: Enlarge swatches and add aria-pressed**
|
||||||
|
|
||||||
|
Change swatch buttons (line 247) from `h-[22px] w-[22px]` to `h-[28px] w-[28px]` with a `min-h-[44px] min-w-[44px]` clickable area (via padding or wrapper).
|
||||||
|
|
||||||
|
Add `aria-pressed={value === color}` to each swatch button.
|
||||||
|
|
||||||
|
Change the flex container gap from `gap-[6px]` to `gap-[8px]` to accommodate larger swatches.
|
||||||
|
|
||||||
|
Replace `text-[#8a8a8a]` with `text-text-sec`.
|
||||||
|
|
||||||
|
**Step 2: Verify build**
|
||||||
|
|
||||||
|
Run: `cd break-timer && npm run build`
|
||||||
|
|
||||||
|
**Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add break-timer/src/lib/components/ColorPicker.svelte
|
||||||
|
git commit -m "a11y: enlarge color swatches, add aria-pressed"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 17: ActivityManager Target Sizes (`ActivityManager.svelte`)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `break-timer/src/lib/components/ActivityManager.svelte`
|
||||||
|
|
||||||
|
**Step 1: Enlarge star/remove buttons**
|
||||||
|
|
||||||
|
Find all favorite (★) and remove (✕) buttons. Change from any `w-8 h-8` / `w-7 h-7` to `w-9 h-9 min-h-[44px] min-w-[44px]`.
|
||||||
|
|
||||||
|
Replace `text-[#8a8a8a]` with `text-text-sec`.
|
||||||
|
|
||||||
|
**Step 2: Verify build**
|
||||||
|
|
||||||
|
Run: `cd break-timer && npm run build`
|
||||||
|
|
||||||
|
**Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add break-timer/src/lib/components/ActivityManager.svelte
|
||||||
|
git commit -m "a11y: enlarge activity manager action buttons to 44px targets"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 18: Final Build Verification & Cleanup
|
||||||
|
|
||||||
|
**Step 1: Full build**
|
||||||
|
|
||||||
|
Run: `cd break-timer && npm run build`
|
||||||
|
Expected: Build succeeds with zero errors.
|
||||||
|
|
||||||
|
**Step 2: Verify all theme token propagation**
|
||||||
|
|
||||||
|
Search for any remaining hardcoded `#8a8a8a` in Svelte files — there should be none (all replaced with `text-text-sec` or inline `#a8a8a8`).
|
||||||
|
|
||||||
|
Run: `grep -r "#8a8a8a" break-timer/src/` — should return zero results.
|
||||||
|
|
||||||
|
**Step 3: Verify no remaining h3 headings in Settings/Stats**
|
||||||
|
|
||||||
|
Run: `grep -n "<h3" break-timer/src/lib/components/Settings.svelte break-timer/src/lib/components/StatsView.svelte` — should return zero results.
|
||||||
|
|
||||||
|
**Step 4: Final commit if any cleanup needed**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add -A break-timer/src/
|
||||||
|
git commit -m "a11y: final cleanup pass for WCAG 2.2 AAA compliance"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependency Graph
|
||||||
|
|
||||||
|
```
|
||||||
|
Task 1 (app.css tokens)
|
||||||
|
├── Task 2 (App.svelte)
|
||||||
|
├── Task 3 (Titlebar)
|
||||||
|
├── Task 4 (ToggleSwitch)
|
||||||
|
├── Task 5 (Stepper)
|
||||||
|
├── Task 6 (animate.ts)
|
||||||
|
├── Task 7 (Dashboard) ← depends on Task 6
|
||||||
|
├── Task 8 (Settings part 1)
|
||||||
|
├── Task 9 (Settings part 2) ← depends on Task 8
|
||||||
|
├── Task 10 (StatsView)
|
||||||
|
├── Task 11 (BreakScreen)
|
||||||
|
├── Task 12 (Celebration)
|
||||||
|
├── Task 13 (MiniTimer)
|
||||||
|
├── Task 14 (MicrobreakOverlay)
|
||||||
|
├── Task 15 (BreakOverlay)
|
||||||
|
├── Task 16 (ColorPicker)
|
||||||
|
└── Task 17 (ActivityManager)
|
||||||
|
Task 18 (Final verification) ← depends on all above
|
||||||
|
```
|
||||||
|
|
||||||
|
Tasks 2–17 are mostly independent of each other (except 7 depends on 6, and 9 depends on 8). They all depend on Task 1 being done first.
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "core-cooldown",
|
"name": "core-cooldown",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.3",
|
"version": "0.2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@@ -480,7 +480,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-cooldown"
|
name = "core-cooldown"
|
||||||
version = "0.1.2"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "core-cooldown"
|
name = "core-cooldown"
|
||||||
version = "0.1.3"
|
version = "0.2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
|
|||||||
@@ -9,17 +9,70 @@ fn main() {
|
|||||||
|
|
||||||
// On GNU targets, replace the WebView2Loader import library with the static
|
// On GNU targets, replace the WebView2Loader import library with the static
|
||||||
// library so the loader is baked into the exe — no DLL to ship.
|
// library so the loader is baked into the exe — no DLL to ship.
|
||||||
#[cfg(target_env = "gnu")]
|
if std::env::var("CARGO_CFG_TARGET_ENV").as_deref() == Ok("gnu") {
|
||||||
swap_webview2_to_static();
|
swap_webview2_to_static();
|
||||||
|
}
|
||||||
|
|
||||||
tauri_build::build()
|
tauri_build::build();
|
||||||
|
|
||||||
|
// When targeting GNU, embed-resource may find MSVC's rc.exe (via Windows
|
||||||
|
// SDK) and produce a .res file instead of COFF .o. GNU ld can't link .res
|
||||||
|
// files. Fix: re-compile with windres if needed.
|
||||||
|
// Note: cfg() in build scripts checks the HOST, not target. Use env var.
|
||||||
|
if std::env::var("CARGO_CFG_TARGET_ENV").as_deref() == Ok("gnu") {
|
||||||
|
fix_resource_lib();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If resource.lib is in .res format (produced by MSVC rc.exe), re-compile
|
||||||
|
/// the .rc source with MinGW windres to produce a COFF object that GNU ld
|
||||||
|
/// can link.
|
||||||
|
fn fix_resource_lib() {
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
let out_dir = std::env::var("OUT_DIR").unwrap_or_default();
|
||||||
|
let out_path = PathBuf::from(&out_dir);
|
||||||
|
let rc_file = out_path.join("resource.rc");
|
||||||
|
let lib_file = out_path.join("resource.lib");
|
||||||
|
|
||||||
|
if !rc_file.exists() || !lib_file.exists() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the file is already COFF (starts with COFF machine type or
|
||||||
|
// archive signature). A .res file starts with 0x00000000.
|
||||||
|
if let Ok(header) = std::fs::read(&lib_file) {
|
||||||
|
if header.len() >= 4 && header[0..4] == [0, 0, 0, 0] {
|
||||||
|
// This is a .res file, not COFF — re-compile with windres
|
||||||
|
let windres = "C:/Users/lashman/mingw-w64/mingw64/bin/windres.exe";
|
||||||
|
let status = Command::new(windres)
|
||||||
|
.args([
|
||||||
|
"-i", &rc_file.to_string_lossy(),
|
||||||
|
"-o", &lib_file.to_string_lossy(),
|
||||||
|
"--output-format=coff",
|
||||||
|
])
|
||||||
|
.status();
|
||||||
|
|
||||||
|
match status {
|
||||||
|
Ok(s) if s.success() => {
|
||||||
|
println!("cargo:warning=Re-compiled resource.rc with windres (COFF output)");
|
||||||
|
}
|
||||||
|
Ok(s) => {
|
||||||
|
println!("cargo:warning=windres failed with exit code: {}", s);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("cargo:warning=Failed to run windres: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replace `WebView2Loader.dll.lib` (dynamic import lib) with the contents of
|
/// Replace `WebView2Loader.dll.lib` (dynamic import lib) with the contents of
|
||||||
/// `WebView2LoaderStatic.lib` (static archive) in the webview2-com-sys build
|
/// `WebView2LoaderStatic.lib` (static archive) in the webview2-com-sys build
|
||||||
/// output. The linker then statically links the WebView2 loader code, removing
|
/// output. The linker then statically links the WebView2 loader code, removing
|
||||||
/// the runtime dependency on WebView2Loader.dll.
|
/// the runtime dependency on WebView2Loader.dll.
|
||||||
#[cfg(target_env = "gnu")]
|
|
||||||
fn swap_webview2_to_static() {
|
fn swap_webview2_to_static() {
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-config-schema/schema.json",
|
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-config-schema/schema.json",
|
||||||
"productName": "Core Cooldown",
|
"productName": "Core Cooldown",
|
||||||
"version": "0.1.3",
|
"version": "0.2.0",
|
||||||
"identifier": "com.corecooldown.app",
|
"identifier": "com.corecooldown.app",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../dist",
|
"frontendDist": "../dist",
|
||||||
|
|||||||
@@ -75,6 +75,17 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// WCAG 2.4.2: Document title reflects current view
|
||||||
|
$effect(() => {
|
||||||
|
const viewNames: Record<string, string> = {
|
||||||
|
dashboard: "Dashboard",
|
||||||
|
breakScreen: "Break",
|
||||||
|
settings: "Settings",
|
||||||
|
stats: "Statistics",
|
||||||
|
};
|
||||||
|
document.title = `Core Cooldown — ${viewNames[effectiveView] ?? "Dashboard"}`;
|
||||||
|
});
|
||||||
|
|
||||||
// When fullscreen_mode is OFF, the separate break window handles breaks,
|
// When fullscreen_mode is OFF, the separate break window handles breaks,
|
||||||
// so the main window should keep showing whatever view it was on (dashboard).
|
// so the main window should keep showing whatever view it was on (dashboard).
|
||||||
const effectiveView = $derived(
|
const effectiveView = $derived(
|
||||||
@@ -85,11 +96,13 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main class="relative h-full bg-black">
|
<main class="relative h-full bg-black">
|
||||||
|
<a href="#main-content" class="skip-link">Skip to content</a>
|
||||||
{#if $config.background_blobs_enabled}
|
{#if $config.background_blobs_enabled}
|
||||||
<BackgroundBlobs accentColor={$config.accent_color} breakColor={$config.break_color} />
|
<BackgroundBlobs accentColor={$config.accent_color} breakColor={$config.break_color} />
|
||||||
{/if}
|
{/if}
|
||||||
<Titlebar />
|
<Titlebar />
|
||||||
<div
|
<div
|
||||||
|
id="main-content"
|
||||||
class="relative h-full overflow-hidden origin-top-left"
|
class="relative h-full overflow-hidden origin-top-left"
|
||||||
style="
|
style="
|
||||||
transform: scale({zoomScale});
|
transform: scale({zoomScale});
|
||||||
|
|||||||
36
src/app.css
36
src/app.css
@@ -5,17 +5,19 @@
|
|||||||
--color-surface: #0e0e0e;
|
--color-surface: #0e0e0e;
|
||||||
--color-card: #141414;
|
--color-card: #141414;
|
||||||
--color-card-lt: #1c1c1c;
|
--color-card-lt: #1c1c1c;
|
||||||
--color-border: #222222;
|
--color-border: #3a3a3a;
|
||||||
--color-accent: #ff4d00;
|
--color-accent: #ff4d00;
|
||||||
--color-accent-lt: #ff7733;
|
--color-accent-lt: #ff7733;
|
||||||
--color-accent-dim: #ff4d0018;
|
--color-accent-dim: #ff4d0018;
|
||||||
--color-accent-glow: #ff4d0040;
|
--color-accent-glow: #ff4d0040;
|
||||||
--color-success: #3fb950;
|
--color-success: #3fb950;
|
||||||
--color-warning: #f0a500;
|
--color-warning: #f0a500;
|
||||||
--color-danger: #f85149;
|
--color-danger: #ff6b6b;
|
||||||
--color-text-pri: #ffffff;
|
--color-text-pri: #ffffff;
|
||||||
--color-text-sec: #8a8a8a;
|
--color-text-sec: #a8a8a8;
|
||||||
--color-text-dim: #3a3a3a;
|
--color-text-dim: #5c5c5c;
|
||||||
|
--color-input-border: #444444;
|
||||||
|
--color-surface-lt: #1e1e1e;
|
||||||
--color-caption-bg: #050505;
|
--color-caption-bg: #050505;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,6 +32,11 @@ body {
|
|||||||
Arial, sans-serif;
|
Arial, sans-serif;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
line-height: 1.625;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-tauri-drag-region],
|
||||||
|
[data-tauri-drag-region] * {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
}
|
}
|
||||||
@@ -69,10 +76,31 @@ body {
|
|||||||
border-width: 0;
|
border-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Accessibility: Skip link ── */
|
||||||
|
.skip-link {
|
||||||
|
position: absolute;
|
||||||
|
top: -100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 10000;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: var(--color-accent);
|
||||||
|
color: #000;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 0 0 8px 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: top 0.15s ease;
|
||||||
|
}
|
||||||
|
.skip-link:focus {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Accessibility: Focus indicators ── */
|
/* ── Accessibility: Focus indicators ── */
|
||||||
:focus-visible {
|
:focus-visible {
|
||||||
outline: 2px solid var(--color-accent);
|
outline: 2px solid var(--color-accent);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
|
box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Accessibility: Reduced motion ── */
|
/* ── Accessibility: Reduced motion ── */
|
||||||
|
|||||||
@@ -409,7 +409,7 @@
|
|||||||
<button
|
<button
|
||||||
bind:this={dropdownTriggerRef}
|
bind:this={dropdownTriggerRef}
|
||||||
use:pressable
|
use:pressable
|
||||||
class="flex items-center gap-1.5 rounded-xl border border-[#161616] bg-black px-3 py-2.5 text-[13px] text-[#8a8a8a]
|
class="flex items-center gap-1.5 rounded-xl border border-[#161616] bg-black px-3 py-2.5 text-[13px] text-text-sec
|
||||||
hover:border-[#333] hover:text-white transition-colors"
|
hover:border-[#333] hover:text-white transition-colors"
|
||||||
onclick={() => { dropdownOpen = !dropdownOpen; }}
|
onclick={() => { dropdownOpen = !dropdownOpen; }}
|
||||||
aria-haspopup="listbox"
|
aria-haspopup="listbox"
|
||||||
@@ -422,12 +422,12 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
{#if dropdownOpen}
|
{#if dropdownOpen}
|
||||||
<div class="absolute top-full left-0 mt-1 z-50 min-w-[140px] rounded-xl border border-[#222] bg-[#111] shadow-xl shadow-black/50 overflow-hidden"
|
<div class="absolute top-full left-0 mt-1 z-50 min-w-[140px] rounded-xl border border-border bg-[#111] shadow-xl shadow-black/50 overflow-hidden"
|
||||||
role="listbox" aria-label="Activity category">
|
role="listbox" aria-label="Activity category">
|
||||||
{#each categories as cat, i}
|
{#each categories as cat, i}
|
||||||
<button
|
<button
|
||||||
class="w-full text-left px-3.5 py-2.5 text-[13px] transition-colors outline-none
|
class="w-full text-left px-3.5 py-2.5 text-[13px] transition-colors outline-none
|
||||||
{newActivityCategory === cat ? 'text-white bg-[#1a1a1a]' : 'text-[#8a8a8a] hover:bg-[#1a1a1a] hover:text-white'}
|
{newActivityCategory === cat ? 'text-white bg-[#1a1a1a]' : 'text-text-sec hover:bg-[#1a1a1a] hover:text-white'}
|
||||||
{focusedOptionIndex === i ? 'bg-[#1a1a1a] text-white' : ''}"
|
{focusedOptionIndex === i ? 'bg-[#1a1a1a] text-white' : ''}"
|
||||||
role="option"
|
role="option"
|
||||||
aria-selected={newActivityCategory === cat}
|
aria-selected={newActivityCategory === cat}
|
||||||
@@ -442,7 +442,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
use:pressable
|
use:pressable
|
||||||
class="flex items-center justify-center w-10 h-10 rounded-xl border border-[#161616] text-[18px] text-[#8a8a8a]
|
class="flex items-center justify-center w-10 h-10 rounded-xl border border-[#161616] text-[18px] text-text-sec
|
||||||
hover:border-[#333] hover:text-white transition-colors disabled:opacity-30"
|
hover:border-[#333] hover:text-white transition-colors disabled:opacity-30"
|
||||||
onclick={addCustomActivity}
|
onclick={addCustomActivity}
|
||||||
disabled={!newActivityText.trim()}
|
disabled={!newActivityText.trim()}
|
||||||
@@ -463,13 +463,13 @@
|
|||||||
class="rounded-xl px-3 py-2 text-[11px] tracking-wider transition-all duration-200
|
class="rounded-xl px-3 py-2 text-[11px] tracking-wider transition-all duration-200
|
||||||
{isExpanded
|
{isExpanded
|
||||||
? 'bg-[#1a1a1a] text-white border border-[#333]'
|
? 'bg-[#1a1a1a] text-white border border-[#333]'
|
||||||
: 'bg-[#0a0a0a] text-[#8a8a8a] border border-[#161616] hover:border-[#333] hover:text-white'}"
|
: 'bg-[#0a0a0a] text-text-sec border border-[#161616] hover:border-[#333] hover:text-white'}"
|
||||||
onclick={() => toggleCategory(cat)}
|
onclick={() => toggleCategory(cat)}
|
||||||
aria-expanded={isExpanded}
|
aria-expanded={isExpanded}
|
||||||
aria-controls="activity-panel-{cat}"
|
aria-controls="activity-panel-{cat}"
|
||||||
>
|
>
|
||||||
<span class="uppercase">{getCategoryLabel(cat)}</span>
|
<span class="uppercase">{getCategoryLabel(cat)}</span>
|
||||||
<span class="ml-1.5 text-[10px] {isExpanded ? 'text-[#8a8a8a]' : 'text-[#8a8a8a] opacity-60'}">
|
<span class="ml-1.5 text-[10px] {isExpanded ? 'text-text-sec' : 'text-text-sec opacity-60'}">
|
||||||
{total - disabled}/{total}
|
{total - disabled}/{total}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -486,7 +486,7 @@
|
|||||||
<div class="rounded-xl border border-[#161616] bg-[#0a0a0a] overflow-hidden" use:blockParentDrag>
|
<div class="rounded-xl border border-[#161616] bg-[#0a0a0a] overflow-hidden" use:blockParentDrag>
|
||||||
{#if catCustoms.length > 0}
|
{#if catCustoms.length > 0}
|
||||||
<div class="px-3 pt-2.5 pb-1">
|
<div class="px-3 pt-2.5 pb-1">
|
||||||
<div class="text-[9px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">Custom</div>
|
<div class="text-[9px] font-medium tracking-[0.15em] text-text-sec uppercase">Custom</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -497,17 +497,17 @@
|
|||||||
{#each catCustoms as activity (activity.id)}
|
{#each catCustoms as activity (activity.id)}
|
||||||
<div class="flex items-center gap-1 px-1.5 py-0.5 group hover:bg-[#111]">
|
<div class="flex items-center gap-1 px-1.5 py-0.5 group hover:bg-[#111]">
|
||||||
<button
|
<button
|
||||||
class="flex items-center justify-center min-w-[32px] min-h-[32px] text-[13px] transition-opacity {activity.is_favorite ? 'opacity-100' : 'opacity-30 hover:opacity-60'}"
|
class="flex items-center justify-center w-9 h-9 min-w-[44px] min-h-[44px] text-[13px] transition-opacity {activity.is_favorite ? 'opacity-100' : 'opacity-30 hover:opacity-60'}"
|
||||||
onclick={() => toggleCustomFavorite(activity.id)}
|
onclick={() => toggleCustomFavorite(activity.id)}
|
||||||
aria-label="{activity.is_favorite ? 'Remove from favorites' : 'Add to favorites'}"
|
aria-label="{activity.is_favorite ? 'Remove from favorites' : 'Add to favorites'}"
|
||||||
>
|
>
|
||||||
★
|
★
|
||||||
</button>
|
</button>
|
||||||
<span class="flex-1 text-[12px] {activity.enabled ? 'text-white' : 'text-[#8a8a8a] line-through'}">
|
<span class="flex-1 text-[12px] {activity.enabled ? 'text-white' : 'text-text-sec line-through'}">
|
||||||
{activity.text}
|
{activity.text}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
class="flex items-center justify-center min-w-[32px] min-h-[32px] text-[#8a8a8a] hover:text-[#f85149] opacity-0 group-hover:opacity-100 focus-visible:opacity-100 transition-all text-[11px]"
|
class="flex items-center justify-center w-9 h-9 min-w-[44px] min-h-[44px] text-text-sec hover:text-[#ff6b6b] opacity-0 group-hover:opacity-100 focus-visible:opacity-100 transition-all text-[11px]"
|
||||||
onclick={() => removeCustomActivity(activity.id)}
|
onclick={() => removeCustomActivity(activity.id)}
|
||||||
aria-label="Remove {activity.text}"
|
aria-label="Remove {activity.text}"
|
||||||
>
|
>
|
||||||
@@ -523,20 +523,20 @@
|
|||||||
|
|
||||||
{#if catCustoms.length > 0 && catBuiltins.length > 0}
|
{#if catCustoms.length > 0 && catBuiltins.length > 0}
|
||||||
<div class="px-3 pt-2 pb-1">
|
<div class="px-3 pt-2 pb-1">
|
||||||
<div class="text-[9px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">Built-in</div>
|
<div class="text-[9px] font-medium tracking-[0.15em] text-text-sec uppercase">Built-in</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#each catBuiltins as activity (activity.text)}
|
{#each catBuiltins as activity (activity.text)}
|
||||||
<div class="flex items-center gap-1 px-1.5 py-0.5 hover:bg-[#111]">
|
<div class="flex items-center gap-1 px-1.5 py-0.5 hover:bg-[#111]">
|
||||||
<button
|
<button
|
||||||
class="flex items-center justify-center min-w-[32px] min-h-[32px] text-[13px] transition-opacity {isBuiltinFavorite(activity.text) ? 'opacity-100' : 'opacity-30 hover:opacity-60'}"
|
class="flex items-center justify-center w-9 h-9 min-w-[44px] min-h-[44px] text-[13px] transition-opacity {isBuiltinFavorite(activity.text) ? 'opacity-100' : 'opacity-30 hover:opacity-60'}"
|
||||||
onclick={() => toggleBuiltinFavorite(activity.text)}
|
onclick={() => toggleBuiltinFavorite(activity.text)}
|
||||||
aria-label="{isBuiltinFavorite(activity.text) ? 'Remove from favorites' : 'Add to favorites'}"
|
aria-label="{isBuiltinFavorite(activity.text) ? 'Remove from favorites' : 'Add to favorites'}"
|
||||||
>
|
>
|
||||||
★
|
★
|
||||||
</button>
|
</button>
|
||||||
<span class="flex-1 text-[12px] {!isBuiltinDisabled(activity.text) ? 'text-[#8a8a8a]' : 'text-[#8a8a8a] line-through opacity-60'}">
|
<span class="flex-1 text-[12px] {!isBuiltinDisabled(activity.text) ? 'text-text-sec' : 'text-text-sec line-through opacity-60'}">
|
||||||
{activity.text}
|
{activity.text}
|
||||||
</span>
|
</span>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
|
|||||||
@@ -37,9 +37,12 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
role="alertdialog"
|
||||||
|
aria-label="Break in progress"
|
||||||
class="fixed inset-0 flex flex-col items-center justify-center"
|
class="fixed inset-0 flex flex-col items-center justify-center"
|
||||||
style="background: rgba(0, 0, 0, {$config.backdrop_opacity});"
|
style="background: rgba(0, 0, 0, {$config.backdrop_opacity});"
|
||||||
>
|
>
|
||||||
|
<h2 class="sr-only">Break in Progress</h2>
|
||||||
<p class="text-[16px] font-medium text-white mb-4">Break in progress</p>
|
<p class="text-[16px] font-medium text-white mb-4">Break in progress</p>
|
||||||
|
|
||||||
<span class="text-[42px] font-semibold tabular-nums text-white leading-none mb-6">
|
<span class="text-[42px] font-semibold tabular-nums text-white leading-none mb-6">
|
||||||
|
|||||||
@@ -74,6 +74,18 @@
|
|||||||
let breathCountdown = $state(4);
|
let breathCountdown = $state(4);
|
||||||
let breathScale = $state(0.6);
|
let breathScale = $state(0.6);
|
||||||
|
|
||||||
|
// Only announce phase name changes (not countdown ticks) to screen readers
|
||||||
|
let breathAnnouncement = $state("");
|
||||||
|
let lastBreathPhase = $state("");
|
||||||
|
$effect(() => {
|
||||||
|
// Extract just the phase name (e.g., "Inhale" from "Inhale 4")
|
||||||
|
const phaseName = breathPhase?.split(' ')[0] ?? "";
|
||||||
|
if (phaseName && phaseName !== lastBreathPhase) {
|
||||||
|
lastBreathPhase = phaseName;
|
||||||
|
breathAnnouncement = phaseName;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Map raw 0.6–1.0 scale to 0.9–1.6 range for visible breathing text
|
// Map raw 0.6–1.0 scale to 0.9–1.6 range for visible breathing text
|
||||||
const textScale = $derived(0.9 + (breathScale - 0.6) * (0.7 / 0.4));
|
const textScale = $derived(0.9 + (breathScale - 0.6) * (0.7 / 0.4));
|
||||||
|
|
||||||
@@ -169,11 +181,11 @@
|
|||||||
<span
|
<span
|
||||||
class="block mt-1.5 text-[9px] tracking-wider uppercase text-center font-medium"
|
class="block mt-1.5 text-[9px] tracking-wider uppercase text-center font-medium"
|
||||||
style="transform: scale({textScale}); color: {breathColor}; opacity: {0.5 + breathT * 0.5}; transition: transform 0.15s ease-out, opacity 0.15s ease-out, color 0.15s ease-out;"
|
style="transform: scale({textScale}); color: {breathColor}; opacity: {0.5 + breathT * 0.5}; transition: transform 0.15s ease-out, opacity 0.15s ease-out, color 0.15s ease-out;"
|
||||||
aria-live="polite" aria-atomic="true"
|
aria-hidden="true"
|
||||||
aria-label="Breathing guide: {breathPhase} {breathCountdown > 0 ? breathCountdown + ' seconds' : ''}"
|
|
||||||
>
|
>
|
||||||
{breathPhase}{breathCountdown > 0 ? ` ${breathCountdown}s` : ""}
|
{breathPhase}{breathCountdown > 0 ? ` ${breathCountdown}s` : ""}
|
||||||
</span>
|
</span>
|
||||||
|
<span class="sr-only" aria-live="polite" aria-atomic="true">{breathAnnouncement}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</TimerRing>
|
</TimerRing>
|
||||||
@@ -185,16 +197,16 @@
|
|||||||
<h2 class="text-[17px] font-medium text-white mb-1.5" tabindex="-1">
|
<h2 class="text-[17px] font-medium text-white mb-1.5" tabindex="-1">
|
||||||
{$timer.breakTitle}
|
{$timer.breakTitle}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-[12px] leading-relaxed text-[#8a8a8a] mb-4 max-w-[240px]">
|
<p class="text-[12px] leading-relaxed text-text-sec mb-4 max-w-[240px]">
|
||||||
{$timer.breakMessage}
|
{$timer.breakMessage}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{#if $config.show_break_activities}
|
{#if $config.show_break_activities}
|
||||||
<div class="rounded-xl border border-[#ffffff08] bg-[#ffffff06] px-4 py-2.5 mb-4 max-w-[260px]">
|
<div class="rounded-xl border border-[#ffffff08] bg-[#ffffff06] px-4 py-2.5 mb-4 max-w-[260px]">
|
||||||
<div class="text-[9px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase mb-1">
|
<div class="text-[9px] font-medium tracking-[0.15em] text-text-sec uppercase mb-1">
|
||||||
{getCategoryLabel(currentActivity.category)}
|
{getCategoryLabel(currentActivity.category)}
|
||||||
</div>
|
</div>
|
||||||
<p class="text-[12px] leading-relaxed text-[#8a8a8a]" aria-live="polite">
|
<p class="text-[12px] leading-relaxed text-text-sec" aria-live="polite">
|
||||||
{currentActivity.text}
|
{currentActivity.text}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -205,7 +217,7 @@
|
|||||||
<button
|
<button
|
||||||
use:pressable
|
use:pressable
|
||||||
class="rounded-full border border-[#333] px-5 py-2 text-[11px]
|
class="rounded-full border border-[#333] px-5 py-2 text-[11px]
|
||||||
tracking-wider text-[#8a8a8a] uppercase
|
tracking-wider text-text-sec uppercase
|
||||||
transition-colors duration-200
|
transition-colors duration-200
|
||||||
hover:border-[#444] hover:text-[#ccc]"
|
hover:border-[#444] hover:text-[#ccc]"
|
||||||
onclick={cancelBreak}
|
onclick={cancelBreak}
|
||||||
@@ -226,10 +238,15 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if $config.snooze_limit > 0}
|
{#if $config.snooze_limit > 0}
|
||||||
<p class="mt-2 text-[9px] text-[#8a8a8a]">
|
<p class="mt-2 text-[9px] text-text-sec">
|
||||||
{$timer.snoozesUsed}/{$config.snooze_limit} snoozes used
|
{$timer.snoozesUsed}/{$config.snooze_limit} snoozes used
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||||
|
<span tabindex="0" class="sr-only" aria-live="polite">
|
||||||
|
Break in progress, please wait
|
||||||
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -302,11 +319,11 @@
|
|||||||
class:text-[10px]={!isModal}
|
class:text-[10px]={!isModal}
|
||||||
class:text-[9px]={isModal}
|
class:text-[9px]={isModal}
|
||||||
style="transform: scale({textScale}); color: {breathColor}; opacity: {0.5 + breathT * 0.5}; transition: transform 0.15s ease-out, opacity 0.15s ease-out, color 0.15s ease-out;"
|
style="transform: scale({textScale}); color: {breathColor}; opacity: {0.5 + breathT * 0.5}; transition: transform 0.15s ease-out, opacity 0.15s ease-out, color 0.15s ease-out;"
|
||||||
aria-live="polite" aria-atomic="true"
|
aria-hidden="true"
|
||||||
aria-label="Breathing guide: {breathPhase} {breathCountdown > 0 ? breathCountdown + ' seconds' : ''}"
|
|
||||||
>
|
>
|
||||||
{breathPhase}{breathCountdown > 0 ? ` ${breathCountdown}s` : ""}
|
{breathPhase}{breathCountdown > 0 ? ` ${breathCountdown}s` : ""}
|
||||||
</span>
|
</span>
|
||||||
|
<span class="sr-only" aria-live="polite" aria-atomic="true">{breathAnnouncement}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</TimerRing>
|
</TimerRing>
|
||||||
@@ -328,7 +345,7 @@
|
|||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
class="max-w-[300px] text-center text-[13px] leading-relaxed text-[#8a8a8a]"
|
class="max-w-[300px] text-center text-[13px] leading-relaxed text-text-sec"
|
||||||
class:mb-4={$config.show_break_activities}
|
class:mb-4={$config.show_break_activities}
|
||||||
class:mb-8={!$config.show_break_activities}
|
class:mb-8={!$config.show_break_activities}
|
||||||
use:fadeIn={{ delay: 0.35, y: 10 }}
|
use:fadeIn={{ delay: 0.35, y: 10 }}
|
||||||
@@ -341,10 +358,10 @@
|
|||||||
class="mb-8 mx-auto max-w-[320px] rounded-2xl border border-[#1a1a1a] bg-[#0a0a0a] px-5 py-3.5 text-center"
|
class="mb-8 mx-auto max-w-[320px] rounded-2xl border border-[#1a1a1a] bg-[#0a0a0a] px-5 py-3.5 text-center"
|
||||||
use:fadeIn={{ delay: 0.4, y: 10 }}
|
use:fadeIn={{ delay: 0.4, y: 10 }}
|
||||||
>
|
>
|
||||||
<div class="mb-1.5 text-[10px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
|
<div class="mb-1.5 text-[10px] font-medium tracking-[0.15em] text-text-sec uppercase">
|
||||||
{getCategoryLabel(currentActivity.category)}
|
{getCategoryLabel(currentActivity.category)}
|
||||||
</div>
|
</div>
|
||||||
<p class="text-[13px] leading-relaxed text-[#8a8a8a]" aria-live="polite">
|
<p class="text-[13px] leading-relaxed text-text-sec" aria-live="polite">
|
||||||
{currentActivity.text}
|
{currentActivity.text}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -354,8 +371,8 @@
|
|||||||
<div class="flex items-center gap-3" use:fadeIn={{ delay: 0.45, y: 10 }}>
|
<div class="flex items-center gap-3" use:fadeIn={{ delay: 0.45, y: 10 }}>
|
||||||
<button
|
<button
|
||||||
use:pressable
|
use:pressable
|
||||||
class="rounded-full border border-[#222] px-6 py-2.5 text-[12px]
|
class="rounded-full border border-border px-6 py-2.5 text-[12px]
|
||||||
tracking-wider text-[#8a8a8a] uppercase
|
tracking-wider text-text-sec uppercase
|
||||||
transition-colors duration-200
|
transition-colors duration-200
|
||||||
hover:border-[#333] hover:text-[#ccc]"
|
hover:border-[#333] hover:text-[#ccc]"
|
||||||
onclick={cancelBreak}
|
onclick={cancelBreak}
|
||||||
@@ -376,10 +393,15 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if $config.snooze_limit > 0}
|
{#if $config.snooze_limit > 0}
|
||||||
<p class="mt-3 text-[10px] text-[#8a8a8a]">
|
<p class="mt-3 text-[10px] text-text-sec">
|
||||||
{$timer.snoozesUsed}/{$config.snooze_limit} snoozes used
|
{$timer.snoozesUsed}/{$config.snooze_limit} snoozes used
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||||
|
<span tabindex="0" class="sr-only" aria-live="polite">
|
||||||
|
Break in progress, please wait
|
||||||
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Bottom progress bar for modal -->
|
<!-- Bottom progress bar for modal -->
|
||||||
|
|||||||
@@ -2,10 +2,99 @@
|
|||||||
import { milestoneEvent, dailyGoalEvent } from "../stores/timer";
|
import { milestoneEvent, dailyGoalEvent } from "../stores/timer";
|
||||||
import { config } from "../stores/config";
|
import { config } from "../stores/config";
|
||||||
|
|
||||||
const showMilestone = $derived($milestoneEvent !== null && $config.milestone_celebrations);
|
const storeMilestone = $derived($milestoneEvent !== null && $config.milestone_celebrations);
|
||||||
const showGoal = $derived($dailyGoalEvent && $config.milestone_celebrations);
|
const storeGoal = $derived($dailyGoalEvent && $config.milestone_celebrations);
|
||||||
const streakDays = $derived($milestoneEvent ?? 0);
|
const streakDays = $derived($milestoneEvent ?? 0);
|
||||||
|
|
||||||
|
// Local visibility state (decoupled from store for hover persistence)
|
||||||
|
let showMilestone = $state(false);
|
||||||
|
let showGoal = $state(false);
|
||||||
|
let milestoneHovering = $state(false);
|
||||||
|
let goalHovering = $state(false);
|
||||||
|
let milestoneFading = $state(false);
|
||||||
|
let goalFading = $state(false);
|
||||||
|
|
||||||
|
// Timeout handles for auto-dismiss
|
||||||
|
let milestoneTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
let goalTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
const DISMISS_DELAY = 3500; // matches original animation duration
|
||||||
|
const FADE_DURATION = 600; // fade-out transition time
|
||||||
|
|
||||||
|
function dismissMilestone() {
|
||||||
|
milestoneFading = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
showMilestone = false;
|
||||||
|
milestoneFading = false;
|
||||||
|
}, FADE_DURATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismissGoal() {
|
||||||
|
goalFading = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
showGoal = false;
|
||||||
|
goalFading = false;
|
||||||
|
}, FADE_DURATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startMilestoneTimer() {
|
||||||
|
if (milestoneTimeout) clearTimeout(milestoneTimeout);
|
||||||
|
milestoneTimeout = setTimeout(() => {
|
||||||
|
milestoneTimeout = null;
|
||||||
|
dismissMilestone();
|
||||||
|
}, DISMISS_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startGoalTimer() {
|
||||||
|
if (goalTimeout) clearTimeout(goalTimeout);
|
||||||
|
goalTimeout = setTimeout(() => {
|
||||||
|
goalTimeout = null;
|
||||||
|
dismissGoal();
|
||||||
|
}, DISMISS_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the store signals a milestone, show locally and start auto-dismiss
|
||||||
|
$effect(() => {
|
||||||
|
if (storeMilestone) {
|
||||||
|
showMilestone = true;
|
||||||
|
milestoneFading = false;
|
||||||
|
startMilestoneTimer();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// When the store signals daily goal, show locally and start auto-dismiss
|
||||||
|
$effect(() => {
|
||||||
|
if (storeGoal) {
|
||||||
|
showGoal = true;
|
||||||
|
goalFading = false;
|
||||||
|
startGoalTimer();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pause/resume milestone dismiss on hover/focus
|
||||||
|
$effect(() => {
|
||||||
|
if (milestoneHovering) {
|
||||||
|
if (milestoneTimeout) {
|
||||||
|
clearTimeout(milestoneTimeout);
|
||||||
|
milestoneTimeout = null;
|
||||||
|
}
|
||||||
|
} else if (showMilestone && !milestoneFading) {
|
||||||
|
startMilestoneTimer();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pause/resume goal dismiss on hover/focus
|
||||||
|
$effect(() => {
|
||||||
|
if (goalHovering) {
|
||||||
|
if (goalTimeout) {
|
||||||
|
clearTimeout(goalTimeout);
|
||||||
|
goalTimeout = null;
|
||||||
|
}
|
||||||
|
} else if (showGoal && !goalFading) {
|
||||||
|
startGoalTimer();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Generate confetti particles on milestone
|
// Generate confetti particles on milestone
|
||||||
const confettiColors = ["#ff4d00", "#7c6aef", "#3fb950", "#fca311", "#f72585", "#4361ee"];
|
const confettiColors = ["#ff4d00", "#7c6aef", "#3fb950", "#fca311", "#f72585", "#4361ee"];
|
||||||
const confettiParticles = $derived(
|
const confettiParticles = $derived(
|
||||||
@@ -23,7 +112,27 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if showMilestone}
|
{#if showMilestone}
|
||||||
<div class="celebration-overlay" role="alert" aria-live="assertive">
|
<!-- svelte-ignore a11y_no_static_element_interactions a11y_no_noninteractive_element_interactions -->
|
||||||
|
<div
|
||||||
|
class="celebration-overlay"
|
||||||
|
class:fading={milestoneFading}
|
||||||
|
role="alert"
|
||||||
|
aria-live="assertive"
|
||||||
|
tabindex="-1"
|
||||||
|
onmouseenter={() => milestoneHovering = true}
|
||||||
|
onmouseleave={() => milestoneHovering = false}
|
||||||
|
onfocusin={() => milestoneHovering = true}
|
||||||
|
onfocusout={() => milestoneHovering = false}
|
||||||
|
onkeydown={(e) => { if (e.key === 'Escape') dismissMilestone(); }}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onclick={() => dismissMilestone()}
|
||||||
|
class="absolute top-3 right-3 w-8 h-8 flex items-center justify-center text-white/50 hover:text-white rounded-full"
|
||||||
|
aria-label="Dismiss notification"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
|
||||||
<!-- Confetti burst -->
|
<!-- Confetti burst -->
|
||||||
<div class="confetti-container">
|
<div class="confetti-container">
|
||||||
{#each confettiParticles as p (p.id)}
|
{#each confettiParticles as p (p.id)}
|
||||||
@@ -51,12 +160,31 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if showGoal && !showMilestone}
|
{#if showGoal && !showMilestone}
|
||||||
<div class="goal-overlay" role="alert" aria-live="assertive">
|
<!-- svelte-ignore a11y_no_static_element_interactions a11y_no_noninteractive_element_interactions -->
|
||||||
|
<div
|
||||||
|
class="goal-overlay"
|
||||||
|
class:fading={goalFading}
|
||||||
|
role="alert"
|
||||||
|
aria-live="assertive"
|
||||||
|
tabindex="-1"
|
||||||
|
onmouseenter={() => goalHovering = true}
|
||||||
|
onmouseleave={() => goalHovering = false}
|
||||||
|
onfocusin={() => goalHovering = true}
|
||||||
|
onfocusout={() => goalHovering = false}
|
||||||
|
onkeydown={(e) => { if (e.key === 'Escape') dismissGoal(); }}
|
||||||
|
>
|
||||||
<div class="goal-badge">
|
<div class="goal-badge">
|
||||||
<svg aria-hidden="true" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#3fb950" stroke-width="2.5">
|
<svg aria-hidden="true" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#3fb950" stroke-width="2.5">
|
||||||
<path d="M20 6L9 17l-5-5"/>
|
<path d="M20 6L9 17l-5-5"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="text-[14px] font-medium text-[#3fb950] ml-2">Daily goal reached!</span>
|
<span class="text-[14px] font-medium text-[#3fb950] ml-2">Daily goal reached!</span>
|
||||||
|
<button
|
||||||
|
onclick={() => dismissGoal()}
|
||||||
|
class="w-6 h-6 flex items-center justify-center text-[#3fb950]/50 hover:text-[#3fb950] rounded-full ml-2"
|
||||||
|
aria-label="Dismiss notification"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -69,13 +197,18 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
pointer-events: none;
|
opacity: 1;
|
||||||
animation: celebration-fade 3.5s ease forwards;
|
transition: opacity 0.6s ease;
|
||||||
|
animation: celebration-enter 0.3s ease forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes celebration-fade {
|
.celebration-overlay.fading {
|
||||||
0%, 70% { opacity: 1; }
|
opacity: 0;
|
||||||
100% { opacity: 0; }
|
}
|
||||||
|
|
||||||
|
@keyframes celebration-enter {
|
||||||
|
0% { opacity: 0; }
|
||||||
|
100% { opacity: 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.confetti-container {
|
.confetti-container {
|
||||||
@@ -130,15 +263,18 @@
|
|||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
pointer-events: none;
|
opacity: 1;
|
||||||
animation: goal-slide 3.5s ease forwards;
|
transition: opacity 0.6s ease;
|
||||||
|
animation: goal-enter 0.35s ease forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes goal-slide {
|
.goal-overlay.fading {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes goal-enter {
|
||||||
0% { transform: translateX(-50%) translateY(-20px); opacity: 0; }
|
0% { transform: translateX(-50%) translateY(-20px); opacity: 0; }
|
||||||
10% { transform: translateX(-50%) translateY(0); opacity: 1; }
|
100% { transform: translateX(-50%) translateY(0); opacity: 1; }
|
||||||
75% { opacity: 1; }
|
|
||||||
100% { transform: translateX(-50%) translateY(-10px); opacity: 0; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.goal-badge {
|
.goal-badge {
|
||||||
@@ -158,6 +294,11 @@
|
|||||||
.goal-overlay {
|
.goal-overlay {
|
||||||
animation: none;
|
animation: none;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
.celebration-overlay.fading,
|
||||||
|
.goal-overlay.fading {
|
||||||
|
opacity: 0;
|
||||||
}
|
}
|
||||||
.confetti-particle {
|
.confetti-particle {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@@ -231,7 +231,7 @@
|
|||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-[13px] text-white">{label}</div>
|
<div class="text-[13px] text-white">{label}</div>
|
||||||
<div class="font-mono text-[11px] text-[#8a8a8a]">{value}</div>
|
<div class="font-mono text-[11px] text-text-sec">{value}</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="h-6 w-6 rounded-full border border-[#333] shadow-[0_0_8px_0px] transition-shadow duration-300"
|
class="h-6 w-6 rounded-full border border-[#333] shadow-[0_0_8px_0px] transition-shadow duration-300"
|
||||||
@@ -240,31 +240,41 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Preset swatches -->
|
<!-- Preset swatches -->
|
||||||
<div class="flex flex-wrap gap-[6px]">
|
<div class="flex flex-wrap gap-[8px]">
|
||||||
{#each presets as color}
|
{#each presets as color}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="h-[22px] w-[22px] rounded-full transition-all duration-150
|
class="group flex items-center justify-center min-h-[44px] min-w-[44px] bg-transparent border-none p-0"
|
||||||
{value === color
|
|
||||||
? 'ring-[1.5px] ring-white ring-offset-1 ring-offset-black scale-110'
|
|
||||||
: 'hover:scale-110 opacity-80 hover:opacity-100'}"
|
|
||||||
style="background: {color};"
|
|
||||||
onclick={() => selectPreset(color)}
|
onclick={() => selectPreset(color)}
|
||||||
aria-label="Select {getColorName(color)}"
|
aria-label="Select {getColorName(color)}"
|
||||||
></button>
|
aria-pressed={value === color}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="block h-[28px] w-[28px] rounded-full transition-all duration-150 pointer-events-none
|
||||||
|
{value === color
|
||||||
|
? 'ring-[1.5px] ring-white ring-offset-1 ring-offset-black scale-110'
|
||||||
|
: 'group-hover:scale-110 opacity-80 group-hover:opacity-100'}"
|
||||||
|
style="background: {color};"
|
||||||
|
></span>
|
||||||
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<!-- Custom toggle swatch — ring shows when picker open OR value is custom -->
|
<!-- Custom toggle swatch — ring shows when picker open OR value is custom -->
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="h-[22px] w-[22px] rounded-full transition-all duration-150
|
class="group flex items-center justify-center min-h-[44px] min-w-[44px] bg-transparent border-none p-0"
|
||||||
{showCustom || !isPreset
|
|
||||||
? 'ring-[1.5px] ring-white ring-offset-1 ring-offset-black scale-110'
|
|
||||||
: 'hover:scale-110 opacity-80 hover:opacity-100'}"
|
|
||||||
style="background: conic-gradient(from 0deg, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00);"
|
|
||||||
onclick={() => { showCustom = !showCustom; }}
|
onclick={() => { showCustom = !showCustom; }}
|
||||||
aria-label="Custom color"
|
aria-label="Custom color"
|
||||||
></button>
|
aria-pressed={showCustom || !isPreset}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="block h-[28px] w-[28px] rounded-full transition-all duration-150 pointer-events-none
|
||||||
|
{showCustom || !isPreset
|
||||||
|
? 'ring-[1.5px] ring-white ring-offset-1 ring-offset-black scale-110'
|
||||||
|
: 'opacity-80 group-hover:opacity-100 group-hover:scale-110'}"
|
||||||
|
style="background: conic-gradient(from 0deg, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00);"
|
||||||
|
></span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Inline custom picker — slides open/closed -->
|
<!-- Inline custom picker — slides open/closed -->
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
|
|
||||||
// Natural break notification
|
// Natural break notification
|
||||||
let showNaturalBreakToast = $state(false);
|
let showNaturalBreakToast = $state(false);
|
||||||
|
let toastHovering = $state(false);
|
||||||
let naturalBreakToastTimeout: ReturnType<typeof setTimeout> | null = null;
|
let naturalBreakToastTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
// Watch for natural break detection
|
// Watch for natural break detection
|
||||||
@@ -120,7 +121,9 @@
|
|||||||
showNaturalBreakToast = true;
|
showNaturalBreakToast = true;
|
||||||
if (naturalBreakToastTimeout) clearTimeout(naturalBreakToastTimeout);
|
if (naturalBreakToastTimeout) clearTimeout(naturalBreakToastTimeout);
|
||||||
naturalBreakToastTimeout = setTimeout(() => {
|
naturalBreakToastTimeout = setTimeout(() => {
|
||||||
|
if (!toastHovering) {
|
||||||
showNaturalBreakToast = false;
|
showNaturalBreakToast = false;
|
||||||
|
}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -177,7 +180,7 @@
|
|||||||
<!-- Status label -->
|
<!-- Status label -->
|
||||||
<span
|
<span
|
||||||
class="block text-center text-[11px] font-medium tracking-[0.25em]"
|
class="block text-center text-[11px] font-medium tracking-[0.25em]"
|
||||||
class:text-[#8a8a8a]={!$timer.prebreakWarning && !$timer.deferredBreakPending}
|
class:text-text-sec={!$timer.prebreakWarning && !$timer.deferredBreakPending}
|
||||||
class:text-warning={$timer.prebreakWarning}
|
class:text-warning={$timer.prebreakWarning}
|
||||||
class:text-[#fca311]={$timer.deferredBreakPending}
|
class:text-[#fca311]={$timer.deferredBreakPending}
|
||||||
>
|
>
|
||||||
@@ -205,15 +208,16 @@
|
|||||||
></div>
|
></div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<span class="text-[9px] text-[#8a8a8a] tabular-nums">
|
<span class="text-[9px] text-text-sec tabular-nums">
|
||||||
{$timer.pomodoroCyclePosition + 1}/{$timer.pomodoroTotalInCycle}
|
{$timer.pomodoroCyclePosition + 1}/{$timer.pomodoroTotalInCycle}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<span class="sr-only">Pomodoro cycle: session {$timer.pomodoroCyclePosition + 1} of {$timer.pomodoroTotalInCycle}</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Microbreak countdown -->
|
<!-- Microbreak countdown -->
|
||||||
{#if $timer.microbreakEnabled && !$timer.microbreakActive && $timer.state === "running"}
|
{#if $timer.microbreakEnabled && !$timer.microbreakActive && $timer.state === "running"}
|
||||||
<div class="flex items-center gap-1 text-[9px] text-[#8a8a8a]">
|
<div class="flex items-center gap-1 text-[9px] text-text-sec">
|
||||||
<svg aria-hidden="true" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
<svg aria-hidden="true" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
<path d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z"/>
|
<path d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z"/>
|
||||||
<circle cx="12" cy="12" r="3"/>
|
<circle cx="12" cy="12" r="3"/>
|
||||||
@@ -231,14 +235,22 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<span class="text-[9px] text-[#3fb950]">Goal met</span>
|
<span class="text-[9px] text-[#3fb950]">Goal met</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="text-[9px] text-[#8a8a8a]">Goal</span>
|
<span class="text-[9px] text-text-sec">Goal</span>
|
||||||
<div class="w-16 h-[2px] rounded-full overflow-hidden" style="background: #161616;">
|
<div
|
||||||
|
class="w-16 h-[2px] rounded-full overflow-hidden"
|
||||||
|
style="background: #161616;"
|
||||||
|
role="progressbar"
|
||||||
|
aria-label="Daily goal progress"
|
||||||
|
aria-valuemin={0}
|
||||||
|
aria-valuemax={$config.daily_goal_breaks}
|
||||||
|
aria-valuenow={dailyGoalProgress}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="h-full rounded-full transition-[width] duration-500"
|
class="h-full rounded-full transition-[width] duration-500"
|
||||||
style="width: {Math.min(100, (dailyGoalProgress / $config.daily_goal_breaks) * 100)}%; background: {$config.accent_color};"
|
style="width: {Math.min(100, (dailyGoalProgress / $config.daily_goal_breaks) * 100)}%; background: {$config.accent_color};"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-[9px] text-[#8a8a8a] tabular-nums">
|
<span class="text-[9px] text-text-sec tabular-nums">
|
||||||
{dailyGoalProgress}/{$config.daily_goal_breaks}
|
{dailyGoalProgress}/{$config.daily_goal_breaks}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -253,7 +265,7 @@
|
|||||||
<!-- Last break info -->
|
<!-- Last break info -->
|
||||||
<div use:fadeIn={{ delay: 0.4, y: 10 }}>
|
<div use:fadeIn={{ delay: 0.4, y: 10 }}>
|
||||||
{#if $timer.hasHadBreak}
|
{#if $timer.hasHadBreak}
|
||||||
<p style="margin-bottom: {ringMargin}px;" class="text-[12px] text-[#8a8a8a]">
|
<p style="margin-bottom: {ringMargin}px;" class="text-[12px] text-text-sec">
|
||||||
Last break {formatDurationAgo($timer.secondsSinceLastBreak)}
|
Last break {formatDurationAgo($timer.secondsSinceLastBreak)}
|
||||||
</p>
|
</p>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -263,17 +275,24 @@
|
|||||||
|
|
||||||
<!-- Natural break notification toast -->
|
<!-- Natural break notification toast -->
|
||||||
{#if showNaturalBreakToast}
|
{#if showNaturalBreakToast}
|
||||||
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||||
<div
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
class="absolute top-6 left-1/2 -translate-x-1/2 rounded-2xl border px-5 py-3 text-center backdrop-blur-xl transition-all duration-500"
|
class="absolute top-6 left-1/2 -translate-x-1/2 rounded-2xl border px-5 py-3 text-center backdrop-blur-xl transition-all duration-500"
|
||||||
style="border-color: rgba(63, 185, 80, 0.3); background: rgba(63, 185, 80, 0.1);"
|
style="border-color: rgba(63, 185, 80, 0.3); background: rgba(63, 185, 80, 0.1);"
|
||||||
use:scaleIn={{ duration: 0.3, delay: 0 }}
|
use:scaleIn={{ duration: 0.3, delay: 0 }}
|
||||||
|
onmouseenter={() => toastHovering = true}
|
||||||
|
onmouseleave={() => { toastHovering = false; showNaturalBreakToast = false; }}
|
||||||
|
onfocusin={() => toastHovering = true}
|
||||||
|
onfocusout={() => { toastHovering = false; showNaturalBreakToast = false; }}
|
||||||
|
onkeydown={(e) => { if (e.key === 'Escape') showNaturalBreakToast = false; }}
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<svg aria-hidden="true" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#3fb950" stroke-width="2">
|
<svg aria-hidden="true" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#3fb950" stroke-width="2">
|
||||||
<path d="M20 6L9 17l-5-5"/>
|
<path d="M20 6L9 17l-5-5"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="text-[13px] text-[#3fb950]">Natural break detected - timer reset</span>
|
<span class="text-[13px] text-[#3fb950]">Natural break detected - timer reset</span>
|
||||||
|
<button onclick={() => showNaturalBreakToast = false} class="ml-2 text-text-sec hover:text-white" aria-label="Dismiss notification">×</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -292,13 +311,15 @@
|
|||||||
{toggleBtnText}
|
{toggleBtnText}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- Bottom navigation buttons -->
|
||||||
|
<nav aria-label="Main actions" class="contents">
|
||||||
<!-- Bottom left: start break now -->
|
<!-- Bottom left: start break now -->
|
||||||
<div class="absolute bottom-5 left-5" use:fadeIn={{ delay: 0.5, y: 8 }}>
|
<div class="absolute bottom-5 left-5" use:fadeIn={{ delay: 0.5, y: 8 }}>
|
||||||
<button
|
<button
|
||||||
aria-label="Start break now"
|
aria-label="Start break now"
|
||||||
use:pressable
|
use:pressable
|
||||||
class="flex h-11 w-11 items-center justify-center rounded-full
|
class="flex h-11 w-11 items-center justify-center rounded-full
|
||||||
border border-[#222] text-[#8a8a8a]
|
border border-border text-text-sec
|
||||||
transition-colors duration-200
|
transition-colors duration-200
|
||||||
hover:border-[#333] hover:text-[#aaa]"
|
hover:border-[#333] hover:text-[#aaa]"
|
||||||
onclick={startBreakNow}
|
onclick={startBreakNow}
|
||||||
@@ -325,7 +346,7 @@
|
|||||||
aria-label="Statistics"
|
aria-label="Statistics"
|
||||||
use:pressable
|
use:pressable
|
||||||
class="flex h-11 w-11 items-center justify-center rounded-full
|
class="flex h-11 w-11 items-center justify-center rounded-full
|
||||||
border border-[#222] text-[#8a8a8a]
|
border border-border text-text-sec
|
||||||
transition-colors duration-200
|
transition-colors duration-200
|
||||||
hover:border-[#333] hover:text-[#aaa]"
|
hover:border-[#333] hover:text-[#aaa]"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
@@ -357,7 +378,7 @@
|
|||||||
aria-label="Settings"
|
aria-label="Settings"
|
||||||
use:pressable
|
use:pressable
|
||||||
class="flex h-11 w-11 items-center justify-center rounded-full
|
class="flex h-11 w-11 items-center justify-center rounded-full
|
||||||
border border-[#222] text-[#8a8a8a]
|
border border-border text-text-sec
|
||||||
transition-colors duration-200
|
transition-colors duration-200
|
||||||
hover:border-[#333] hover:text-[#aaa]"
|
hover:border-[#333] hover:text-[#aaa]"
|
||||||
onclick={openSettings}
|
onclick={openSettings}
|
||||||
@@ -380,6 +401,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-[13px] text-white">Countdown font</div>
|
<div class="text-[13px] text-white">Countdown font</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">
|
<div class="text-[11px] text-text-sec">
|
||||||
{value || "System default"}
|
{value || "System default"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
aria-expanded={expanded}
|
aria-expanded={expanded}
|
||||||
aria-label={expanded ? "Close font browser" : "Browse fonts"}
|
aria-label={expanded ? "Close font browser" : "Browse fonts"}
|
||||||
class="rounded-lg border border-[#1a1a1a] bg-black px-3 py-1.5 text-[12px] text-[#8a8a8a]
|
class="rounded-lg border border-[#1a1a1a] bg-black px-3 py-1.5 text-[12px] text-text-sec
|
||||||
transition-colors hover:border-[#333] hover:text-white"
|
transition-colors hover:border-[#333] hover:text-white"
|
||||||
onclick={() => { expanded = !expanded; }}
|
onclick={() => { expanded = !expanded; }}
|
||||||
>
|
>
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
transition-all duration-150
|
transition-all duration-150
|
||||||
{value === font.family
|
{value === font.family
|
||||||
? 'border-white/30 bg-[#141414]'
|
? 'border-white/30 bg-[#141414]'
|
||||||
: 'border-[#141414] bg-[#0a0a0a] hover:border-[#222] hover:bg-[#0f0f0f]'}"
|
: 'border-[#141414] bg-[#0a0a0a] hover:border-border hover:bg-[#0f0f0f]'}"
|
||||||
onclick={() => selectFont(font.family)}
|
onclick={() => selectFont(font.family)}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
>
|
>
|
||||||
25:00
|
25:00
|
||||||
</span>
|
</span>
|
||||||
<span class="text-[9px] tracking-wider text-[#8a8a8a] uppercase">
|
<span class="text-[9px] tracking-wider text-text-sec uppercase">
|
||||||
{font.label}
|
{font.label}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -38,17 +38,18 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="microbreak-card">
|
<div class="microbreak-card" role="alertdialog" aria-label="Microbreak" aria-describedby="microbreak-msg">
|
||||||
|
<h2 class="sr-only">Microbreak</h2>
|
||||||
<div class="flex items-center gap-3 mb-2">
|
<div class="flex items-center gap-3 mb-2">
|
||||||
<svg aria-hidden="true" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#7c6aef" stroke-width="1.5">
|
<svg aria-hidden="true" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#7c6aef" stroke-width="1.5">
|
||||||
<path d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z"/>
|
<path d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z"/>
|
||||||
<circle cx="12" cy="12" r="3"/>
|
<circle cx="12" cy="12" r="3"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="text-[15px] font-medium text-white">Look away — 20 feet for {timeRemaining}s</span>
|
<span id="microbreak-msg" class="text-[15px] font-medium text-white">Look away — 20 feet for {timeRemaining}s</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if activity && $config.microbreak_show_activity}
|
{#if activity && $config.microbreak_show_activity}
|
||||||
<p class="text-[12px] text-[#8a8a8a] mb-3 ml-[34px]">
|
<p class="text-[12px] text-text-sec mb-3 ml-[34px]">
|
||||||
{activity.text}
|
{activity.text}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -335,13 +335,13 @@ const fontStyle = $derived(
|
|||||||
<!-- Countdown text -->
|
<!-- Countdown text -->
|
||||||
<span
|
<span
|
||||||
class="ml-2.5 text-[18px] font-semibold leading-none tabular-nums"
|
class="ml-2.5 text-[18px] font-semibold leading-none tabular-nums"
|
||||||
style="color: {state === 'paused' ? '#555' : '#fff'}; {fontStyle}"
|
style="color: {state === 'paused' ? '#a8a8a8' : '#fff'}; {fontStyle}"
|
||||||
>
|
>
|
||||||
{timeText}
|
{timeText}
|
||||||
</span>
|
</span>
|
||||||
<!-- F3: Pomodoro cycle indicator -->
|
<!-- F3: Pomodoro cycle indicator -->
|
||||||
{#if pomodoroEnabled}
|
{#if pomodoroEnabled}
|
||||||
<span class="ml-1.5 text-[10px] tabular-nums" style="color: #8a8a8a;">
|
<span class="ml-1.5 text-[10px] tabular-nums" style="color: #a8a8a8;">
|
||||||
{pomodoroCyclePosition + 1}/{pomodoroTotalInCycle}
|
{pomodoroCyclePosition + 1}/{pomodoroTotalInCycle}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -124,8 +124,8 @@
|
|||||||
<button
|
<button
|
||||||
aria-label="Back to dashboard"
|
aria-label="Back to dashboard"
|
||||||
use:pressable
|
use:pressable
|
||||||
class="mr-3 flex h-8 w-8 items-center justify-center rounded-full
|
class="mr-3 flex h-10 w-10 min-h-[44px] min-w-[44px] items-center justify-center rounded-full
|
||||||
text-[#8a8a8a] transition-colors hover:text-white"
|
text-text-sec transition-colors hover:text-white"
|
||||||
onclick={goBack}
|
onclick={goBack}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -159,16 +159,16 @@
|
|||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
|
|
||||||
<!-- 1. Timer -->
|
<!-- 1. Timer -->
|
||||||
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0 }}>
|
<section aria-labelledby="settings-timer" class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0 }}>
|
||||||
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
|
<h2 id="settings-timer" class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
|
||||||
Timer
|
Timer
|
||||||
</h3>
|
</h2>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Break frequency</div>
|
<div class="text-[13px] text-white">Break frequency</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">
|
<div class="text-[11px] text-text-sec">
|
||||||
Every {$config.break_frequency} min
|
Every {$config.break_frequency} <abbr title="minutes">min</abbr>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Stepper
|
<Stepper
|
||||||
@@ -180,12 +180,12 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Break duration</div>
|
<div class="text-[13px] text-white">Break duration</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">
|
<div class="text-[11px] text-text-sec">
|
||||||
{$config.break_duration} min
|
{$config.break_duration} min
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -198,12 +198,12 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Auto-start</div>
|
<div class="text-[13px] text-white">Auto-start</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">Start timer on launch</div>
|
<div class="text-[11px] text-text-sec">Start timer on launch</div>
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
bind:checked={$config.auto_start}
|
bind:checked={$config.auto_start}
|
||||||
@@ -214,61 +214,61 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 2. Pomodoro Mode -->
|
<!-- 2. Pomodoro Mode -->
|
||||||
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.03 }}>
|
<section aria-labelledby="settings-pomodoro" class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.03 }}>
|
||||||
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
|
<h2 id="settings-pomodoro" title="Pomodoro technique alternates focused work sessions with short and long breaks" class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
|
||||||
Pomodoro Mode
|
Pomodoro Mode
|
||||||
</h3>
|
</h2>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Enable Pomodoro</div>
|
<div class="text-[13px] text-white">Enable Pomodoro</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">Short breaks then a long break</div>
|
<div class="text-[11px] text-text-sec">Short breaks then a long break</div>
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch bind:checked={$config.pomodoro_enabled} label="Pomodoro mode" onchange={markChanged} />
|
<ToggleSwitch bind:checked={$config.pomodoro_enabled} label="Pomodoro mode" onchange={markChanged} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $config.pomodoro_enabled}
|
{#if $config.pomodoro_enabled}
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Short breaks before long</div>
|
<div class="text-[13px] text-white">Short breaks before long</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">{$config.pomodoro_short_breaks} short + 1 long</div>
|
<div class="text-[11px] text-text-sec">{$config.pomodoro_short_breaks} short + 1 long</div>
|
||||||
</div>
|
</div>
|
||||||
<Stepper bind:value={$config.pomodoro_short_breaks} label="Short breaks" min={1} max={10} onchange={markChanged} />
|
<Stepper bind:value={$config.pomodoro_short_breaks} label="Short breaks" min={1} max={10} onchange={markChanged} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Long break duration</div>
|
<div class="text-[13px] text-white">Long break duration</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">{$config.pomodoro_long_break_duration} min</div>
|
<div class="text-[11px] text-text-sec">{$config.pomodoro_long_break_duration} min</div>
|
||||||
</div>
|
</div>
|
||||||
<Stepper bind:value={$config.pomodoro_long_break_duration} label="Long break duration" min={5} max={60} onchange={markChanged} />
|
<Stepper bind:value={$config.pomodoro_long_break_duration} label="Long break duration" min={5} max={60} onchange={markChanged} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
<div class="flex flex-col gap-1.5">
|
<div class="flex flex-col gap-1.5">
|
||||||
<label class="text-[13px] text-white" for="pomo-title">Long break title</label>
|
<label class="text-[13px] text-white" for="pomo-title">Long break title</label>
|
||||||
<input id="pomo-title" type="text" maxlength={100}
|
<input id="pomo-title" type="text" maxlength={100}
|
||||||
class="rounded-xl border border-[#161616] bg-black px-3.5 py-2.5 text-[13px] text-white outline-none transition-colors placeholder:text-[#2a2a2a] focus:border-[#333]"
|
class="rounded-xl border border-border bg-black px-3.5 py-2.5 text-[13px] text-white outline-none transition-colors placeholder:text-[#555] focus:border-[#333]"
|
||||||
bind:value={$config.pomodoro_long_break_title} oninput={markChanged}
|
bind:value={$config.pomodoro_long_break_title} oninput={markChanged}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
<div class="flex flex-col gap-1.5">
|
<div class="flex flex-col gap-1.5">
|
||||||
<label class="text-[13px] text-white" for="pomo-msg">Long break message</label>
|
<label class="text-[13px] text-white" for="pomo-msg">Long break message</label>
|
||||||
<input id="pomo-msg" type="text" maxlength={500}
|
<input id="pomo-msg" type="text" maxlength={500}
|
||||||
class="rounded-xl border border-[#161616] bg-black px-3.5 py-2.5 text-[13px] text-white outline-none transition-colors placeholder:text-[#2a2a2a] focus:border-[#333]"
|
class="rounded-xl border border-border bg-black px-3.5 py-2.5 text-[13px] text-white outline-none transition-colors placeholder:text-[#555] focus:border-[#333]"
|
||||||
bind:value={$config.pomodoro_long_break_message} oninput={markChanged}
|
bind:value={$config.pomodoro_long_break_message} oninput={markChanged}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Reset on skip</div>
|
<div class="text-[13px] text-white">Reset on skip</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">Reset cycle when skipping a break</div>
|
<div class="text-[11px] text-text-sec">Reset cycle when skipping a break</div>
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch bind:checked={$config.pomodoro_reset_on_skip} label="Reset on skip" onchange={markChanged} />
|
<ToggleSwitch bind:checked={$config.pomodoro_reset_on_skip} label="Reset on skip" onchange={markChanged} />
|
||||||
</div>
|
</div>
|
||||||
@@ -276,15 +276,15 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 3. Microbreaks -->
|
<!-- 3. Microbreaks -->
|
||||||
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.06 }}>
|
<section aria-labelledby="settings-microbreaks" class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.06 }}>
|
||||||
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
|
<h2 id="settings-microbreaks" title="20-20-20 rule: every 20 minutes, look 20 feet away for 20 seconds" class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
|
||||||
Microbreaks
|
Microbreaks
|
||||||
</h3>
|
</h2>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">20-20-20 eye breaks</div>
|
<div class="text-[13px] text-white">20-20-20 eye breaks</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">Quick eye rest reminders</div>
|
<div class="text-[11px] text-text-sec">Quick eye rest reminders</div>
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
bind:checked={$config.microbreak_enabled}
|
bind:checked={$config.microbreak_enabled}
|
||||||
@@ -294,47 +294,47 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $config.microbreak_enabled}
|
{#if $config.microbreak_enabled}
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Frequency</div>
|
<div class="text-[13px] text-white">Frequency</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">Every {$config.microbreak_frequency} min</div>
|
<div class="text-[11px] text-text-sec">Every {$config.microbreak_frequency} min</div>
|
||||||
</div>
|
</div>
|
||||||
<Stepper bind:value={$config.microbreak_frequency} label="Microbreak frequency" min={5} max={60} onchange={markChanged} />
|
<Stepper bind:value={$config.microbreak_frequency} label="Microbreak frequency" min={5} max={60} onchange={markChanged} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Duration</div>
|
<div class="text-[13px] text-white">Duration</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">{$config.microbreak_duration} seconds</div>
|
<div class="text-[11px] text-text-sec">{$config.microbreak_duration} seconds</div>
|
||||||
</div>
|
</div>
|
||||||
<Stepper bind:value={$config.microbreak_duration} label="Microbreak duration" min={10} max={60} step={5} onchange={markChanged} />
|
<Stepper bind:value={$config.microbreak_duration} label="Microbreak duration" min={10} max={60} step={5} onchange={markChanged} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Sound</div>
|
<div class="text-[13px] text-white">Sound</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">Play sound on eye break</div>
|
<div class="text-[11px] text-text-sec">Play sound on eye break</div>
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch bind:checked={$config.microbreak_sound_enabled} label="Microbreak sound" onchange={markChanged} />
|
<ToggleSwitch bind:checked={$config.microbreak_sound_enabled} label="Microbreak sound" onchange={markChanged} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Show activity</div>
|
<div class="text-[13px] text-white">Show activity</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">Activity suggestion during eye break</div>
|
<div class="text-[11px] text-text-sec">Activity suggestion during eye break</div>
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch bind:checked={$config.microbreak_show_activity} label="Microbreak activity" onchange={markChanged} />
|
<ToggleSwitch bind:checked={$config.microbreak_show_activity} label="Microbreak activity" onchange={markChanged} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Pause during breaks</div>
|
<div class="text-[13px] text-white">Pause during breaks</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">No eye breaks during main breaks</div>
|
<div class="text-[11px] text-text-sec">No eye breaks during main breaks</div>
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch bind:checked={$config.microbreak_pause_during_break} label="Pause during breaks" onchange={markChanged} />
|
<ToggleSwitch bind:checked={$config.microbreak_pause_during_break} label="Pause during breaks" onchange={markChanged} />
|
||||||
</div>
|
</div>
|
||||||
@@ -342,10 +342,10 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 4. Break Screen (stripped down) -->
|
<!-- 4. Break Screen (stripped down) -->
|
||||||
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.09 }}>
|
<section aria-labelledby="settings-breakscreen" class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.09 }}>
|
||||||
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
|
<h2 id="settings-breakscreen" class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
|
||||||
Break Screen
|
Break Screen
|
||||||
</h3>
|
</h2>
|
||||||
|
|
||||||
<div class="flex flex-col gap-1.5">
|
<div class="flex flex-col gap-1.5">
|
||||||
<label class="text-[13px] text-white" for="break-title">
|
<label class="text-[13px] text-white" for="break-title">
|
||||||
@@ -354,16 +354,16 @@
|
|||||||
<input
|
<input
|
||||||
id="break-title"
|
id="break-title"
|
||||||
type="text"
|
type="text"
|
||||||
class="rounded-xl border border-[#161616] bg-black px-3.5 py-2.5 text-[13px]
|
class="rounded-xl border border-border bg-black px-3.5 py-2.5 text-[13px]
|
||||||
text-white outline-none transition-colors
|
text-white outline-none transition-colors
|
||||||
placeholder:text-[#2a2a2a] focus:border-[#333]"
|
placeholder:text-[#555] focus:border-[#333]"
|
||||||
placeholder="Enter break title..."
|
placeholder="Enter break title..."
|
||||||
bind:value={$config.break_title}
|
bind:value={$config.break_title}
|
||||||
oninput={markChanged}
|
oninput={markChanged}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-1.5">
|
<div class="flex flex-col gap-1.5">
|
||||||
<label class="text-[13px] text-white" for="break-message">
|
<label class="text-[13px] text-white" for="break-message">
|
||||||
@@ -372,21 +372,21 @@
|
|||||||
<input
|
<input
|
||||||
id="break-message"
|
id="break-message"
|
||||||
type="text"
|
type="text"
|
||||||
class="rounded-xl border border-[#161616] bg-black px-3.5 py-2.5 text-[13px]
|
class="rounded-xl border border-border bg-black px-3.5 py-2.5 text-[13px]
|
||||||
text-white outline-none transition-colors
|
text-white outline-none transition-colors
|
||||||
placeholder:text-[#2a2a2a] focus:border-[#333]"
|
placeholder:text-[#555] focus:border-[#333]"
|
||||||
placeholder="Enter break message..."
|
placeholder="Enter break message..."
|
||||||
bind:value={$config.break_message}
|
bind:value={$config.break_message}
|
||||||
oninput={markChanged}
|
oninput={markChanged}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Fullscreen break</div>
|
<div class="text-[13px] text-white">Fullscreen break</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">
|
<div class="text-[11px] text-text-sec">
|
||||||
{$config.fullscreen_mode ? "Fills entire screen" : "Centered modal"}
|
{$config.fullscreen_mode ? "Fills entire screen" : "Centered modal"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -397,12 +397,12 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Activity suggestions</div>
|
<div class="text-[13px] text-white">Activity suggestions</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">
|
<div class="text-[11px] text-text-sec">
|
||||||
Exercise ideas during breaks
|
Exercise ideas during breaks
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -414,11 +414,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $config.fullscreen_mode}
|
{#if $config.fullscreen_mode}
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Block all monitors</div>
|
<div class="text-[13px] text-white">Block all monitors</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">Show overlay on all screens during breaks</div>
|
<div class="text-[11px] text-text-sec">Show overlay on all screens during breaks</div>
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
bind:checked={$config.multi_monitor_break}
|
bind:checked={$config.multi_monitor_break}
|
||||||
@@ -431,24 +431,24 @@
|
|||||||
|
|
||||||
<!-- 5. Break Activities (own card, conditional) -->
|
<!-- 5. Break Activities (own card, conditional) -->
|
||||||
{#if $config.show_break_activities}
|
{#if $config.show_break_activities}
|
||||||
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.12 }}>
|
<section aria-labelledby="settings-activities" class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.12 }}>
|
||||||
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
|
<h2 id="settings-activities" class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
|
||||||
Break Activities
|
Break Activities
|
||||||
</h3>
|
</h2>
|
||||||
<ActivityManager />
|
<ActivityManager />
|
||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- 6. Breathing Guide (own card) -->
|
<!-- 6. Breathing Guide (own card) -->
|
||||||
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.15 }}>
|
<section aria-labelledby="settings-breathing" class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.15 }}>
|
||||||
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
|
<h2 id="settings-breathing" title="Visual breathing exercise during breaks to reduce stress" class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
|
||||||
Breathing Guide
|
Breathing Guide
|
||||||
</h3>
|
</h2>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Guided breathing</div>
|
<div class="text-[13px] text-white">Guided breathing</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">Visual breathing guide during breaks</div>
|
<div class="text-[11px] text-text-sec">Visual breathing guide during breaks</div>
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
bind:checked={$config.breathing_guide_enabled}
|
bind:checked={$config.breathing_guide_enabled}
|
||||||
@@ -458,18 +458,20 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $config.breathing_guide_enabled}
|
{#if $config.breathing_guide_enabled}
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-3 text-[13px] text-white">Breathing pattern</div>
|
<div class="mb-3 text-[13px] text-white" id="breathing-pattern-label">Breathing pattern</div>
|
||||||
<div class="flex flex-col gap-1.5">
|
<div class="flex flex-col gap-1.5" role="radiogroup" aria-label="Breathing pattern">
|
||||||
{#each breathingPatternMeta as bp}
|
{#each breathingPatternMeta as bp}
|
||||||
<button
|
<button
|
||||||
use:pressable
|
use:pressable
|
||||||
|
role="radio"
|
||||||
|
aria-checked={$config.breathing_pattern === bp.id}
|
||||||
class="flex items-center gap-3 rounded-xl px-3.5 py-2.5 text-left
|
class="flex items-center gap-3 rounded-xl px-3.5 py-2.5 text-left
|
||||||
transition-all duration-200
|
transition-all duration-200
|
||||||
{$config.breathing_pattern === bp.id
|
{$config.breathing_pattern === bp.id
|
||||||
? 'bg-[#1a1a1a] border border-[#333]'
|
? 'bg-[#1a1a1a] border border-[#333]'
|
||||||
: 'bg-[#0a0a0a] border border-[#161616] hover:border-[#333]'}"
|
: 'bg-[#0a0a0a] border border-border hover:border-[#333]'}"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
$config.breathing_pattern = bp.id;
|
$config.breathing_pattern = bp.id;
|
||||||
markChanged();
|
markChanged();
|
||||||
@@ -480,10 +482,10 @@
|
|||||||
style="border-color: {$config.breathing_pattern === bp.id ? $config.accent_color : '#333'};
|
style="border-color: {$config.breathing_pattern === bp.id ? $config.accent_color : '#333'};
|
||||||
background: {$config.breathing_pattern === bp.id ? $config.accent_color : 'transparent'};"
|
background: {$config.breathing_pattern === bp.id ? $config.accent_color : 'transparent'};"
|
||||||
></div>
|
></div>
|
||||||
<span class="text-[12px] font-medium {$config.breathing_pattern === bp.id ? 'text-white' : 'text-[#8a8a8a]'}">
|
<span class="text-[12px] font-medium {$config.breathing_pattern === bp.id ? 'text-white' : 'text-text-sec'}">
|
||||||
{bp.label}
|
{bp.label}
|
||||||
</span>
|
</span>
|
||||||
<span class="ml-auto text-[11px] text-[#8a8a8a] opacity-60 tabular-nums">
|
<span class="ml-auto text-[11px] text-text-sec opacity-60 tabular-nums">
|
||||||
{bp.desc}
|
{bp.desc}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -494,15 +496,15 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 7. Behavior -->
|
<!-- 7. Behavior -->
|
||||||
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.18 }}>
|
<section aria-labelledby="settings-behavior" class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.18 }}>
|
||||||
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
|
<h2 id="settings-behavior" class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
|
||||||
Behavior
|
Behavior
|
||||||
</h3>
|
</h2>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Strict mode</div>
|
<div class="text-[13px] text-white">Strict mode</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">
|
<div class="text-[11px] text-text-sec">
|
||||||
Disable skip and snooze
|
Disable skip and snooze
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -514,12 +516,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if !$config.strict_mode}
|
{#if !$config.strict_mode}
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Allow end early</div>
|
<div class="text-[13px] text-white">Allow end early</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">After 50% of break</div>
|
<div class="text-[11px] text-text-sec">After 50% of break</div>
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
bind:checked={$config.allow_end_early}
|
bind:checked={$config.allow_end_early}
|
||||||
@@ -528,12 +530,12 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Snooze duration</div>
|
<div class="text-[13px] text-white">Snooze duration</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">
|
<div class="text-[11px] text-text-sec">
|
||||||
{$config.snooze_duration} min
|
{$config.snooze_duration} min
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -546,12 +548,12 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Snooze limit</div>
|
<div class="text-[13px] text-white">Snooze limit</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">
|
<div class="text-[11px] text-text-sec">
|
||||||
{$config.snooze_limit === 0
|
{$config.snooze_limit === 0
|
||||||
? "Unlimited"
|
? "Unlimited"
|
||||||
: `${$config.snooze_limit} per break`}
|
: `${$config.snooze_limit} per break`}
|
||||||
@@ -568,12 +570,12 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Immediate breaks</div>
|
<div class="text-[13px] text-white">Immediate breaks</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">
|
<div class="text-[11px] text-text-sec">
|
||||||
Skip pre-break warning
|
Skip pre-break warning
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -586,15 +588,15 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 8. Alerts (MERGED: Notifications + Pre-Break Nudge) -->
|
<!-- 8. Alerts (MERGED: Notifications + Pre-Break Nudge) -->
|
||||||
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.21 }}>
|
<section aria-labelledby="settings-alerts" class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.21 }}>
|
||||||
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
|
<h2 id="settings-alerts" class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
|
||||||
Alerts
|
Alerts
|
||||||
</h3>
|
</h2>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Pre-break alert</div>
|
<div class="text-[13px] text-white">Pre-break alert</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">Warn before breaks</div>
|
<div class="text-[11px] text-text-sec">Warn before breaks</div>
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
bind:checked={$config.notification_enabled}
|
bind:checked={$config.notification_enabled}
|
||||||
@@ -604,13 +606,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $config.notification_enabled}
|
{#if $config.notification_enabled}
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Alert timing</div>
|
<div class="text-[13px] text-white">Alert timing</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">
|
<div class="text-[11px] text-text-sec">
|
||||||
{$config.notification_before_break}s before
|
{$config.notification_before_break}<abbr title="seconds">s</abbr> before
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Stepper
|
<Stepper
|
||||||
@@ -624,31 +626,31 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Screen dimming</div>
|
<div class="text-[13px] text-white">Screen dimming</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">Gradually dim screen before breaks</div>
|
<div class="text-[11px] text-text-sec">Gradually dim screen before breaks</div>
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch bind:checked={$config.screen_dim_enabled} label="Screen dimming" onchange={markChanged} />
|
<ToggleSwitch bind:checked={$config.screen_dim_enabled} label="Screen dimming" onchange={markChanged} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $config.screen_dim_enabled}
|
{#if $config.screen_dim_enabled}
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Start dimming</div>
|
<div class="text-[13px] text-white">Start dimming</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">{$config.screen_dim_seconds}s before break</div>
|
<div class="text-[11px] text-text-sec">{$config.screen_dim_seconds}s before break</div>
|
||||||
</div>
|
</div>
|
||||||
<Stepper bind:value={$config.screen_dim_seconds} label="Dim start" min={3} max={60} onchange={markChanged} />
|
<Stepper bind:value={$config.screen_dim_seconds} label="Dim start" min={3} max={60} onchange={markChanged} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Max dimming</div>
|
<div class="text-[13px] text-white">Max dimming</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">{Math.round($config.screen_dim_max_opacity * 100)}%</div>
|
<div class="text-[11px] text-text-sec">{Math.round($config.screen_dim_max_opacity * 100)}%</div>
|
||||||
</div>
|
</div>
|
||||||
<Stepper
|
<Stepper
|
||||||
bind:value={$config.screen_dim_max_opacity}
|
bind:value={$config.screen_dim_max_opacity}
|
||||||
@@ -664,15 +666,15 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 9. Sound -->
|
<!-- 9. Sound -->
|
||||||
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.24 }}>
|
<section aria-labelledby="settings-sound" class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.24 }}>
|
||||||
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
|
<h2 id="settings-sound" class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
|
||||||
Sound
|
Sound
|
||||||
</h3>
|
</h2>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Sound effects</div>
|
<div class="text-[13px] text-white">Sound effects</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">Play sounds on break events</div>
|
<div class="text-[11px] text-text-sec">Play sounds on break events</div>
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
bind:checked={$config.sound_enabled}
|
bind:checked={$config.sound_enabled}
|
||||||
@@ -682,12 +684,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $config.sound_enabled}
|
{#if $config.sound_enabled}
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Volume</div>
|
<div class="text-[13px] text-white">Volume</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">{$config.sound_volume}%</div>
|
<div class="text-[11px] text-text-sec">{$config.sound_volume}%</div>
|
||||||
</div>
|
</div>
|
||||||
<Stepper
|
<Stepper
|
||||||
bind:value={$config.sound_volume}
|
bind:value={$config.sound_volume}
|
||||||
@@ -700,7 +702,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-3 text-[13px] text-white">Sound preset</div>
|
<div class="mb-3 text-[13px] text-white">Sound preset</div>
|
||||||
@@ -708,11 +710,12 @@
|
|||||||
{#each soundPresets as preset}
|
{#each soundPresets as preset}
|
||||||
<button
|
<button
|
||||||
use:pressable
|
use:pressable
|
||||||
|
aria-pressed={$config.sound_preset === preset}
|
||||||
class="rounded-xl py-2.5 text-[11px] tracking-wider uppercase
|
class="rounded-xl py-2.5 text-[11px] tracking-wider uppercase
|
||||||
transition-all duration-200
|
transition-all duration-200
|
||||||
{$config.sound_preset === preset
|
{$config.sound_preset === preset
|
||||||
? 'bg-[#1a1a1a] text-white border border-[#333]'
|
? 'bg-[#1a1a1a] text-white border border-[#333]'
|
||||||
: 'bg-[#0a0a0a] text-[#8a8a8a] border border-[#161616] hover:border-[#333] hover:text-white'}"
|
: 'bg-[#0a0a0a] text-text-sec border border-border hover:border-[#333] hover:text-white'}"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
$config.sound_preset = preset;
|
$config.sound_preset = preset;
|
||||||
markChanged();
|
markChanged();
|
||||||
@@ -728,15 +731,15 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 10. Idle & Smart Breaks (MERGED) -->
|
<!-- 10. Idle & Smart Breaks (MERGED) -->
|
||||||
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.27 }}>
|
<section aria-labelledby="settings-idle" class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.27 }}>
|
||||||
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
|
<h2 id="settings-idle" class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
|
||||||
Idle & Smart Breaks
|
Idle & Smart Breaks
|
||||||
</h3>
|
</h2>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Auto-pause when idle</div>
|
<div class="text-[13px] text-white">Auto-pause when idle</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">Pause timer when away</div>
|
<div class="text-[11px] text-text-sec">Pause timer when away</div>
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
bind:checked={$config.idle_detection_enabled}
|
bind:checked={$config.idle_detection_enabled}
|
||||||
@@ -746,12 +749,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $config.idle_detection_enabled}
|
{#if $config.idle_detection_enabled}
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Idle timeout</div>
|
<div class="text-[13px] text-white">Idle timeout</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">
|
<div class="text-[11px] text-text-sec">
|
||||||
{$config.idle_timeout}s of inactivity
|
{$config.idle_timeout}s of inactivity
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -767,12 +770,12 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Smart breaks</div>
|
<div class="text-[13px] text-white">Smart breaks</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">Auto-reset timer when you step away</div>
|
<div class="text-[11px] text-text-sec">Auto-reset timer when you step away</div>
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
bind:checked={$config.smart_breaks_enabled}
|
bind:checked={$config.smart_breaks_enabled}
|
||||||
@@ -782,12 +785,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $config.smart_breaks_enabled}
|
{#if $config.smart_breaks_enabled}
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Minimum away time</div>
|
<div class="text-[13px] text-white">Minimum away time</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">
|
<div class="text-[11px] text-text-sec">
|
||||||
{$config.smart_break_threshold >= 60
|
{$config.smart_break_threshold >= 60
|
||||||
? `${Math.floor($config.smart_break_threshold / 60)} min`
|
? `${Math.floor($config.smart_break_threshold / 60)} min`
|
||||||
: `${$config.smart_break_threshold}s`} to count as break
|
: `${$config.smart_break_threshold}s`} to count as break
|
||||||
@@ -804,12 +807,12 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Count in statistics</div>
|
<div class="text-[13px] text-white">Count in statistics</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">Track natural breaks in stats</div>
|
<div class="text-[11px] text-text-sec">Track natural breaks in stats</div>
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
bind:checked={$config.smart_break_count_stats}
|
bind:checked={$config.smart_break_count_stats}
|
||||||
@@ -821,36 +824,36 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 11. Presentation Mode -->
|
<!-- 11. Presentation Mode -->
|
||||||
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.30 }}>
|
<section aria-labelledby="settings-presentation" class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.30 }}>
|
||||||
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
|
<h2 id="settings-presentation" class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
|
||||||
Presentation Mode
|
Presentation Mode
|
||||||
</h3>
|
</h2>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Auto-detect fullscreen</div>
|
<div class="text-[13px] text-white">Auto-detect fullscreen</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">Defer breaks during fullscreen apps</div>
|
<div class="text-[11px] text-text-sec">Defer breaks during fullscreen apps</div>
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch bind:checked={$config.presentation_mode_enabled} label="Presentation mode" onchange={markChanged} />
|
<ToggleSwitch bind:checked={$config.presentation_mode_enabled} label="Presentation mode" onchange={markChanged} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $config.presentation_mode_enabled}
|
{#if $config.presentation_mode_enabled}
|
||||||
{#if $config.microbreak_enabled}
|
{#if $config.microbreak_enabled}
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Defer microbreaks</div>
|
<div class="text-[13px] text-white">Defer microbreaks</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">Also pause eye breaks</div>
|
<div class="text-[11px] text-text-sec">Also pause eye breaks</div>
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch bind:checked={$config.presentation_mode_defer_microbreaks} label="Defer microbreaks" onchange={markChanged} />
|
<ToggleSwitch bind:checked={$config.presentation_mode_defer_microbreaks} label="Defer microbreaks" onchange={markChanged} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Notification</div>
|
<div class="text-[13px] text-white">Notification</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">Show toast when break is deferred</div>
|
<div class="text-[11px] text-text-sec">Show toast when break is deferred</div>
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch bind:checked={$config.presentation_mode_notification} label="Deferral notification" onchange={markChanged} />
|
<ToggleSwitch bind:checked={$config.presentation_mode_notification} label="Deferral notification" onchange={markChanged} />
|
||||||
</div>
|
</div>
|
||||||
@@ -858,59 +861,59 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 12. Goals & Streaks -->
|
<!-- 12. Goals & Streaks -->
|
||||||
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.33 }}>
|
<section aria-labelledby="settings-goals" class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.33 }}>
|
||||||
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
|
<h2 id="settings-goals" class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
|
||||||
Goals & Streaks
|
Goals & Streaks
|
||||||
</h3>
|
</h2>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Daily goal</div>
|
<div class="text-[13px] text-white">Daily goal</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">Track daily break target</div>
|
<div class="text-[11px] text-text-sec">Track daily break target</div>
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch bind:checked={$config.daily_goal_enabled} label="Daily goal" onchange={markChanged} />
|
<ToggleSwitch bind:checked={$config.daily_goal_enabled} label="Daily goal" onchange={markChanged} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $config.daily_goal_enabled}
|
{#if $config.daily_goal_enabled}
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Target breaks</div>
|
<div class="text-[13px] text-white">Target breaks</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">{$config.daily_goal_breaks} per day</div>
|
<div class="text-[11px] text-text-sec">{$config.daily_goal_breaks} per day</div>
|
||||||
</div>
|
</div>
|
||||||
<Stepper bind:value={$config.daily_goal_breaks} label="Daily goal breaks" min={1} max={30} onchange={markChanged} />
|
<Stepper bind:value={$config.daily_goal_breaks} label="Daily goal breaks" min={1} max={30} onchange={markChanged} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Celebrations</div>
|
<div class="text-[13px] text-white">Celebrations</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">Confetti on milestones and goals</div>
|
<div class="text-[11px] text-text-sec">Confetti on milestones and goals</div>
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch bind:checked={$config.milestone_celebrations} label="Celebrations" onchange={markChanged} />
|
<ToggleSwitch bind:checked={$config.milestone_celebrations} label="Celebrations" onchange={markChanged} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Streak notifications</div>
|
<div class="text-[13px] text-white">Streak notifications</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">Toast on streak milestones</div>
|
<div class="text-[11px] text-text-sec">Toast on streak milestones</div>
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch bind:checked={$config.streak_notifications} label="Streak notifications" onchange={markChanged} />
|
<ToggleSwitch bind:checked={$config.streak_notifications} label="Streak notifications" onchange={markChanged} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 13. Appearance -->
|
<!-- 13. Appearance -->
|
||||||
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.36 }}>
|
<section aria-labelledby="settings-appearance" class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.36 }}>
|
||||||
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
|
<h2 id="settings-appearance" class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
|
||||||
Appearance
|
Appearance
|
||||||
</h3>
|
</h2>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">UI zoom</div>
|
<div class="text-[13px] text-white">UI zoom</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">
|
<div class="text-[11px] text-text-sec">
|
||||||
{$config.ui_zoom}%
|
{$config.ui_zoom}%
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -925,7 +928,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
|
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
label="Accent color"
|
label="Accent color"
|
||||||
@@ -933,7 +936,7 @@
|
|||||||
onchange={markChanged}
|
onchange={markChanged}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
|
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
label="Break screen color"
|
label="Break screen color"
|
||||||
@@ -947,19 +950,19 @@
|
|||||||
onchange={markChanged}
|
onchange={markChanged}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
|
|
||||||
<FontSelector
|
<FontSelector
|
||||||
bind:value={$config.countdown_font}
|
bind:value={$config.countdown_font}
|
||||||
onchange={markChanged}
|
onchange={markChanged}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Animated background</div>
|
<div class="text-[13px] text-white">Animated background</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">
|
<div class="text-[11px] text-text-sec">
|
||||||
Gradient blobs with film grain
|
Gradient blobs with film grain
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -972,11 +975,15 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 14. Working Hours -->
|
<!-- 14. Working Hours -->
|
||||||
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.39 }}>
|
<section aria-labelledby="settings-workinghours" class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.39 }}>
|
||||||
|
<h2 id="settings-workinghours" class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
|
||||||
|
Working Hours
|
||||||
|
</h2>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Working hours</div>
|
<div class="text-[13px] text-white">Working hours</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">
|
<div class="text-[11px] text-text-sec">
|
||||||
Only show breaks during your configured work schedule
|
Only show breaks during your configured work schedule
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -988,7 +995,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $config.working_hours_enabled}
|
{#if $config.working_hours_enabled}
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
|
|
||||||
{#each $config.working_hours_schedule as daySchedule, dayIndex}
|
{#each $config.working_hours_schedule as daySchedule, dayIndex}
|
||||||
{@const dayName = daysOfWeek[dayIndex]}
|
{@const dayName = daysOfWeek[dayIndex]}
|
||||||
@@ -1013,7 +1020,7 @@
|
|||||||
countdownFont={$config.countdown_font}
|
countdownFont={$config.countdown_font}
|
||||||
onchange={(v) => updateTimeRange(dayIndex, rangeIndex, "start", v)}
|
onchange={(v) => updateTimeRange(dayIndex, rangeIndex, "start", v)}
|
||||||
/>
|
/>
|
||||||
<span class="text-[#8a8a8a] text-[13px]">to</span>
|
<span class="text-text-sec text-[13px]">to</span>
|
||||||
<TimeSpinner
|
<TimeSpinner
|
||||||
value={range.end}
|
value={range.end}
|
||||||
countdownFont={$config.countdown_font}
|
countdownFont={$config.countdown_font}
|
||||||
@@ -1024,7 +1031,7 @@
|
|||||||
{#if rangeIndex === daySchedule.ranges.length - 1}
|
{#if rangeIndex === daySchedule.ranges.length - 1}
|
||||||
<button
|
<button
|
||||||
use:pressable
|
use:pressable
|
||||||
class="ml-2 w-8 h-8 flex items-center justify-center rounded-full text-[#8a8a8a] hover:text-white hover:bg-[#1a1a1a] transition-colors"
|
class="ml-2 w-8 h-8 flex items-center justify-center rounded-full text-text-sec hover:text-white hover:bg-[#1a1a1a] transition-colors"
|
||||||
onclick={() => addTimeRange(dayIndex)}
|
onclick={() => addTimeRange(dayIndex)}
|
||||||
aria-label="Add time range"
|
aria-label="Add time range"
|
||||||
>
|
>
|
||||||
@@ -1037,7 +1044,7 @@
|
|||||||
<!-- Clone button -->
|
<!-- Clone button -->
|
||||||
<button
|
<button
|
||||||
use:pressable
|
use:pressable
|
||||||
class="w-8 h-8 flex items-center justify-center rounded-full text-[#8a8a8a] hover:text-white hover:bg-[#1a1a1a] transition-colors"
|
class="w-8 h-8 flex items-center justify-center rounded-full text-text-sec hover:text-white hover:bg-[#1a1a1a] transition-colors"
|
||||||
onclick={() => cloneTimeRange(dayIndex, rangeIndex)}
|
onclick={() => cloneTimeRange(dayIndex, rangeIndex)}
|
||||||
aria-label="Clone time range"
|
aria-label="Clone time range"
|
||||||
>
|
>
|
||||||
@@ -1051,7 +1058,7 @@
|
|||||||
{#if rangeIndex > 0}
|
{#if rangeIndex > 0}
|
||||||
<button
|
<button
|
||||||
use:pressable
|
use:pressable
|
||||||
class="w-8 h-8 flex items-center justify-center rounded-full text-[#8a8a8a] hover:text-[#f85149] hover:bg-[#f8514915] transition-colors"
|
class="w-8 h-8 flex items-center justify-center rounded-full text-text-sec hover:text-[#ff6b6b] hover:bg-[#ff6b6b15] transition-colors"
|
||||||
onclick={() => removeTimeRange(dayIndex, rangeIndex)}
|
onclick={() => removeTimeRange(dayIndex, rangeIndex)}
|
||||||
aria-label="Remove time range"
|
aria-label="Remove time range"
|
||||||
>
|
>
|
||||||
@@ -1067,22 +1074,22 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if dayIndex < 6}
|
{#if dayIndex < 6}
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 15. Mini Mode -->
|
<!-- 15. Mini Mode -->
|
||||||
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.42 }}>
|
<section aria-labelledby="settings-minimode" class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.42 }}>
|
||||||
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
|
<h2 id="settings-minimode" class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
|
||||||
Mini Mode
|
Mini Mode
|
||||||
</h3>
|
</h2>
|
||||||
|
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-[13px] text-white">Click-through</div>
|
<div class="text-[13px] text-white">Click-through</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">
|
<div class="text-[11px] text-text-sec">
|
||||||
Mini timer ignores clicks until you hover over it
|
Mini timer ignores clicks until you hover over it
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1097,7 +1104,7 @@
|
|||||||
<div class="mt-4 flex items-center justify-between">
|
<div class="mt-4 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-[13px] text-white">Hover delay</div>
|
<div class="text-[13px] text-white">Hover delay</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">
|
<div class="text-[11px] text-text-sec">
|
||||||
Seconds to hover before it becomes draggable
|
Seconds to hover before it becomes draggable
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1115,15 +1122,15 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 16. General -->
|
<!-- 16. General -->
|
||||||
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.45 }}>
|
<section aria-labelledby="settings-general" class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.45 }}>
|
||||||
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
|
<h2 id="settings-general" class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
|
||||||
General
|
General
|
||||||
</h3>
|
</h2>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[13px] text-white">Start on Windows login</div>
|
<div class="text-[13px] text-white">Start on Windows login</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">Launch automatically at startup</div>
|
<div class="text-[11px] text-text-sec">Launch automatically at startup</div>
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
checked={autoStartEnabled}
|
checked={autoStartEnabled}
|
||||||
@@ -1134,42 +1141,44 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 17. Keyboard Shortcuts -->
|
<!-- 17. Keyboard Shortcuts -->
|
||||||
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.48 }}>
|
<section aria-labelledby="settings-shortcuts" class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.48 }}>
|
||||||
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
|
<h2 id="settings-shortcuts" class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
|
||||||
Keyboard Shortcuts
|
Keyboard Shortcuts
|
||||||
</h3>
|
</h2>
|
||||||
|
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-[13px] text-white">Pause / Resume</span>
|
<span class="text-[13px] text-white">Pause / Resume</span>
|
||||||
<kbd class="rounded-lg bg-[#161616] px-2.5 py-1 text-[11px] text-[#8a8a8a]">Ctrl+Shift+P</kbd>
|
<kbd class="rounded-lg bg-border px-2.5 py-1 text-[11px] text-text-sec">Ctrl+Shift+P</kbd>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-[13px] text-white">Start break now</span>
|
<span class="text-[13px] text-white">Start break now</span>
|
||||||
<kbd class="rounded-lg bg-[#161616] px-2.5 py-1 text-[11px] text-[#8a8a8a]">Ctrl+Shift+B</kbd>
|
<kbd class="rounded-lg bg-border px-2.5 py-1 text-[11px] text-text-sec">Ctrl+Shift+B</kbd>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-[13px] text-white">Show / Hide window</span>
|
<span class="text-[13px] text-white">Show / Hide window</span>
|
||||||
<kbd class="rounded-lg bg-[#161616] px-2.5 py-1 text-[11px] text-[#8a8a8a]">Ctrl+Shift+S</kbd>
|
<kbd class="rounded-lg bg-border px-2.5 py-1 text-[11px] text-text-sec">Ctrl+Shift+S</kbd>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 18. Reset -->
|
<!-- 18. Reset -->
|
||||||
<div class="pt-2 pb-6" use:inView={{ delay: 0.51 }}>
|
<section aria-labelledby="settings-reset" class="pt-2 pb-6" use:inView={{ delay: 0.51 }}>
|
||||||
|
<h2 id="settings-reset" class="sr-only">Reset</h2>
|
||||||
<button
|
<button
|
||||||
use:pressable
|
use:pressable
|
||||||
|
aria-live="polite"
|
||||||
class="w-full rounded-full border py-3 text-[12px]
|
class="w-full rounded-full border py-3 text-[12px]
|
||||||
tracking-wider uppercase
|
tracking-wider uppercase
|
||||||
transition-all duration-200
|
transition-all duration-200
|
||||||
{resetConfirming
|
{resetConfirming
|
||||||
? 'border-[#f85149] text-[#f85149] hover:bg-[#f85149] hover:text-white'
|
? 'border-[#ff6b6b] text-[#ff6b6b] hover:bg-[#ff6b6b] hover:text-white'
|
||||||
: 'border-[#1a1a1a] text-[#8a8a8a] hover:border-[#333] hover:text-white'}"
|
: 'border-[#1a1a1a] text-text-sec hover:border-[#333] hover:text-white'}"
|
||||||
onclick={handleReset}
|
onclick={handleReset}
|
||||||
>
|
>
|
||||||
{resetConfirming ? "Tap again to confirm reset" : "Reset to defaults"}
|
{resetConfirming ? "Tap again to confirm reset" : "Reset to defaults"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -156,7 +156,7 @@
|
|||||||
|
|
||||||
// Day label — show every Nth for 30-day
|
// Day label — show every Nth for 30-day
|
||||||
if (data.length <= 7 || i % 5 === 0) {
|
if (data.length <= 7 || i % 5 === 0) {
|
||||||
ctx.fillStyle = "#8a8a8a";
|
ctx.fillStyle = "#a8a8a8";
|
||||||
ctx.font = "10px -apple-system, sans-serif";
|
ctx.font = "10px -apple-system, sans-serif";
|
||||||
ctx.textAlign = "center";
|
ctx.textAlign = "center";
|
||||||
const label = day.date.slice(5);
|
const label = day.date.slice(5);
|
||||||
@@ -266,8 +266,8 @@
|
|||||||
<button
|
<button
|
||||||
aria-label="Back to dashboard"
|
aria-label="Back to dashboard"
|
||||||
use:pressable
|
use:pressable
|
||||||
class="mr-3 flex h-8 w-8 items-center justify-center rounded-full
|
class="mr-3 flex h-10 w-10 min-h-[44px] min-w-[44px] items-center justify-center rounded-full
|
||||||
text-[#8a8a8a] transition-colors hover:text-white"
|
text-text-sec transition-colors hover:text-white"
|
||||||
onclick={goBack}
|
onclick={goBack}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -294,14 +294,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab navigation -->
|
<!-- Tab navigation -->
|
||||||
<div class="flex gap-1 px-5 mb-3" use:fadeIn={{ duration: 0.3, y: 6 }}>
|
<div class="flex gap-1 px-5 mb-3" role="tablist" aria-label="Statistics time range" use:fadeIn={{ duration: 0.3, y: 6 }}>
|
||||||
{#each [["today", "Today"], ["weekly", "Weekly"], ["monthly", "Monthly"]] as [tab, label]}
|
{#each [["today", "Today"], ["weekly", "Weekly"], ["monthly", "Monthly"]] as [tab, label]}
|
||||||
<button
|
<button
|
||||||
use:pressable
|
use:pressable
|
||||||
class="rounded-lg px-4 py-1.5 text-[11px] tracking-wider uppercase transition-all duration-200
|
role="tab"
|
||||||
|
id="tab-{tab}"
|
||||||
|
aria-selected={activeTab === tab}
|
||||||
|
aria-controls="tabpanel-{tab}"
|
||||||
|
class="min-h-[44px] rounded-lg px-4 py-1.5 text-[11px] tracking-wider uppercase transition-all duration-200
|
||||||
{activeTab === tab
|
{activeTab === tab
|
||||||
? 'bg-[#1a1a1a] text-white'
|
? 'bg-[#1a1a1a] text-white'
|
||||||
: 'text-[#8a8a8a] hover:text-white'}"
|
: 'text-text-sec hover:text-white'}"
|
||||||
onclick={() => activeTab = tab as any}
|
onclick={() => activeTab = tab as any}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
@@ -314,18 +318,19 @@
|
|||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
|
|
||||||
{#if activeTab === "today"}
|
{#if activeTab === "today"}
|
||||||
|
<div role="tabpanel" id="tabpanel-today" aria-labelledby="tab-today">
|
||||||
<!-- Today's summary -->
|
<!-- Today's summary -->
|
||||||
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0 }}>
|
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0 }}>
|
||||||
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
|
<h2 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
|
||||||
Today
|
Today
|
||||||
</h3>
|
</h2>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="text-[28px] font-semibold text-white tabular-nums">
|
<div class="text-[28px] font-semibold text-white tabular-nums">
|
||||||
{stats?.todayCompleted ?? 0}
|
{stats?.todayCompleted ?? 0}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">Breaks taken</div>
|
<div class="text-[11px] text-text-sec">Breaks taken</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="text-[28px] font-semibold tabular-nums"
|
<div class="text-[28px] font-semibold tabular-nums"
|
||||||
@@ -333,19 +338,19 @@
|
|||||||
>
|
>
|
||||||
{compliancePercent}%
|
{compliancePercent}%
|
||||||
</div>
|
</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">Compliance</div>
|
<div class="text-[11px] text-text-sec">Compliance</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="text-[28px] font-semibold text-white tabular-nums">
|
<div class="text-[28px] font-semibold text-white tabular-nums">
|
||||||
{breakTimeFormatted()}
|
{breakTimeFormatted()}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">Break time</div>
|
<div class="text-[11px] text-text-sec">Break time</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="text-[28px] font-semibold text-white tabular-nums">
|
<div class="text-[28px] font-semibold text-white tabular-nums">
|
||||||
{stats?.todaySkipped ?? 0}
|
{stats?.todaySkipped ?? 0}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">Skipped</div>
|
<div class="text-[11px] text-text-sec">Skipped</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -353,9 +358,9 @@
|
|||||||
<!-- F10: Daily goal -->
|
<!-- F10: Daily goal -->
|
||||||
{#if $config.daily_goal_enabled}
|
{#if $config.daily_goal_enabled}
|
||||||
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.04 }}>
|
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.04 }}>
|
||||||
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
|
<h2 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
|
||||||
Daily Goal
|
Daily Goal
|
||||||
</h3>
|
</h2>
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<div class="relative w-16 h-16">
|
<div class="relative w-16 h-16">
|
||||||
<svg width="64" height="64" viewBox="0 0 64 64" style="transform: rotate(-90deg);">
|
<svg width="64" height="64" viewBox="0 0 64 64" style="transform: rotate(-90deg);">
|
||||||
@@ -375,7 +380,7 @@
|
|||||||
<div class="text-[14px] text-white font-medium">
|
<div class="text-[14px] text-white font-medium">
|
||||||
{stats?.dailyGoalProgress ?? 0} / {$config.daily_goal_breaks} breaks
|
{stats?.dailyGoalProgress ?? 0} / {$config.daily_goal_breaks} breaks
|
||||||
</div>
|
</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">
|
<div class="text-[11px] text-text-sec">
|
||||||
{stats?.dailyGoalMet ? "Goal reached!" : `${$config.daily_goal_breaks - (stats?.dailyGoalProgress ?? 0)} more to go`}
|
{stats?.dailyGoalMet ? "Goal reached!" : `${$config.daily_goal_breaks - (stats?.dailyGoalProgress ?? 0)} more to go`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -385,26 +390,26 @@
|
|||||||
|
|
||||||
<!-- Streak -->
|
<!-- Streak -->
|
||||||
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.06 }}>
|
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.06 }}>
|
||||||
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
|
<h2 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
|
||||||
Streak
|
Streak
|
||||||
</h3>
|
</h2>
|
||||||
|
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-[13px] text-white">Current streak</div>
|
<div class="text-[13px] text-white">Current streak</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">Consecutive days with breaks</div>
|
<div class="text-[11px] text-text-sec">Consecutive days with breaks</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-[24px] font-semibold tabular-nums" style="color: {$config.accent_color}">
|
<div class="text-[24px] font-semibold tabular-nums" style="color: {$config.accent_color}">
|
||||||
{stats?.currentStreak ?? 0}
|
{stats?.currentStreak ?? 0}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
|
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-[13px] text-white">Best streak</div>
|
<div class="text-[13px] text-white">Best streak</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">All-time record</div>
|
<div class="text-[11px] text-text-sec">All-time record</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-[24px] font-semibold text-white tabular-nums">
|
<div class="text-[24px] font-semibold text-white tabular-nums">
|
||||||
{stats?.bestStreak ?? 0}
|
{stats?.bestStreak ?? 0}
|
||||||
@@ -413,13 +418,13 @@
|
|||||||
|
|
||||||
<!-- F10: Next milestone -->
|
<!-- F10: Next milestone -->
|
||||||
{#if nextMilestone()}
|
{#if nextMilestone()}
|
||||||
<div class="my-4 h-px bg-[#161616]"></div>
|
<div class="my-4 h-px bg-border"></div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-[13px] text-white">Next milestone</div>
|
<div class="text-[13px] text-white">Next milestone</div>
|
||||||
<div class="text-[11px] text-[#8a8a8a]">{nextMilestone()} day streak</div>
|
<div class="text-[11px] text-text-sec">{nextMilestone()} day streak</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-[13px] text-[#8a8a8a] tabular-nums">
|
<div class="text-[13px] text-text-sec tabular-nums">
|
||||||
{nextMilestone()! - (stats?.currentStreak ?? 0)} days away
|
{nextMilestone()! - (stats?.currentStreak ?? 0)} days away
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -428,9 +433,9 @@
|
|||||||
|
|
||||||
<!-- Weekly chart -->
|
<!-- Weekly chart -->
|
||||||
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.12 }}>
|
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.12 }}>
|
||||||
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
|
<h2 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
|
||||||
Last 7 Days
|
Last 7 Days
|
||||||
</h3>
|
</h2>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y_no_interactive_element_to_noninteractive_role -->
|
<!-- svelte-ignore a11y_no_interactive_element_to_noninteractive_role -->
|
||||||
<canvas
|
<canvas
|
||||||
@@ -458,7 +463,7 @@
|
|||||||
</table>
|
</table>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="mt-3 flex items-center justify-center gap-4 text-[10px] text-[#8a8a8a]">
|
<div class="mt-3 flex items-center justify-center gap-4 text-[10px] text-text-sec">
|
||||||
<div class="flex items-center gap-1.5">
|
<div class="flex items-center gap-1.5">
|
||||||
<div class="h-2 w-2 rounded-sm" style="background: {$config.accent_color}"></div>
|
<div class="h-2 w-2 rounded-sm" style="background: {$config.accent_color}"></div>
|
||||||
Completed
|
Completed
|
||||||
@@ -470,38 +475,40 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
{:else if activeTab === "weekly"}
|
{:else if activeTab === "weekly"}
|
||||||
|
<div role="tabpanel" id="tabpanel-weekly" aria-labelledby="tab-weekly">
|
||||||
<!-- Weekly summaries -->
|
<!-- Weekly summaries -->
|
||||||
{#each weeklySummaries as week, i}
|
{#each weeklySummaries as week, i}
|
||||||
{@const prevWeek = weeklySummaries[i + 1]}
|
{@const prevWeek = weeklySummaries[i + 1]}
|
||||||
{@const trend = prevWeek ? week.complianceRate - prevWeek.complianceRate : 0}
|
{@const trend = prevWeek ? week.complianceRate - prevWeek.complianceRate : 0}
|
||||||
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: i * 0.06 }}>
|
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: i * 0.06 }}>
|
||||||
<h3 class="mb-3 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
|
<h2 class="mb-3 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
|
||||||
Week of {week.weekStart}
|
Week of {week.weekStart}
|
||||||
</h3>
|
</h2>
|
||||||
|
|
||||||
<div class="grid grid-cols-3 gap-3 mb-3">
|
<div class="grid grid-cols-3 gap-3 mb-3">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="text-[22px] font-semibold text-white tabular-nums">{week.totalCompleted}</div>
|
<div class="text-[22px] font-semibold text-white tabular-nums">{week.totalCompleted}</div>
|
||||||
<div class="text-[10px] text-[#8a8a8a]">Completed</div>
|
<div class="text-[10px] text-text-sec">Completed</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="text-[22px] font-semibold text-white tabular-nums">{week.totalSkipped}</div>
|
<div class="text-[22px] font-semibold text-white tabular-nums">{week.totalSkipped}</div>
|
||||||
<div class="text-[10px] text-[#8a8a8a]">Skipped</div>
|
<div class="text-[10px] text-text-sec">Skipped</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="text-[22px] font-semibold tabular-nums" style="color: {$config.accent_color}">
|
<div class="text-[22px] font-semibold tabular-nums" style="color: {$config.accent_color}">
|
||||||
{Math.round(week.complianceRate * 100)}%
|
{Math.round(week.complianceRate * 100)}%
|
||||||
</div>
|
</div>
|
||||||
<div class="text-[10px] text-[#8a8a8a]">Compliance</div>
|
<div class="text-[10px] text-text-sec">Compliance</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-between text-[11px]">
|
<div class="flex items-center justify-between text-[11px]">
|
||||||
<span class="text-[#8a8a8a]">Avg {week.avgDailyCompleted.toFixed(1)} breaks/day</span>
|
<span class="text-text-sec">Avg {week.avgDailyCompleted.toFixed(1)} breaks/day</span>
|
||||||
{#if prevWeek}
|
{#if prevWeek}
|
||||||
<span class="flex items-center gap-1"
|
<span class="flex items-center gap-1"
|
||||||
style="color: {trend > 0 ? '#3fb950' : trend < 0 ? '#f85149' : '#8a8a8a'};"
|
style="color: {trend > 0 ? '#3fb950' : trend < 0 ? '#ff6b6b' : '#a8a8a8'};"
|
||||||
>
|
>
|
||||||
{#if trend > 0}
|
{#if trend > 0}
|
||||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M18 15l-6-6-6 6"/></svg>
|
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M18 15l-6-6-6 6"/></svg>
|
||||||
@@ -517,12 +524,14 @@
|
|||||||
</section>
|
</section>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
<div role="tabpanel" id="tabpanel-monthly" aria-labelledby="tab-monthly">
|
||||||
<!-- Monthly: 30-day chart -->
|
<!-- Monthly: 30-day chart -->
|
||||||
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0 }}>
|
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0 }}>
|
||||||
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
|
<h2 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
|
||||||
Last 30 Days
|
Last 30 Days
|
||||||
</h3>
|
</h2>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y_no_interactive_element_to_noninteractive_role -->
|
<!-- svelte-ignore a11y_no_interactive_element_to_noninteractive_role -->
|
||||||
<canvas
|
<canvas
|
||||||
@@ -532,7 +541,21 @@
|
|||||||
aria-label="30-day break history chart"
|
aria-label="30-day break history chart"
|
||||||
></canvas>
|
></canvas>
|
||||||
|
|
||||||
<div class="mt-3 flex items-center justify-center gap-4 text-[10px] text-[#8a8a8a]">
|
{#if monthHistory.length > 0}
|
||||||
|
<table class="sr-only">
|
||||||
|
<caption>Break history for the last {monthHistory.length} days</caption>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Date</th><th>Completed</th><th>Skipped</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each monthHistory as day}
|
||||||
|
<tr><td>{day.date}</td><td>{day.breaksCompleted}</td><td>{day.breaksSkipped}</td></tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="mt-3 flex items-center justify-center gap-4 text-[10px] text-text-sec">
|
||||||
<div class="flex items-center gap-1.5">
|
<div class="flex items-center gap-1.5">
|
||||||
<div class="h-2 w-2 rounded-sm" style="background: {$config.accent_color}"></div>
|
<div class="h-2 w-2 rounded-sm" style="background: {$config.accent_color}"></div>
|
||||||
Completed
|
Completed
|
||||||
@@ -546,9 +569,9 @@
|
|||||||
|
|
||||||
<!-- Heatmap -->
|
<!-- Heatmap -->
|
||||||
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.06 }}>
|
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.06 }}>
|
||||||
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
|
<h2 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
|
||||||
Activity Heatmap
|
Activity Heatmap
|
||||||
</h3>
|
</h2>
|
||||||
|
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<!-- svelte-ignore a11y_no_interactive_element_to_noninteractive_role -->
|
<!-- svelte-ignore a11y_no_interactive_element_to_noninteractive_role -->
|
||||||
@@ -559,7 +582,21 @@
|
|||||||
></canvas>
|
></canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-3 flex items-center justify-center gap-2 text-[10px] text-[#8a8a8a]">
|
{#if monthHistory.length > 0}
|
||||||
|
<table class="sr-only">
|
||||||
|
<caption>Activity heatmap for the last {monthHistory.length} days</caption>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Date</th><th>Breaks completed</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each monthHistory as day}
|
||||||
|
<tr><td>{day.date}</td><td>{day.breaksCompleted}</td></tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="mt-3 flex items-center justify-center gap-2 text-[10px] text-text-sec">
|
||||||
<span>Less</span>
|
<span>Less</span>
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
<div class="w-3 h-3 rounded-sm" style="background: #161616;"></div>
|
<div class="w-3 h-3 rounded-sm" style="background: #161616;"></div>
|
||||||
@@ -574,35 +611,36 @@
|
|||||||
|
|
||||||
<!-- Monthly totals -->
|
<!-- Monthly totals -->
|
||||||
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.12 }}>
|
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.12 }}>
|
||||||
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
|
<h2 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
|
||||||
Monthly Summary
|
Monthly Summary
|
||||||
</h3>
|
</h2>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="text-[22px] font-semibold text-white tabular-nums">{monthTotalCompleted}</div>
|
<div class="text-[22px] font-semibold text-white tabular-nums">{monthTotalCompleted}</div>
|
||||||
<div class="text-[10px] text-[#8a8a8a]">Total breaks</div>
|
<div class="text-[10px] text-text-sec">Total breaks</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="text-[22px] font-semibold tabular-nums" style="color: {$config.accent_color}">
|
<div class="text-[22px] font-semibold tabular-nums" style="color: {$config.accent_color}">
|
||||||
{monthAvgCompliance()}%
|
{monthAvgCompliance()}%
|
||||||
</div>
|
</div>
|
||||||
<div class="text-[10px] text-[#8a8a8a]">Avg compliance</div>
|
<div class="text-[10px] text-text-sec">Avg compliance</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="text-[22px] font-semibold text-white tabular-nums">
|
<div class="text-[22px] font-semibold text-white tabular-nums">
|
||||||
{Math.floor(monthTotalTime / 60)} min
|
{Math.floor(monthTotalTime / 60)} min
|
||||||
</div>
|
</div>
|
||||||
<div class="text-[10px] text-[#8a8a8a]">Total break time</div>
|
<div class="text-[10px] text-text-sec">Total break time</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="text-[22px] font-semibold text-white tabular-nums">
|
<div class="text-[22px] font-semibold text-white tabular-nums">
|
||||||
{(monthTotalCompleted / 30).toFixed(1)}
|
{(monthTotalCompleted / 30).toFixed(1)}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-[10px] text-[#8a8a8a]">Avg daily breaks</div>
|
<div class="text-[10px] text-text-sec">Avg daily breaks</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -55,20 +55,34 @@
|
|||||||
if (holdTimer) { clearTimeout(holdTimer); holdTimer = null; }
|
if (holdTimer) { clearTimeout(holdTimer); holdTimer = null; }
|
||||||
if (holdInterval) { clearTimeout(holdInterval as unknown as ReturnType<typeof setTimeout>); holdInterval = null; }
|
if (holdInterval) { clearTimeout(holdInterval as unknown as ReturnType<typeof setTimeout>); holdInterval = null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleKeydown(fn: () => void, e: KeyboardEvent) {
|
||||||
|
if (["Enter", " "].includes(e.key)) {
|
||||||
|
e.preventDefault();
|
||||||
|
startHold(fn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyup(e: KeyboardEvent) {
|
||||||
|
if (["Enter", " "].includes(e.key)) {
|
||||||
|
stopHold();
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex items-center gap-1.5" role="group" aria-label={label}>
|
<div class="flex items-center gap-1.5" role="group" aria-label={label}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Decrease"
|
aria-label="Decrease"
|
||||||
class="flex h-7 w-7 items-center justify-center rounded-lg
|
class="flex h-9 w-9 min-h-[44px] min-w-[44px] items-center justify-center rounded-lg
|
||||||
bg-[#141414] text-[#8a8a8a] transition-colors
|
border border-[#3a3a3a] bg-[#1a1a1a] text-text-sec transition-colors
|
||||||
hover:bg-[#1c1c1c] hover:text-white
|
hover:bg-[#1c1c1c] hover:text-white
|
||||||
disabled:opacity-20"
|
disabled:opacity-20"
|
||||||
onmousedown={() => startHold(decrement)}
|
onmousedown={() => startHold(decrement)}
|
||||||
onmouseup={stopHold}
|
onmouseup={stopHold}
|
||||||
onmouseleave={stopHold}
|
onmouseleave={stopHold}
|
||||||
onclick={(e) => { if (e.detail === 0) decrement(); }}
|
onkeydown={(e) => handleKeydown(decrement, e)}
|
||||||
|
onkeyup={handleKeyup}
|
||||||
disabled={value <= min}
|
disabled={value <= min}
|
||||||
>
|
>
|
||||||
−
|
−
|
||||||
@@ -79,14 +93,15 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Increase"
|
aria-label="Increase"
|
||||||
class="flex h-7 w-7 items-center justify-center rounded-lg
|
class="flex h-9 w-9 min-h-[44px] min-w-[44px] items-center justify-center rounded-lg
|
||||||
bg-[#141414] text-[#8a8a8a] transition-colors
|
border border-[#3a3a3a] bg-[#1a1a1a] text-text-sec transition-colors
|
||||||
hover:bg-[#1c1c1c] hover:text-white
|
hover:bg-[#1c1c1c] hover:text-white
|
||||||
disabled:opacity-20"
|
disabled:opacity-20"
|
||||||
onmousedown={() => startHold(increment)}
|
onmousedown={() => startHold(increment)}
|
||||||
onmouseup={stopHold}
|
onmouseup={stopHold}
|
||||||
onmouseleave={stopHold}
|
onmouseleave={stopHold}
|
||||||
onclick={(e) => { if (e.detail === 0) increment(); }}
|
onkeydown={(e) => handleKeydown(increment, e)}
|
||||||
|
onkeyup={handleKeyup}
|
||||||
disabled={value >= max}
|
disabled={value >= max}
|
||||||
>
|
>
|
||||||
+
|
+
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Invisible drag region – traffic lights on the right -->
|
<!-- Invisible drag region – traffic lights on the right -->
|
||||||
<div
|
<header
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
class="group absolute top-0 left-0 right-0 z-50 flex h-10 items-center justify-end pr-3.5 select-none"
|
class="group absolute top-0 left-0 right-0 z-50 flex h-10 items-center justify-end pr-3.5 select-none"
|
||||||
>
|
>
|
||||||
@@ -20,15 +20,14 @@
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- Traffic light buttons (order: maximize, minimize, close → close is rightmost) -->
|
<!-- Traffic light buttons (order: maximize, minimize, close → close is rightmost) -->
|
||||||
<div class="flex items-center gap-[8px] opacity-10 transition-opacity duration-300 group-hover:opacity-100 group-focus-within:opacity-100">
|
<div class="flex items-center gap-0 opacity-10 transition-opacity duration-300 group-hover:opacity-100 group-focus-within:opacity-100">
|
||||||
<!-- Maximize (green) -->
|
<!-- Maximize (green) -->
|
||||||
<button
|
<button
|
||||||
aria-label="Maximize"
|
aria-label="Maximize"
|
||||||
class="traffic-btn group/btn relative flex h-[15px] w-[15px] items-center justify-center
|
class="group/btn relative flex h-[44px] w-[44px] items-center justify-center"
|
||||||
rounded-full bg-[#27C93F] transition-all duration-150
|
|
||||||
hover:brightness-110"
|
|
||||||
onclick={() => appWindow.toggleMaximize()}
|
onclick={() => appWindow.toggleMaximize()}
|
||||||
>
|
>
|
||||||
|
<span class="flex h-[20px] w-[20px] items-center justify-center rounded-full bg-[#27C93F] transition-all duration-150 group-hover/btn:brightness-110">
|
||||||
<svg
|
<svg
|
||||||
class="absolute opacity-0 transition-opacity duration-150 group-hover/btn:opacity-100"
|
class="absolute opacity-0 transition-opacity duration-150 group-hover/btn:opacity-100"
|
||||||
width="8"
|
width="8"
|
||||||
@@ -40,16 +39,16 @@
|
|||||||
<polyline points="7,3 7,1 5,1" stroke="#006500" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" fill="none" />
|
<polyline points="7,3 7,1 5,1" stroke="#006500" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" fill="none" />
|
||||||
<line x1="1" y1="7" x2="7" y2="1" stroke="#006500" stroke-width="1.2" stroke-linecap="round" />
|
<line x1="1" y1="7" x2="7" y2="1" stroke="#006500" stroke-width="1.2" stroke-linecap="round" />
|
||||||
</svg>
|
</svg>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Minimize (yellow) -->
|
<!-- Minimize (yellow) -->
|
||||||
<button
|
<button
|
||||||
aria-label="Minimize"
|
aria-label="Minimize"
|
||||||
class="traffic-btn group/btn relative flex h-[15px] w-[15px] items-center justify-center
|
class="group/btn relative flex h-[44px] w-[44px] items-center justify-center"
|
||||||
rounded-full bg-[#FFBD2E] transition-all duration-150
|
|
||||||
hover:brightness-110"
|
|
||||||
onclick={() => appWindow.minimize()}
|
onclick={() => appWindow.minimize()}
|
||||||
>
|
>
|
||||||
|
<span class="flex h-[20px] w-[20px] items-center justify-center rounded-full bg-[#FFBD2E] transition-all duration-150 group-hover/btn:brightness-110">
|
||||||
<svg
|
<svg
|
||||||
class="absolute opacity-0 transition-opacity duration-150 group-hover/btn:opacity-100"
|
class="absolute opacity-0 transition-opacity duration-150 group-hover/btn:opacity-100"
|
||||||
width="8"
|
width="8"
|
||||||
@@ -59,16 +58,16 @@
|
|||||||
>
|
>
|
||||||
<line x1="1" y1="1" x2="7" y2="1" stroke="#995700" stroke-width="1.3" stroke-linecap="round" />
|
<line x1="1" y1="1" x2="7" y2="1" stroke="#995700" stroke-width="1.3" stroke-linecap="round" />
|
||||||
</svg>
|
</svg>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Close (red) — rightmost -->
|
<!-- Close (red) — rightmost -->
|
||||||
<button
|
<button
|
||||||
aria-label="Close"
|
aria-label="Close"
|
||||||
class="traffic-btn group/btn relative flex h-[15px] w-[15px] items-center justify-center
|
class="group/btn relative flex h-[44px] w-[44px] items-center justify-center"
|
||||||
rounded-full bg-[#FF5F57] transition-all duration-150
|
|
||||||
hover:brightness-110"
|
|
||||||
onclick={() => appWindow.close()}
|
onclick={() => appWindow.close()}
|
||||||
>
|
>
|
||||||
|
<span class="flex h-[20px] w-[20px] items-center justify-center rounded-full bg-[#FF5F57] transition-all duration-150 group-hover/btn:brightness-110">
|
||||||
<svg
|
<svg
|
||||||
class="absolute opacity-0 transition-opacity duration-150 group-hover/btn:opacity-100"
|
class="absolute opacity-0 transition-opacity duration-150 group-hover/btn:opacity-100"
|
||||||
width="8"
|
width="8"
|
||||||
@@ -79,6 +78,7 @@
|
|||||||
<line x1="1.5" y1="1.5" x2="6.5" y2="6.5" stroke="#4a0002" stroke-width="1.3" stroke-linecap="round" />
|
<line x1="1.5" y1="1.5" x2="6.5" y2="6.5" stroke="#4a0002" stroke-width="1.3" stroke-linecap="round" />
|
||||||
<line x1="6.5" y1="1.5" x2="1.5" y2="6.5" stroke="#4a0002" stroke-width="1.3" stroke-linecap="round" />
|
<line x1="6.5" y1="1.5" x2="1.5" y2="6.5" stroke="#4a0002" stroke-width="1.3" stroke-linecap="round" />
|
||||||
</svg>
|
</svg>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</header>
|
||||||
|
|||||||
@@ -15,19 +15,24 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- 44px hit area wrapper (WCAG 2.5.8) with compact visual toggle inside -->
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
role="switch"
|
role="switch"
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
aria-checked={checked}
|
aria-checked={checked}
|
||||||
class="relative inline-flex h-[24px] w-[48px] shrink-0 cursor-pointer rounded-full
|
class="relative inline-flex min-h-[44px] min-w-[52px] shrink-0 cursor-pointer items-center justify-center
|
||||||
transition-colors duration-200 ease-in-out"
|
bg-transparent border-none p-0"
|
||||||
style="background: {checked ? $config.accent_color : '#1a1a1a'};"
|
|
||||||
onclick={toggle}
|
onclick={toggle}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="pointer-events-none inline-block h-[19px] w-[19px] rounded-full
|
class="inline-flex h-[28px] w-[52px] items-center rounded-full transition-colors duration-200 ease-in-out"
|
||||||
|
style="background: {checked ? $config.accent_color : '#1a1a1a'};"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="pointer-events-none inline-block h-[22px] w-[22px] rounded-full
|
||||||
shadow-sm transition-transform duration-200 ease-in-out
|
shadow-sm transition-transform duration-200 ease-in-out
|
||||||
{checked ? 'translate-x-[26px] bg-white' : 'translate-x-[3px] bg-[#444]'} mt-[2.5px]"
|
{checked ? 'translate-x-[27px] bg-white' : 'translate-x-[3px] bg-[#666]'}"
|
||||||
></span>
|
></span>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -140,15 +140,32 @@ export function pressable(node: HTMLElement) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onKeyDown(e: KeyboardEvent) {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
onDown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeyUp(e: KeyboardEvent) {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
onUp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
node.addEventListener("mousedown", onDown);
|
node.addEventListener("mousedown", onDown);
|
||||||
node.addEventListener("mouseup", onUp);
|
node.addEventListener("mouseup", onUp);
|
||||||
node.addEventListener("mouseleave", onUp);
|
node.addEventListener("mouseleave", onUp);
|
||||||
|
node.addEventListener("keydown", onKeyDown);
|
||||||
|
node.addEventListener("keyup", onKeyUp);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
destroy() {
|
destroy() {
|
||||||
node.removeEventListener("mousedown", onDown);
|
node.removeEventListener("mousedown", onDown);
|
||||||
node.removeEventListener("mouseup", onUp);
|
node.removeEventListener("mouseup", onUp);
|
||||||
node.removeEventListener("mouseleave", onUp);
|
node.removeEventListener("mouseleave", onUp);
|
||||||
|
node.removeEventListener("keydown", onKeyDown);
|
||||||
|
node.removeEventListener("keyup", onKeyUp);
|
||||||
active?.cancel();
|
active?.cancel();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -200,6 +217,8 @@ export function glowHover(
|
|||||||
|
|
||||||
node.addEventListener("mouseenter", onEnter);
|
node.addEventListener("mouseenter", onEnter);
|
||||||
node.addEventListener("mouseleave", onLeave);
|
node.addEventListener("mouseleave", onLeave);
|
||||||
|
node.addEventListener("focusin", onEnter);
|
||||||
|
node.addEventListener("focusout", onLeave);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
update(newOptions?: { color?: string }) {
|
update(newOptions?: { color?: string }) {
|
||||||
@@ -209,6 +228,8 @@ export function glowHover(
|
|||||||
destroy() {
|
destroy() {
|
||||||
node.removeEventListener("mouseenter", onEnter);
|
node.removeEventListener("mouseenter", onEnter);
|
||||||
node.removeEventListener("mouseleave", onLeave);
|
node.removeEventListener("mouseleave", onLeave);
|
||||||
|
node.removeEventListener("focusin", onEnter);
|
||||||
|
node.removeEventListener("focusout", onLeave);
|
||||||
enterAnim?.cancel();
|
enterAnim?.cancel();
|
||||||
leaveAnim?.cancel();
|
leaveAnim?.cancel();
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user