Files
zeroclock/docs/plans/2026-02-17-ui-polish-design.md
Your Name 90d557487f docs: add UI polish & UX upgrade design doc
Covers amber accent system, charcoal background lift, button hierarchy,
toast notifications, rich empty states, UI zoom, portable storage,
and window state persistence.
2026-02-17 21:15:41 +02:00

12 KiB

ZeroClock UI Polish & UX Upgrade — Design

Problem

The first redesign pass established a Swiss/Dieter Rams foundation but went too far into monochrome territory. The app is a wall of near-black grey with zero personality. The signature amber accent color is completely absent. All buttons look identical. Empty states are sad grey text. alert() calls for all feedback. No visual hierarchy for primary actions. The app also stores data in AppData (not portable) and doesn't remember window position/size.

Goals

  1. Reintroduce warm amber (#D97706) as the strategic accent color
  2. Lift the entire background palette from near-black to charcoal
  3. Establish clear button hierarchy (primary/secondary/ghost/danger)
  4. Replace all alert() calls with a toast notification system
  5. Design rich empty states with icons, copy, and CTA buttons
  6. Add amber focus states on all inputs
  7. Add UI zoom control in Settings (persistent)
  8. Make the app fully portable (data next to exe)
  9. Persist window position and size between runs

Design System

Color Palette (Revised)

Background layers (lifted from near-black to charcoal):
  --bg-base:      #1A1A18    app background
  --bg-surface:   #222220    cards, panels, navRail, titlebar
  --bg-elevated:  #2C2C28    hover, active, raised elements
  --bg-inset:     #141413    inputs, recessed areas

Text (warm whites — unchanged):
  --text-primary:   #F5F5F0  headings, active items
  --text-secondary: #8A8A82  body, descriptions
  --text-tertiary:  #5A5A54  disabled, placeholders (bumped from #4A4A45)

Borders (bumped to match brighter backgrounds):
  --border-subtle:  #2E2E2A  dividers, card borders
  --border-visible: #3D3D38  input borders, focused

Accent (NEW — amber):
  --accent:       #D97706    button fills, active indicators
  --accent-hover: #B45309    hover/pressed amber
  --accent-muted: rgba(217,119,6,0.12)  subtle glows, backgrounds
  --accent-text:  #FBBF24    amber text on dark backgrounds

Status (semantic — unchanged):
  --status-running: #34D399  timer active, success, toggle on
  --status-warning: #EAB308  pending, caution
  --status-error:   #EF4444  destructive, overdue
  --status-info:    #3B82F6  informational

Remove all legacy alias colors (--color-amber mapped to white, --color-background, --color-surface, etc.).

Button Hierarchy

Primary — amber fill, for the ONE main action per view:

  • bg-accent text-[#1A1A18] font-medium rounded
  • Hover: bg-accent-hover
  • Used: Start timer, Create Project, Save Settings, Generate Report, Apply Filters, Create Invoice

Secondary — outlined, for supporting actions:

  • border border-border-visible text-text-primary rounded
  • Hover: bg-bg-elevated
  • Used: Export CSV, Export Data, Cancel buttons, Clear filters

Ghost — text only, for low-priority actions:

  • text-text-secondary hover:text-text-primary
  • No border, no background
  • Used: "View all" links, tab navigation inactive

Danger — destructive actions:

  • border border-status-error text-status-error rounded
  • Hover: bg-status-error/10
  • Used: Clear Data, Delete buttons in confirmation dialogs

Input Focus States

All inputs, selects, and textareas:

  • Current: focus:border-border-visible (grey, barely visible)
  • New: focus:border-accent focus:outline-none with box-shadow: 0 0 0 2px rgba(217,119,6,0.12) (amber border + subtle amber glow)

Shell

TitleBar

  • "ZEROCLOCK" wordmark: text-accent-text (amber #FBBF24) instead of text-text-secondary
  • Running timer section: unchanged (green dot + project + time + stop)
  • Window controls: unchanged

NavRail

  • Active indicator: 2px left border in bg-accent (#D97706) instead of bg-text-primary (white)
  • Active icon color: stays text-text-primary (white)
  • Tooltip: add subtle caret/triangle pointing left for polish
  • Timer dot at bottom: stays green (semantic)

View Designs

Dashboard

Header:

  • Greeting: "Good morning/afternoon/evening" in text-lg text-text-secondary
  • Date: "Monday, February 17, 2026" in text-xs text-text-tertiary

Stats row (4 stats):

  • This Week | This Month | Today | Active Projects
  • Labels: text-xs text-text-tertiary uppercase tracking-[0.08em]
  • Values: text-[1.25rem] font-mono text-accent-text (amber)

Weekly chart:

  • Bar fill: #D97706 (amber), today's bar: #FBBF24 (lighter amber)
  • Grid: #2E2E2A, ticks: #5A5A54

Recent entries:

  • Flat list as current
  • "View all" ghost link bottom-right, navigates to Entries

Empty state:

  • Centered vertically in available space
  • Lucide Clock icon, 48px, text-text-tertiary
  • "Start tracking your time" — text-text-secondary
  • "Your dashboard will come alive with stats, charts, and recent activity once you start logging hours." — text-xs text-text-tertiary
  • Primary amber button: "Go to Timer"

Timer

Hero display:

  • text-[3rem] font-mono text-text-primary centered
  • When running: colon separators pulse amber (text-accent-text with opacity animation)
  • When stopped: all white, static

Start/Stop button:

  • Start: bg-accent text-[#1A1A18] px-10 py-3 text-sm font-medium rounded
  • Stop: bg-status-error text-white px-10 py-3 text-sm font-medium rounded (filled red)
  • 150ms color transition between states

Inputs:

  • max-w-[36rem] mx-auto (centered)
  • Amber focus states
  • Project select: small colored dot preview of selected project color

Recent entries:

  • Scoped to today's entries
  • Most recent entry: subtle amber left border
  • Max 5, "View all" ghost link

Empty state:

  • Lucide Timer icon, 40px, text-text-tertiary
  • "No entries today" — text-text-secondary
  • "Select a project and hit Start to begin tracking" — text-xs text-text-tertiary

Projects

Header:

  • Title "Projects" left
  • "+ Add" becomes small amber primary button: bg-accent text-[#1A1A18] px-3 py-1.5 text-xs font-medium rounded

Cards:

  • 2px left border in project color
  • bg-bg-surface hover:bg-bg-elevated transition
  • Hover: left border widens from 2px to 3px
  • Rate and client inline: "ClientName · $50.00/hr"

Create/Edit dialog:

  • Submit: amber primary
  • Cancel: secondary
  • Color picker: row of 8 preset swatches above hex input
    • Presets: #D97706, #3B82F6, #8B5CF6, #EC4899, #10B981, #EF4444, #06B6D4, #6B7280

Empty state:

  • Lucide FolderKanban icon, 48px, text-text-tertiary
  • "No projects yet" — text-text-secondary
  • "Projects organize your time entries and set billing rates for clients." — text-xs text-text-tertiary
  • Primary amber button: "Create Project" (opens dialog)

Entries

Filter bar:

  • Wrapped in bg-bg-surface rounded p-4 container
  • "Apply": amber primary
  • "Clear": ghost button
  • mb-6 gap between filter bar and table

Table:

  • Header row: bg-bg-surface background
  • Duration column: text-accent-text font-mono (amber)
  • Edit/delete hover reveal: unchanged

Empty state (below filter bar):

  • Lucide List icon, 48px, text-text-tertiary
  • "No entries found" — text-text-secondary
  • "Time entries will appear here as you track your work. Try adjusting the date range if you have existing entries." — text-xs text-text-tertiary
  • Primary amber button: "Go to Timer"

Reports

Filter bar:

  • Same bg-bg-surface rounded p-4 container
  • "Generate": amber primary
  • "Export CSV": secondary

Stats:

  • mt-6 spacing after filter bar
  • Values: text-accent-text font-mono (amber)

Chart:

  • Bars use each project's own assigned color
  • Fallback for no-color projects: #D97706
  • Grid: #2E2E2A, ticks: #5A5A54

Breakdown:

  • Hours value: text-accent-text font-mono
  • Earnings: text-text-secondary font-mono

Empty state:

  • Lucide BarChart3 icon, 48px, text-text-tertiary
  • "Generate a report to see your data" — text-text-secondary
  • No CTA button (Generate button is right there)

Invoices

Tabs:

  • Active: border-b-2 border-accent text-text-primary (amber underline)
  • Inactive: text-text-tertiary hover:text-text-secondary

List table:

  • Header row: bg-bg-surface
  • Amount: text-accent-text font-mono
  • Status colors: unchanged (semantic)

Create form:

  • Submit: amber primary
  • Cancel: secondary
  • Total line: text-accent-text font-mono

Invoice detail dialog:

  • Export PDF: amber primary
  • Total: text-accent-text

Empty state:

  • Lucide FileText icon, 48px, text-text-tertiary
  • "No invoices yet" — text-text-secondary
  • "Create invoices from your tracked time to bill clients." — text-xs text-text-tertiary
  • Primary amber button: "Create Invoice" (switches to Create tab)

Settings

Buttons:

  • "Save Settings": amber primary
  • "Export": secondary
  • "Clear Data": danger (red)

Toggle: stays green when active (semantic "on" state)

All alert() calls: replaced with toasts

UI Zoom control (NEW):

  • New section "Appearance" between Timer and Data sections
  • Label: "UI Scale"
  • Minus button [-] | value display "100%" | Plus button [+]
  • Steps: 80%, 90%, 100%, 110%, 120%, 130%, 150%
  • Implementation: CSS zoom property on the #app root element
  • Persisted via update_settings('ui_zoom', '100') in the settings SQLite table
  • Applied on app startup before first paint (read from settings store in App.vue onMounted)

Toast Notification System

Component: ToastNotification.vue

  • Fixed position, top-center of the main content area, top-4
  • Max width 320px
  • bg-bg-surface border border-border-subtle rounded shadow-lg
  • 3px left border colored by type:
    • Success: border-status-running (green)
    • Error: border-status-error (red)
    • Info: border-accent (amber)
  • Content: Lucide icon (Check/X/Info, 16px) + message in text-sm text-text-primary
  • Enter animation: slide down from -20px + fade, 200ms
  • Exit: fade out 150ms
  • Auto-dismiss: 3 seconds
  • Click to dismiss early
  • Stack with 8px gap, max 3 visible

Store: useToastStore

  • addToast(message: string, type: 'success' | 'error' | 'info')
  • Auto-generates unique ID
  • Auto-removes after 3s timeout
  • Max 3 toasts visible, oldest removed first

Replacements

All alert() calls across the app become toast calls:

  • Settings save success/failure
  • Clear data success/failure
  • Export data success/failure
  • Timer "Please select a project"
  • Reports "Please select a date range"
  • Reports "No data to export"
  • Invoices "Please select a client"

Portable App

Problem

Currently uses directories::ProjectDirs to store the SQLite database in the OS app data directory (e.g., C:\Users\<user>\AppData\Roaming\ZeroClock\). This is not portable.

Solution

Change get_data_dir() in lib.rs to always resolve relative to the executable:

fn get_data_dir() -> PathBuf {
    let exe_path = std::env::current_exe().unwrap();
    let data_dir = exe_path.parent().unwrap().join("data");
    std::fs::create_dir_all(&data_dir).ok();
    data_dir
}

This stores data/timetracker.db next to the .exe. The directories crate dependency can be removed from Cargo.toml.


Window State Persistence

Plugin: tauri-plugin-window-state

Add the Tauri window-state plugin to save/restore:

  • Window position (x, y)
  • Window size (width, height)
  • Maximized state

Implementation

  1. Add dependency: tauri-plugin-window-state = "2" to Cargo.toml
  2. Add "window-state" to plugins in tauri.conf.json
  3. Register plugin: .plugin(tauri_plugin_window_state::Builder::new().build()) in lib.rs
  4. The plugin auto-saves state to a .window-state file. Since we're making the app portable, configure the plugin to store state in our data/ directory next to the exe.

Config

In tauri.conf.json, add to plugins:

"window-state": {
  "all": true
}

Remove "center": true from the window config so the saved position is respected on subsequent launches.


Transitions & Motion

Unchanged from first redesign, plus:

  • Toast enter: translateY(-20px) + opacity 0 to 0+1, 200ms ease-out
  • Toast exit: opacity 1 to 0, 150ms
  • Button hover: 150ms background-color transition
  • Project card left-border width: 150ms transition on hover
  • Timer colon amber pulse: opacity 0.4 to 1.0 on accent-text color, 1s ease-in-out infinite (only when running)