# ZeroClock Enhancement Round 2 - Design Document ## Overview 15 enhancements to existing ZeroClock functionality, balanced across polish/reliability, power-user productivity, and data/insights. All features WCAG 2.2 AAA compliant from the start. ## Global WCAG 2.2 AAA Requirements (apply to every feature) - 7:1 minimum contrast ratio for all text against backgrounds - All interactive elements have `focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-accent` - All dialogs/popovers use `useFocusTrap`, `role="dialog"` or `role="alertdialog"`, `aria-modal="true"`, `aria-labelledby`/`aria-describedby` - All dynamic content changes announced via `aria-live` regions - All animations use `motion-safe:` prefix or respect the global `prefers-reduced-motion` override - All information conveyed by color must also be conveyed by text, icon, or pattern (WCAG 1.4.1) - All drag operations must have keyboard alternatives (WCAG 2.5.7) - All timed interactions must be adjustable or disableable (WCAG 2.2.1) - Design tokens used throughout: bg-bg-base, bg-bg-surface, bg-bg-elevated, text-text-primary, text-text-secondary, text-text-tertiary, border-border-subtle, accent, accent-hover --- ## Phase 1: Polish and Reliability ### 1. Smart Timer Safety Net **Problem:** Stopping the timer without a project selected silently discards tracked time. Timers left running for 8+ hours are likely accidental. **Enhancement:** - When `timerStore.stop()` is called with no `selectedProjectId`, show a "Save Entry" dialog instead of discarding - Dialog contains: project picker (AppSelect), description field, the tracked duration (read-only), Save and Discard buttons - When stopping a timer with elapsed time > 8 hours, show a confirmation: "Timer has been running for X hours. Stop and save?" - Both dialogs use focus trap, are keyboard-operable, and announce via aria-live **Files:** - Modify: `src/stores/timer.ts` (stop method) - Create: `src/components/TimerSaveDialog.vue` - Modify: `src/App.vue` (mount the dialog) **Accessibility:** - `role="alertdialog"` with focus trap - `aria-live="assertive"` announcement when dialog opens: "Timer stopped. Select a project to save X hours of tracked time." - Duration display uses `aria-label` with full spoken format ("2 hours and 34 minutes") --- ### 2. Toast Auto-Dismiss with Undo **Problem:** Toasts persist indefinitely until manually closed or pushed out by newer ones. No way to undo destructive actions. **Enhancement:** - Success toasts auto-dismiss after 4 seconds, error toasts after 8 seconds, info toasts after 6 seconds - WCAG 2.2.1 compliance: toast timers pause on hover and on keyboard focus. Add a setting in General tab to disable auto-dismiss entirely ("Persistent notifications" toggle) - Destructive operations (entry delete, expense delete, client delete, project delete) pass an `onUndo` callback to the toast store - Toast with undo shows an "Undo" button. Clicking it calls the callback within the timeout window - Undo button has `role="button"`, `aria-label="Undo delete"`, keyboard-accessible via Tab **Files:** - Modify: `src/stores/toast.ts` (add duration, onUndo, pause/resume logic) - Modify: `src/components/ToastNotification.vue` (auto-dismiss timer, undo button, hover/focus pause) - Modify: `src/views/Settings.vue` (add persistent notifications toggle in General tab) **Accessibility:** - Toast timer pauses on `:hover` and `:focus-within` (WCAG 2.2.1 Timing Adjustable) - Undo button is keyboard-focusable and announced: "Undo available. Press to reverse." - Screen reader announces remaining time: "Toast will dismiss in X seconds" - User can disable auto-dismiss entirely in Settings --- ### 3. Unified Error Handling with Retry **Problem:** Error handling is inconsistent across stores - some throw, some return null, some only console.error. Users rarely see actionable feedback. **Enhancement:** - Create a shared `handleInvokeError(error, context)` utility that: - Shows a toast with the error context ("Failed to save entry") - For transient errors (connection, database busy), adds a "Retry" button to the toast - Logs the full error to console for debugging - Standardize all store catch blocks to use this utility - The retry callback re-executes the failed operation **Files:** - Create: `src/utils/errorHandler.ts` - Modify: all 13 stores to use the shared handler in catch blocks **Accessibility:** - Retry button in toast: `role="button"`, `aria-label="Retry failed operation"`, keyboard-accessible - Error toasts use `role="alert"` (already the case) for immediate screen reader announcement - After retry succeeds, announce "Operation completed successfully" via aria-live --- ### 4. Onboarding Detection Resilience **Problem:** `detectCompletions()` in the onboarding store has all backend calls in a single try/catch - if `get_clients` fails, nothing else runs. No periodic re-check. **Enhancement:** - Wrap each detection call in its own try/catch so failures are independent - Add a 5-minute periodic re-check interval (started in App.vue alongside recurring entry checks) - Clear the interval in onUnmounted **Files:** - Modify: `src/stores/onboarding.ts` (individual try/catch per detection, no new UI) - Modify: `src/App.vue` (add periodic re-check interval) **Accessibility:** - No UI changes - this is purely backend resilience - When an item transitions from incomplete to complete, the existing checklist UI already handles aria updates --- ### 5. Invoice Items Save Reliability **Problem:** `saveInvoiceItems` creates items one at a time in a loop - if one fails, the rest are silently lost. No transaction rollback. **Enhancement:** - Add a new Rust command `save_invoice_items_batch` that wraps all inserts in a single transaction (BEGIN/COMMIT/ROLLBACK) - Replace the sequential loop in `invoicesStore.saveInvoiceItems` with a single call to the batch command - On failure, show a clear error toast with retry option **Files:** - Modify: `src-tauri/src/commands.rs` (add `save_invoice_items_batch` command) - Modify: `src-tauri/src/lib.rs` (register command) - Modify: `src/stores/invoices.ts` (replace loop with batch call) **Accessibility:** - Error feedback via toast (already accessible) - No new UI elements --- ## Phase 2: Power User Productivity ### 6. Global Quick Entry **Problem:** Logging a manual time entry requires navigating to Entries view and filling out a full form. No way to quickly capture time from wherever you are. **Enhancement:** - Register a global keyboard shortcut (configurable, default Ctrl+Shift+N) via Tauri's global shortcut plugin - Opens a compact floating dialog (Teleported to body, zoom-aware) with: - Project picker (AppSelect) - Task picker (AppSelect, filtered by project) - Description text input - Date picker (defaults to today) - Duration input (supports H:MM and decimal formats) - Tag picker (AppTagInput) - Save and Cancel buttons - Pre-fills with last-used project from settingsStore - On save, creates entry via entriesStore.createEntry and shows success toast - Dialog closes on Escape or Cancel **Files:** - Create: `src/components/QuickEntryDialog.vue` - Modify: `src/App.vue` (mount dialog, register shortcut) - Modify: `src/views/Settings.vue` (add shortcut recorder for quick entry in Timer tab) **Accessibility:** - `role="dialog"` with `aria-modal="true"`, `aria-label="Quick time entry"` - Focus trap activated on open, first focusable element receives focus - `aria-live="polite"` announces "Quick entry dialog opened" - All form controls have visible labels (not just placeholders) - Escape closes and restores focus to previously focused element - `aria-keyshortcuts` attribute on the trigger describes the shortcut --- ### 7. Entry Templates and Duplication **Problem:** Favorites only work for starting timers. No way to save and reuse manual entry configurations, and no quick duplicate for existing entries. **Enhancement:** - Add "Save as Template" option when editing an entry (saves project + task + description + tags + duration to a new `entry_templates` table) - Add "From Template" button in Entries view header that opens a picker dialog listing saved templates - Selecting a template pre-fills the create entry form - Add "Duplicate" button on each entry row in the entries table (creates a copy with today's date) - Templates manageable (delete) from a section in Settings > Timer tab **Files:** - Modify: `src-tauri/src/commands.rs` (add entry_templates CRUD commands + create table in database.rs) - Modify: `src-tauri/src/lib.rs` (register commands) - Create: `src/stores/entryTemplates.ts` - Create: `src/components/EntryTemplatePicker.vue` - Modify: `src/views/Entries.vue` (add From Template button, Duplicate button per row) - Modify: `src/views/Settings.vue` (template management section) **Accessibility:** - Template picker: `role="dialog"` with focus trap, `role="listbox"` for template list, `role="option"` per item - Duplicate button: `aria-label="Duplicate entry for [project] on [date]"` - "From Template" button: `aria-haspopup="dialog"` - Template list items keyboard-navigable (ArrowUp/Down, Enter to select) - Screen reader announces "Entry duplicated" or "Template applied" on action --- ### 8. Timesheet Smart Row Persistence **Problem:** The timesheet view requires manually adding project/task rows each week. Starting a new week means rebuilding the same row configuration. **Enhancement:** - When opening a week with no timesheet data, auto-populate rows from the previous week's row structure (projects + tasks, but with zero hours) - Add a "Copy Last Week" button in the header that copies both structure and values - Save row configuration per week in a `timesheet_rows` table so manually added rows persist across navigation - "Copy Last Week" shows a confirmation dialog before overwriting any existing data **Files:** - Modify: `src-tauri/src/commands.rs` (add `get_timesheet_rows`, `save_timesheet_rows`, `get_previous_week_structure` commands) - Modify: `src-tauri/src/lib.rs` (register commands) - Modify: `src/views/TimesheetView.vue` (auto-populate logic, copy button, persist rows) **Accessibility:** - "Copy Last Week" button: `aria-label="Copy last week's timesheet structure"` - Confirmation dialog for overwrite: `role="alertdialog"` with focus trap - `aria-live="polite"` announces "Timesheet populated with X rows from last week" - Auto-populated rows announced: "Loaded X project rows from previous week" --- ### 9. Client Cascade Awareness **Problem:** Deleting a client has no cascade check or warning, unlike projects which now show dependent counts via AppCascadeDeleteDialog. **Enhancement:** - Add `get_client_dependents` backend command that counts related projects, invoices, and expenses - Add transactional cascade delete to `delete_client` (delete expenses, invoice items, invoices, tracked_apps, entry_tags, time_entries, favorites, recurring_entries, timeline_events, tasks, projects, then client) - Wire AppCascadeDeleteDialog into Clients.vue delete flow (same pattern as Projects.vue) **Files:** - Modify: `src-tauri/src/commands.rs` (add `get_client_dependents`, update `delete_client` to cascade) - Modify: `src-tauri/src/lib.rs` (register command) - Modify: `src/views/Clients.vue` (import AppCascadeDeleteDialog, add dependency check before delete) **Accessibility:** - Reuses existing AppCascadeDeleteDialog which is already WCAG AAA compliant (focus trap, 3-second countdown, aria-live announcement, role="alertdialog") - No new accessibility work needed - the existing component handles everything --- ### 10. Expenses Receipt Management **Problem:** Expenses store a `receipt_path` but there's no way to view receipts inline, and attachment is manual file path entry only. **Enhancement:** - Add inline receipt thumbnail on expense cards (small image preview if receipt_path exists) - Add a lightbox component for full-size receipt viewing (focus trap, Escape to close, zoom controls) - Add a file picker button for attaching receipts (uses Tauri's dialog plugin for native file picker - the keyboard-accessible alternative to drag-and-drop) - Add drag-and-drop zone on the expense edit form as an additional attachment method - Show a "No receipt" indicator (icon + text) on expenses without one - Keyboard alternative for drag-and-drop: the file picker button serves as the primary accessible method (WCAG 2.5.7 Dragging Movements) **Files:** - Create: `src/components/ReceiptLightbox.vue` - Modify: `src/views/Entries.vue` (expense tab: add thumbnail, lightbox trigger, file picker, drop zone) **Accessibility:** - Lightbox: `role="dialog"`, `aria-modal="true"`, `aria-label="Receipt image for [expense description]"`, focus trap, Escape closes - Image: `alt="Receipt for [category] expense on [date], [amount]"` - File picker button: `aria-label="Attach receipt file"` - this is the primary method, drag-and-drop is the enhancement (WCAG 2.5.7) - Drop zone: `role="button"`, `aria-label="Drop receipt file here or press Enter to browse"`, keyboard-activatable (Enter/Space opens file picker) - "No receipt" indicator uses both icon and text (not icon alone) - Lightbox zoom controls: keyboard-accessible (+/- buttons with aria-labels) --- ## Phase 3: Data and Insights ### 11. Dashboard Weekly Comparison **Problem:** Dashboard shows current stats but gives no sense of trend - no way to know if this week is better or worse than last week. **Enhancement:** - Add comparison indicators on each stat card: "+2.5h vs last week" or "-1.2h vs last week" with directional arrow icon - Add a sparkline row below the weekly chart showing the last 4 weeks of daily totals (7 bars each, muted styling) - Comparison data fetched via existing `get_reports` command with last week's date range **Files:** - Modify: `src/views/Dashboard.vue` (add comparison computeds, sparkline rendering, fetch last week data) **Accessibility:** - Comparison text is actual text, not just a colored arrow - e.g., "2.5 hours more than last week" or "1.2 hours less than last week" - Color conveys reinforcement only (green for increase, red for decrease) - the text and arrow direction carry the meaning (WCAG 1.4.1) - Arrow icon has `aria-hidden="true"` since the text already conveys direction - Sparkline bars have a `role="img"` wrapper with `aria-label="Last 4 weeks: [week1] hours, [week2] hours, [week3] hours, [week4] hours"` - Each individual sparkline bar does NOT need to be interactive - the summary label covers it - Comparison values use `sr-only` prefix: "Compared to last week:" for screen readers --- ### 12. Project Health Dashboard Cards **Problem:** Budget progress bars on project cards are basic - no at-a-glance health assessment combining pace, burn rate, and risk level. **Enhancement:** - Add a health badge per project card combining budget burn rate and pace data from `get_project_budget_status` - Three health states: "On Track" (green icon + text), "At Risk" (yellow icon + text), "Over Budget" (red icon + text) - Health determined by: budget percentage > 100% = over budget, pace = "behind" and > 75% budget used = at risk, else on track - Add an "Attention Needed" section at the top of Projects view listing at-risk and over-budget projects - Each state uses BOTH a distinct icon (checkmark, warning triangle, x-circle) AND text label - never color alone **Files:** - Modify: `src/views/Projects.vue` (add health badge computation, attention section) **Accessibility:** - Health badge: icon + text label, never color alone (WCAG 1.4.1 Use of Color) - Icons: CheckCircle for on-track, AlertTriangle for at-risk, XCircle for over-budget - each with `aria-hidden="true"` since text label is present - Badge has `role="status"` so screen readers announce it - Attention section: `role="region"` with `aria-label="Projects needing attention"`, `aria-live="polite"` so it updates when data changes - Each attention item is a focusable link/button that navigates to the project - Text labels: "On Track", "At Risk", "Over Budget" - clear, unambiguous, no jargon --- ### 13. Reports Time-of-Day Heatmap **Problem:** Reports view has no visibility into when work happens - no pattern analysis of productive hours or workday distribution. **Enhancement:** - Add a "Patterns" tab to Reports view - Primary visualization: a 7x24 grid (days of week x hours of day) showing work intensity - Each cell uses both color intensity AND a numeric label showing hours (e.g., "2.5h") - not color alone (WCAG 1.4.1) - Cells with zero hours are empty (no false visual noise) - Below the grid, add a "Data Table" toggle that switches to a fully accessible HTML table view of the same data (accessible alternative to the visual grid) - Data computed from entry start_time timestamps within the selected date range **Files:** - Modify: `src/views/Reports.vue` (add Patterns tab, heatmap grid, data table alternative) **Accessibility:** - Grid uses `role="grid"` with `role="row"` and `role="gridcell"` for keyboard navigation - Each cell has `aria-label="[Day] [Hour]: [X] hours"` (e.g., "Monday 9 AM: 2.5 hours") - Arrow key navigation between cells (up/down/left/right) - Color intensity reinforces the numeric values but is NOT the sole information channel - every cell shows its numeric value as text - "Data Table" toggle button: `aria-pressed` toggle, switches between visual grid and `
| ` and ` | ` - Table alternative is a standard HTML table with proper headers, fully navigable by screen readers - Summary stats below the grid as text: "Most productive: Monday 9-10 AM (avg 1.8h)", "Quietest: Sunday (avg 0.2h)" - `motion-safe:` on any cell hover/focus transitions --- ### 14. Rounding Visibility and Preview **Problem:** Time rounding is configured in Settings but its effect is invisible - users can't see how much time is being rounded. **Enhancement:** - On entry rows in Entries view: show a small "rounded" indicator next to duration when rounding changes the value, with tooltip showing "Actual: 1h 23m, Rounded: 1h 30m" - On invoice line items: show both actual and rounded hours when rounding is active - In Reports view Hours tab: add a "Rounding Impact" summary line showing total time added/subtracted by rounding across all entries in the date range - Rounding calculations use the existing `roundDuration()` utility from `src/utils/rounding.ts` **Files:** - Modify: `src/views/Entries.vue` (add rounded indicator on entry rows) - Modify: `src/views/Invoices.vue` (show actual vs rounded on line items) - Modify: `src/views/Reports.vue` (add rounding impact summary) **Accessibility:** - Rounded indicator: not just a colored dot - shows text "Rounded" with a clock icon, `aria-label="Duration rounded from [actual] to [rounded]"` - Tooltip: accessible via keyboard focus (not hover-only), uses `role="tooltip"` with `aria-describedby` - Rounding impact summary: plain text, 7:1 contrast, e.g., "Rounding added 2h 15m across 47 entries" - Actual vs. rounded on invoices: both values visible as text, difference highlighted with both color and "+Xm" / "-Xm" text label - All diff indicators use 7:1 contrast ratios against their backgrounds --- ### 15. Export Completeness and Scheduling **Problem:** `export_data` only exports clients, projects, entries, and invoices - missing tags, expenses, favorites, recurring entries, tracked apps, timeline events, calendar sources, and settings. No automated backup. **Enhancement:** - Expand `export_data` to include ALL data: tags, entry_tags, expenses, favorites, recurring_entries, tracked_apps, timeline_events, calendar_sources, calendar_events, timesheet_locks, and settings - Update `import_json_data` to handle the expanded format (backward compatible with old exports) - Add a "Last exported" timestamp display in Settings > Data tab - Add an "Auto-backup on close" toggle in Settings > Data tab that saves a backup JSON to a user-configured directory when the app closes - Add a "Backup path" file picker (Tauri dialog) for choosing the auto-backup directory - Backup files named with timestamp: `zeroclock-backup-YYYY-MM-DD.json` - WCAG 2.2.1: the auto-backup happens on app close (user-initiated), not on a timer, so no timing concerns **Files:** - Modify: `src-tauri/src/commands.rs` (expand `export_data`, update `import_json_data`, add `auto_backup` command) - Modify: `src-tauri/src/lib.rs` (register commands) - Modify: `src/views/Settings.vue` (add last-exported display, auto-backup toggle, path picker) - Modify: `src/App.vue` (hook into Tauri window close event for auto-backup) **Accessibility:** - File picker button: `aria-label="Choose backup directory"` - Last exported display: `role="status"`, readable text "Last exported: February 20, 2026 at 3:45 PM" (not relative time like "2 hours ago") - Auto-backup toggle: standard toggle with label, `aria-checked` - Backup path shown as text with a "Change" button, not an editable text field - All controls have visible labels and 7:1 contrast --- ## Implementation Phases ### Phase 1: Polish and Reliability (Features 1-5) Mostly backend and store changes, minimal new UI. Low risk, high impact on data integrity. ### Phase 2: Power User Productivity (Features 6-10) Mix of new components and view modifications. Medium complexity, high impact on daily workflow. ### Phase 3: Data and Insights (Features 11-15) Primarily view-level changes with some backend expansion. Medium complexity, high impact on decision-making. ## Verification Criteria Each feature must pass: 1. `npx vue-tsc --noEmit` - TypeScript type check 2. `npx vite build` - Production build 3. `cargo build` - Rust backend build (if backend changed) 4. Manual WCAG AAA spot check: - All interactive elements keyboard-operable - All dialogs trap focus and close with Escape - All dynamic content announced via aria-live - All form controls have visible labels - No information conveyed by color alone - 7:1 contrast on all text - Drag operations have keyboard alternatives - Timed interactions are adjustable |
|---|