Files
zeroclock/docs/plans/2026-02-20-enhancement-round2-design.md
Your Name dea742707f docs: enhancement round 2 design - 15 feature proposals
Covers smart timer safety net, toast undo system, unified error
handling, onboarding resilience, invoice save reliability, global
quick entry, entry templates, timesheet persistence, client cascade,
receipt management, weekly comparison, project health cards, time
heatmap, rounding preview, and export scheduling. All features
designed for WCAG 2.2 AAA compliance.
2026-02-20 14:22:01 +02:00

22 KiB

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 <table> with <th scope="col"> and <th scope="row">
  • 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