26 Commits

Author SHA1 Message Date
Your Name 123448e904 v0.2.0: WCAG 2.2 AAA accessibility + toggle fix + version bump
- Upgrade accessibility from WCAG 2.1 AA to WCAG 2.2 AAA conformance
- 42 accessibility fixes across 18 frontend components
- Enhanced contrast ratios (7:1 body text, 4.5:1 large text, 3:1 non-text)
- 44px minimum touch/click targets on all interactive elements
- Full WAI-ARIA 1.2: tablist, radiogroup, alertdialog, progressbar, switch
- Screen-reader-only data tables behind chart, dynamic page titles
- Skip navigation link, semantic heading hierarchy (h1 > h2)
- Celebration popups persist on hover/focus with keyboard dismiss
- Fix ToggleSwitch visual height (44px hit area, 28px visual track)
- Update README with detailed WCAG 2.2 AAA accessibility section
- Include WCAG design doc and implementation plan in docs/plans/
2026-02-18 19:18:15 +02:00
Your Name 2429f7c801 fix: build.rs cfg() checks HOST not TARGET — use CARGO_CFG_TARGET_ENV
The #[cfg(target_env = "gnu")] attribute in build.rs checks the host
compiler environment, not the build target. On MSVC-hosted Rust
targeting x86_64-pc-windows-gnu, this was silently false, causing:

1. WebView2Loader static linking swap to never run
2. Resource compilation fix (windres COFF output) to never run

