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.
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"orrole="alertdialog",aria-modal="true",aria-labelledby/aria-describedby - All dynamic content changes announced via
aria-liveregions - All animations use
motion-safe:prefix or respect the globalprefers-reduced-motionoverride - 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 noselectedProjectId, 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 traparia-live="assertive"announcement when dialog opens: "Timer stopped. Select a project to save X hours of tracked time."- Duration display uses
aria-labelwith 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
onUndocallback 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
:hoverand: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_batchthat wraps all inserts in a single transaction (BEGIN/COMMIT/ROLLBACK) - Replace the sequential loop in
invoicesStore.saveInvoiceItemswith a single call to the batch command - On failure, show a clear error toast with retry option
Files:
- Modify:
src-tauri/src/commands.rs(addsave_invoice_items_batchcommand) - 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"witharia-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-keyshortcutsattribute 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_templatestable) - 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_rowstable 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(addget_timesheet_rows,save_timesheet_rows,get_previous_week_structurecommands) - 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_dependentsbackend 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(addget_client_dependents, updatedelete_clientto 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_reportscommand 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 witharia-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-onlyprefix: "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"witharia-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"withrole="row"androle="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-pressedtoggle, 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 fromsrc/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"witharia-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_datato 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_datato 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(expandexport_data, updateimport_json_data, addauto_backupcommand) - 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:
npx vue-tsc --noEmit- TypeScript type checknpx vite build- Production buildcargo build- Rust backend build (if backend changed)- 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