embed-resource finds MSVC's rc.exe via Windows SDK and produces .res
files that GNU ld can't link. Fix: detect .res format (null header)
and re-compile with MinGW windres --output-format=coff.
2026-02-18 19:18:15 +02:00
Your Name 60a13fe14e a11y: final cleanup — remaining hardcoded colors
- FontSelector: text-[#8a8a8a]→text-text-sec, border-[#222]→border-border
- StatsView: Canvas fillStyle and inline trend color #8a8a8a→#a8a8a8
- ActivityManager: border-[#222]→border-border, #f85149→#ff6b6b danger color
- Settings: #f85149→#ff6b6b danger color on reset/delete buttons
2026-02-18 19:18:15 +02:00
Your Name 5c064b2dc9 a11y: Tasks 9,13-17 — Settings ARIA, MiniTimer, overlays, ColorPicker, ActivityManager
- Settings: breathing radiogroup/radio, sound aria-pressed, contrast tokens,
  placeholder contrast, reset aria-live, abbreviation tags, title tooltips,
  back button 44px target
- MiniTimer: paused text #555→#a8a8a8 for AAA contrast
- MicrobreakOverlay: alertdialog role, sr-only heading, aria-describedby
- BreakOverlay: alertdialog role, sr-only heading, aria-label
- ColorPicker: enlarge swatches 22→28px with 44px hit areas, aria-pressed
- ActivityManager: enlarge action buttons to 44px targets, contrast tokens
2026-02-18 19:18:15 +02:00
Your Name a8ab3db01f a11y: Tasks 7-12 — Dashboard, Settings, StatsView, BreakScreen, Celebration
- Dashboard: text-text-sec tokens, nav landmark, toast hover persistence,
  goal progressbar ARIA, pomodoro sr-only text
- Settings: h3→h2 heading hierarchy, section aria-labelledby with ids,
  Working Hours heading added
- StatsView: h3→h2, tablist/tab/tabpanel ARIA pattern, sr-only data tables
  for 30-day chart and heatmap, contrast tokens
- BreakScreen: strict-mode focus safety span, breathing phase-only
  announcements, contrast tokens
- Celebration: JS-controlled hover/focus persistence, dismiss buttons,
  Escape key, removed pointer-events:none
- Titlebar: removed redundant role="banner" on <header>
2026-02-18 19:18:15 +02:00
Your Name 78f13eddaf a11y: Tasks 2-6 — App shell, Titlebar, ToggleSwitch, Stepper, animate.ts
- Add skip-to-content link and dynamic document title (App.svelte)
- Wrap titlebar in header landmark, enlarge traffic lights to 44px (Titlebar.svelte)
- Enlarge toggle switch to 52x28, improve OFF knob contrast (ToggleSwitch.svelte)
- Enlarge stepper buttons to 36px, add keyboard hold-to-repeat (Stepper.svelte)
- Add keyboard feedback to pressable, focus glow to glowHover (animate.ts)
2026-02-18 19:18:15 +02:00
Your Name 7efd19ff84 a11y: update theme tokens and global styles for WCAG AAA 2026-02-18 19:18:15 +02:00
lashman 43571d8584 Update README.md 2026-02-07 13:27:24 +00:00
Your Name 983e63ef4d Add emojis to all top-level README section headers 2026-02-07 15:24:03 +02:00
Your Name 7622d01c9b Add emojis to feature section headers in README 2026-02-07 15:22:11 +02:00
Your Name 66c534330b Add pomodoro, microbreaks, breathing guide, screen dimming, presentation mode, goals, multi-monitor, and activity manager
Major feature release (v0.1.3) adding 15 new features to the break timer:

Backend (Rust):
- Pomodoro cycle tracking with configurable short/long break pattern
- Microbreak scheduling (20-20-20 rule) with independent timer
- Screen dimming events with gradual opacity progression
- Presentation mode detection (fullscreen app deferral)
- Smart break detection (natural idle breaks counting toward goals)
- Daily goal tracking and streak milestone events
- Multi-monitor break overlay support
- Working hours enforcement with per-day schedules
- Weekly summary and natural break stats queries
- Config expanded to 71 validated fields

Frontend (Svelte):
- 6 new components: BreathingGuide, ActivityManager, BreakOverlay,
  MicrobreakOverlay, DimOverlay, Celebration
- Breathing guide with 5 patterns and animated pulsing halo
- Activity manager with favorites, custom activities, momentum scroll
- Confetti celebrations on milestones and goal completion
- Dashboard indicators (pomodoro/microbreak/goal) moved inside ring
- Settings reorganized into 18 logical cards
- Breathing pattern selector redesigned with timing descriptions
- Break activities expanded from 40 to 71 curated exercises
- Sound presets expanded from 4 to 8
- Stats view with weekly summary and natural break tracking

Also: version bump to 0.1.3, CHANGELOG, README and CLAUDE.md updates
2026-02-07 15:11:44 +02:00
Your Name d09affc4c4 Bump version to 0.1.2 2026-02-07 12:16:03 +02:00
Your Name e757d4ee30 Add WCAG 2.1 Level AA accessibility across all components
A break timer designed to prevent RSI should be usable by people who
already live with disabilities. This overhaul adds comprehensive
accessibility without changing the visual design.

Changes across 17 source files:
- Global focus-visible outlines, sr-only utility, forced-colors support
- prefers-reduced-motion kills all CSS animations AND JS Web Animations
- All text upgraded to 4.5:1+ contrast ratio (WCAG AA)
- Keyboard navigation for ColorPicker, Stepper, TimeSpinner
- Screen reader: aria-live status regions, progressbar roles, labeled
  controls, sr-only chart data table, focus management on view changes
- Focus trap on break screen, aria-hidden on decorative elements
- Descriptive labels on all 25+ toggle/stepper instances in Settings
- README updated with accessibility section and WCAG badge
2026-02-07 12:13:03 +02:00
lashman f03425fa59 Update README.md 2026-02-07 09:35:24 +00:00
lashman a9103ef085 Update README.md 2026-02-07 09:34:36 +00:00
Your Name c8d2e40ca1 Update README - fix download links, activity count, and accuracy
- Fix download links to point to Gitea releases page (not GitHub-style relative URLs)
- Correct activity count from 70 to 72
- Enlarge break screen screenshot
- Add missing IPC entries (natural-break-detected event, cursor/window commands)
- Add msvc_compat.rs to backend modules table
- Update winapi description to include WebView2 check dialog
- Remove unused dirs crate from dependencies
2026-02-07 11:24:16 +02:00
Your Name 39f9461475 Bump version to 0.1.1 2026-02-07 11:16:29 +02:00
Your Name 88ccc2cf60 Tighten TimeSpinner spacing and fix build warnings
Reduce gap between numbers and h/m unit labels in the workday
schedule spinners. Remove unused parse_hour function and fix
Svelte state_referenced_locally warning in BreakScreen.
2026-02-07 11:16:29 +02:00
Your Name 5109310059 Enable custom-protocol for embedded frontend assets
Without this feature, Tauri falls back to devUrl even in release
builds. The tauri CLI adds it automatically but direct cargo
builds need it in Cargo.toml.
2026-02-07 11:00:45 +02:00
Your Name 2a12e04788 Fix WebView2 detection - use loader API instead of registry
Registry check gave false negatives on systems where WebView2
is installed through Edge rather than EdgeUpdate. Now calls
GetAvailableCoreWebView2BrowserVersionString (statically linked)
which detects all installation methods.
2026-02-07 10:51:49 +02:00
Your Name 44cf97a1ea Statically link WebView2Loader - single exe, no DLL needed
build.rs swaps the dynamic import library with the static archive
from webview2-com-sys, so the WebView2 loader code is baked into
the exe. msvc_compat.rs provides the MSVC CRT symbols (security
cookie, thread-safe init, C++ operators) that the MSVC-compiled
static library expects.
2026-02-07 02:32:54 +02:00
Your Name 9bfd6ecffe Detect missing WebView2 at startup, show download link
Checks Windows registry for WebView2 Runtime before Tauri
initializes. If missing, shows a native MessageBox explaining
the requirement and opens the Microsoft download page.
2026-02-07 02:08:51 +02:00
Your Name 82d1784efa Fix fullscreen break screen - centering, progress bar, ripple sizing
- Center break content on fullscreen (was top-left aligned)
- Move fullscreen progress bar to screen bottom (was overlapping buttons)
- Scale ripple circles to match ring size per mode (140/160/200px)
- Increase ripple expansion for more dramatic spread
2026-02-07 01:56:10 +02:00
Your Name 1514210c90 Add screenshots, move philosophy section above features 2026-02-07 01:36:43 +02:00
Your Name f15ad48489 Revamp README - emojis, better formatting, cleaner layout 2026-02-07 01:18:48 +02:00
Your Name 9a72f1c670 Initial commit — Core Cooldown v0.1.0
Portable Windows break timer to prevent RSI and eye strain.
Tauri v2 + Svelte 5 + Tailwind CSS v4. No installer, no telemetry,
no data leaves the machine. CC0 public domain.
2026-02-07 01:12:49 +02:00
17 changed files with 1327 additions and 37 deletions
+27
View File
@@ -0,0 +1,27 @@
# Dependencies
node_modules/
# Build output
dist/
# Rust build artifacts
src-tauri/target/
# Generated Tauri schemas (auto-regenerated on build)
src-tauri/gen/schemas/
# Runtime portable data (created next to exe)
config.json
stats.json
data/
# OS files
Thumbs.db
.DS_Store
# Editor
.vscode/
.idea/
*.swp
*.swo
*~
+78
View File
@@ -0,0 +1,78 @@
# Changelog
## v0.1.3
### New Features
- **Pomodoro Mode** — Alternates short breaks with a longer recovery break after configurable focus sessions. Cycle indicator on dashboard, custom long break titles/messages, reset-on-skip option.
- **Microbreaks (20-20-20 Rule)** — Independent short eye breaks between main breaks. Subtle non-blocking overlay with activity suggestion and optional sound. Configurable frequency (5-60 min) and duration (10-60 sec).
- **Breathing Guide** — Visual breathing exercise during breaks with 5 patterns (Box, Relaxing, Energizing, Calm, Deep). Animated pulsing halo behind countdown ring with color interpolation between accent and break colors. Phase labels (Inhale/Hold/Exhale) with countdown.
- **Screen Dimming** — Gradual pre-break screen dimming. Configurable timing (3-60 sec before break) and maximum intensity (10-70%).
- **Presentation Mode** — Detects fullscreen applications and defers breaks until exit. Optional microbreak deferral and toast notification when breaks are deferred.
- **Goals & Streaks** — Daily break target with progress indicator inside dashboard ring. Confetti celebrations on milestones and goal completion. Streak tracking with toast notifications.
- **Multi-Monitor Breaks** — Fullscreen break overlay spans all connected monitors.
- **Working Hours Schedule** — Per-day schedule with multiple time ranges. Timer automatically pauses outside configured hours.
- **Activity Manager** — Browse, search, favorite, and disable built-in activities. Add custom activities with category assignment. Favorites appear 3x more often. Momentum drag scroll with elastic overscroll.
- **Celebration Animations** — Confetti particle effects on streak milestones and daily goal completion.
- **Smart Breaks** — Recognizes natural away-from-desk breaks (idle periods exceeding configurable threshold) and optionally counts them toward daily goal.
- **Break Window (Standalone)** — When fullscreen mode is disabled, breaks open in a separate centered transparent modal window instead of taking over the main window.
- **Break Overlay** — Multi-monitor break enforcement overlay that covers all screens during breaks.
- **Microbreak Overlay** — Subtle non-blocking overlay for 20-20-20 eye breaks with activity suggestion.
- **Dim Overlay** — Smooth pre-break screen dimming with gradual opacity transition.
### UI Polish
- **Settings Reordered** — 18 logically grouped cards: Timer, Pomodoro, Microbreaks, Break Screen, Break Activities (conditional), Breathing Guide, Behavior, Alerts (merged notifications + screen dimming), Sound, Idle & Smart Breaks (merged), Presentation Mode, Goals & Streaks, Appearance, Working Hours, Mini Mode, General, Keyboard Shortcuts, Reset.
- **Breathing Pattern Selector Redesigned** — Replaced cramped 5-column grid with vertical radio-button list showing timing descriptions (e.g., "4s in - 4s hold - 4s out - 4s hold").
- **Dashboard Indicators Inside Ring** — Pomodoro cycle dots, microbreak countdown, and daily goal progress bar moved inside the timer ring for cleaner layout.
- **Daily Goal Label** — Progress bar now includes a target icon and "Goal" label.
- **Break Screen Breathing Halo** — Breathing guide renders as a pulsing circle behind the countdown ring with dynamic color gradient, in both fullscreen and standalone modes.
### Improvements
- Expanded break activity library from 40 to 71 curated activities
- Added 4 new sound presets: Harp, Bowl, Rain, Whistle (now 8 total)
- Stats view now includes weekly summary, natural break tracking, and daily goal progress
- Timer store now handles microbreak, screen dim, celebration, and presentation mode events
- Config store expanded from ~30 to 71 keys with validation for all new features
- Timer state machine now tracks pomodoro cycles, microbreak scheduling, presentation mode deferral, and smart break detection
- Stats backend tracks natural breaks, daily goals, and weekly summaries
### Documentation
- README comprehensively updated with all new features, accurate component/activity/config counts
- CLAUDE.md updated with current architecture (17 commands, 12 events, 20 components, 71 config keys)
---
## v0.1.2
- WCAG 2.1 Level AA accessibility across all components
- Focus indicators, reduced motion support, forced colors, screen reader support
- Keyboard navigation, focus trapping, aria-live regions
## v0.1.1
- Tighten TimeSpinner spacing and fix build warnings
- Enable custom-protocol for embedded frontend assets
- Fix WebView2 detection using loader API instead of registry
- Statically link WebView2Loader for single portable exe
- Fix fullscreen break screen centering and sizing
## v0.1.0
- Initial release
- Dashboard with timer ring and status pill
- Break screen with activity suggestions
- Settings panel with sound, idle detection, and activity configuration
- Statistics view with 7-day bar chart
- System tray with dynamic icon, tooltip, and context menu
- Config and stats persistence (portable JSON files)
- Global keyboard shortcuts (Ctrl+Shift+P/B/S)
- Mini mode floating timer
- Toast notifications
- Always-on-top break enforcement
- Animated view transitions
- Idle detection via Windows API
- Custom titlebar with frosted glass effects
- Background gradient blobs with film grain
+218
View 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 |
@@ -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 320):
- 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 2235):
- 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 7376) 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 143145), 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 201202), 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 296382), 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 118126) 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 297310):
```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 217 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.
+2 -2
View File
@@ -8,7 +8,7 @@ fn main() {
}
// 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.
if std::env::var("CARGO_CFG_TARGET_ENV").as_deref() == Ok("gnu") {
swap_webview2_to_static();
}
@@ -44,7 +44,7 @@ fn fix_resource_lib() {
// 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
// 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([
+7 -7
View File
@@ -405,24 +405,24 @@ fn update_tray(
let tooltip = match snapshot.state {
timer::TimerState::Running => {
if snapshot.deferred_break_pending {
"Core Cooldown - Break deferred (fullscreen)".to_string()
"Core Cooldown Break deferred (fullscreen)".to_string()
} else {
let m = snapshot.time_remaining / 60;
let s = snapshot.time_remaining % 60;
format!("Core Cooldown - {:02}:{:02} until break", m, s)
format!("Core Cooldown {:02}:{:02} until break", m, s)
}
}
timer::TimerState::Paused => {
if snapshot.idle_paused {
"Core Cooldown - Paused (idle)".to_string()
"Core Cooldown Paused (idle)".to_string()
} else {
"Core Cooldown - Paused".to_string()
"Core Cooldown Paused".to_string()
}
}
timer::TimerState::BreakActive => {
let m = snapshot.break_time_remaining / 60;
let s = snapshot.break_time_remaining % 60;
format!("Core Cooldown - Break {:02}:{:02}", m, s)
format!("Core Cooldown Break {:02}:{:02}", m, s)
}
};
let _ = tray.set_tooltip(Some(&tooltip));
@@ -474,7 +474,7 @@ pub fn run() {
"break",
tauri::WebviewUrl::App("index.html?break=1".into()),
)
.title("Core Cooldown - Break")
.title("Core Cooldown \u{2014} Break")
.inner_size(900.0, 540.0)
.decorations(false)
.transparent(true)
@@ -721,7 +721,7 @@ pub fn run() {
.notification()
.builder()
.title("Break deferred")
.body("Fullscreen app detected - break will start when you exit.")
.body("Fullscreen app detected break will start when you exit.")
.show();
}
let _ = handle.emit("break-deferred", &());
+8 -8
View File
@@ -9,7 +9,7 @@ use std::sync::atomic::{AtomicI32, Ordering};
// ── MSVC Buffer Security Check (/GS) ────────────────────────────────────────
//
// MSVC's /GS flag instruments functions with stack canaries. These two symbols
// implement the canary check. The cookie value is arbitrary - real MSVC CRT
// implement the canary check. The cookie value is arbitrary real MSVC CRT
// randomises it at startup, but for a statically-linked helper library this
// fixed sentinel is sufficient.
@@ -44,15 +44,15 @@ pub unsafe extern "C" fn _Init_thread_header(guard: *mut i32) {
loop {
let val = guard.read_volatile();
if val == 0 {
// Already initialised - tell caller to skip
// Already initialised tell caller to skip
return;
}
if val == -1 {
// Not yet initialised - try to claim it
// Not yet initialised try to claim it
guard.write_volatile(1);
return;
}
// val == 1: another thread is initialising - yield and retry
// val == 1: another thread is initialising yield and retry
std::thread::yield_now();
}
}
@@ -71,22 +71,22 @@ pub unsafe extern "C" fn _Init_thread_footer(guard: *mut i32) {
// MinGW's libstdc++ exports the same operators but with GCC/Itanium mangling,
// so the linker can't match them. We provide the MSVC-mangled versions here.
/// `std::nothrow` - MSVC-mangled `?nothrow@std@@3Unothrow_t@1@B`
/// `std::nothrow` MSVC-mangled `?nothrow@std@@3Unothrow_t@1@B`
/// An empty struct constant used as a tag for nothrow `new`.
#[export_name = "?nothrow@std@@3Unothrow_t@1@B"]
pub static MSVC_STD_NOTHROW: u8 = 0;
/// `operator new(size_t, const std::nothrow_t&)` - nothrow allocation
/// `operator new(size_t, const std::nothrow_t&)` nothrow allocation
/// MSVC-mangled: `??2@YAPEAX_KAEBUnothrow_t@std@@@Z`
#[export_name = "??2@YAPEAX_KAEBUnothrow_t@std@@@Z"]
pub unsafe extern "C" fn msvc_operator_new_nothrow(size: usize, _nothrow: *const u8) -> *mut u8 {
let size = if size == 0 { 1 } else { size };
let layout = std::alloc::Layout::from_size_align_unchecked(size, 8);
let ptr = std::alloc::alloc(layout);
ptr // null on failure - nothrow semantics
ptr // null on failure nothrow semantics
}
/// `operator delete(void*, size_t)` - sized deallocation
/// `operator delete(void*, size_t)` sized deallocation
/// MSVC-mangled: `??3@YAXPEAX_K@Z`
#[export_name = "??3@YAXPEAX_K@Z"]
pub unsafe extern "C" fn msvc_operator_delete_sized(ptr: *mut u8, size: usize) {
+2 -2
View File
@@ -472,14 +472,14 @@ impl TimerManager {
let past_half = total > 0 && elapsed * 2 >= total;
if past_half && self.config.allow_end_early {
// "End break" - counts as completed
// "End break" counts as completed
self.has_had_break = true;
self.seconds_since_last_break = 0;
self.advance_pomodoro_cycle();
self.reset_timer();
true
} else if !past_half {
// "Cancel break" - doesn't count
// "Cancel break" doesn't count
// F3: Pomodoro reset-on-skip
if self.config.pomodoro_enabled && self.config.pomodoro_reset_on_skip {
self.pomodoro_cycle_position = 0;
+2 -2
View File
@@ -62,7 +62,7 @@
return () => mq.removeEventListener("change", handler);
});
// Transition parameters - zero when reduced motion active
// Transition parameters zero when reduced motion active
const DURATION = $derived(reducedMotion ? 0 : 700);
const easing = cubicOut;
@@ -83,7 +83,7 @@
settings: "Settings",
stats: "Statistics",
};
document.title = `Core Cooldown - ${viewNames[effectiveView] ?? "Dashboard"}`;
document.title = `Core Cooldown ${viewNames[effectiveView] ?? "Dashboard"}`;
});
// When fullscreen_mode is OFF, the separate break window handles breaks,
+2 -2
View File
@@ -86,7 +86,7 @@
}
});
// Map raw 0.6-1.0 scale to 0.9-1.6 range for visible breathing text
// Map raw 0.61.0 scale to 0.91.6 range for visible breathing text
const textScale = $derived(0.9 + (breathScale - 0.6) * (0.7 / 0.4));
// Interpolate color between break_color (inhale) and accent_color (exhale)
@@ -483,7 +483,7 @@
background: rgba(255, 255, 255, 0.03);
}
/* Ripple container - sits behind the ring, overflows the card */
/* Ripple container sits behind the ring, overflows the card */
.ripple-container {
position: absolute;
inset: 0;
+2 -2
View File
@@ -259,7 +259,7 @@
</button>
{/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
type="button"
class="group flex items-center justify-center min-h-[44px] min-w-[44px] bg-transparent border-none p-0"
@@ -277,7 +277,7 @@
</button>
</div>
<!-- Inline custom picker - slides open/closed -->
<!-- Inline custom picker slides open/closed -->
{#if showCustom}
<div
class="flex flex-col gap-3 rounded-xl bg-[#0f0f0f] p-3 border border-[#1a1a1a]"
+1 -1
View File
@@ -104,7 +104,7 @@
// Gap between ring and button, compensating for CSS transform phantom space.
// transform: scale() doesn't affect layout, so the 280px box stays full-size
// even when visually shrunk - creating phantom space below the visual ring.
// even when visually shrunk creating phantom space below the visual ring.
const ringSize = 280;
const phantomBelow = $derived((ringSize * (1 - ringScale)) / 2);
const targetGap = $derived(Math.max(16, Math.round((windowH - 400) * 0.05 + 16)));
+1 -1
View File
@@ -45,7 +45,7 @@
<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"/>
</svg>
<span id="microbreak-msg" 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>
{#if activity && $config.microbreak_show_activity}
+3 -3
View File
@@ -88,7 +88,7 @@
return milestones.find((m) => m > current) ?? null;
});
// Chart rendering - 7-day
// Chart rendering 7-day
let chartCanvas: HTMLCanvasElement | undefined = $state();
$effect(() => {
@@ -96,7 +96,7 @@
drawChart(chartCanvas, history);
});
// Chart rendering - 30-day
// Chart rendering 30-day
let monthChartCanvas: HTMLCanvasElement | undefined = $state();
$effect(() => {
@@ -154,7 +154,7 @@
ctx.fill();
}
// Day label - show every Nth for 30-day
// Day label show every Nth for 30-day
if (data.length <= 7 || i % 5 === 0) {
ctx.fillStyle = "#a8a8a8";
ctx.font = "10px -apple-system, sans-serif";
+2 -2
View File
@@ -7,7 +7,7 @@
let { value, onchange, countdownFont = "" }: Props = $props();
// Local display values - driven by prop normally, overridden during drag/momentum
// Local display values driven by prop normally, overridden during drag/momentum
let displayHours = $state(0);
let displayMinutes = $state(0);
let isAnimating = $state(false); // true during drag OR momentum
@@ -373,7 +373,7 @@
border-color: rgba(255, 255, 255, 0.18);
}
/* Perspective container - looking into the cylinder from outside */
/* Perspective container looking into the cylinder from outside */
.wheel-viewport {
width: 100%;
height: 100%;
+2 -2
View File
@@ -55,7 +55,7 @@
aria-label={label}
aria-valuetext={valueText}
>
<!-- Glow SVG - drawn larger than the container so blur isn't clipped -->
<!-- Glow SVG drawn larger than the container so blur isn't clipped -->
<svg
aria-hidden="true"
width={viewSize}
@@ -145,7 +145,7 @@
{/if}
</svg>
<!-- Non-glow SVG - exact size, draws the track + crisp ring -->
<!-- Non-glow SVG exact size, draws the track + crisp ring -->
<svg
aria-hidden="true"
width={size}
+3 -3
View File
@@ -4,12 +4,12 @@
const appWindow = getCurrentWebviewWindow();
</script>
<!-- Invisible drag region - traffic lights on the right -->
<!-- Invisible drag region traffic lights on the right -->
<header
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"
>
<!-- Centered app name (decorative - OS window title handles screen readers) -->
<!-- Centered app name (decorative OS window title handles screen readers) -->
<span
aria-hidden="true"
class="pointer-events-none absolute inset-0 flex items-center justify-center
@@ -61,7 +61,7 @@
</span>
</button>
<!-- Close (red) - rightmost -->
<!-- Close (red) rightmost -->
<button
aria-label="Close"
class="group/btn relative flex h-[44px] w-[44px] items-center justify-center"