Files
zeroclock/docs/plans/2026-02-17-custom-dropdowns-datepickers-design.md
2026-02-17 22:15:10 +02:00

3.6 KiB

Custom Dropdowns & Date Pickers Design

Goal: Replace all native <select> and <input type="date"> elements with custom Vue 3 components that match ZeroClock's dark UI.

Architecture: Two reusable components (AppSelect, AppDatePicker) using <Teleport to="body"> for overflow-safe positioning. Drop-in replacements — no store or logic changes needed.


Inventory

Native <select> (6 instances)

  1. Timer.vue — Project selector (selectedProject, nullable, disabled when running)
  2. Timer.vue — Task selector (selectedTask, nullable, disabled when running or no project)
  3. Projects.vue — Client selector in dialog (formData.client_id, optional "No client")
  4. Entries.vue — Project filter (filterProject, nullable "All Projects")
  5. Entries.vue — Project selector in edit dialog (editForm.project_id, required)
  6. Invoices.vue — Client selector in create form (createForm.client_id, required)

Native <input type="date"> (6 instances)

  1. Entries.vue — Start Date filter
  2. Entries.vue — End Date filter
  3. Reports.vue — Start Date
  4. Reports.vue — End Date
  5. Invoices.vue — Invoice Date (required)
  6. Invoices.vue — Due Date (optional)

Component 1: AppSelect

Trigger

  • Styled like existing inputs: bg-bg-inset, border-border-subtle, rounded-xl
  • Shows selected label or placeholder in text-text-tertiary
  • ChevronDown icon (Lucide) on right, rotates 180deg when open
  • Disabled state: opacity-40, cursor-not-allowed

Dropdown Panel

  • Teleported to <body>, positioned via getBoundingClientRect()
  • bg-bg-surface, border border-border-visible, rounded-xl, shadow
  • Max-height with overflow-y scroll
  • Options: hover bg-bg-elevated, selected item shows Check icon in accent color
  • Animate in: scale + fade (150ms)

API

<AppSelect
  v-model="selectedProject"
  :options="activeProjects"
  label-key="name"
  value-key="id"
  placeholder="Select project"
  :disabled="timerStore.isRunning"
/>

Behavior

  • Click trigger toggles open/close
  • Click option selects and closes
  • Click outside closes
  • Keyboard: Arrow keys navigate, Enter selects, Escape closes
  • Tab moves focus away and closes

Component 2: AppDatePicker

Trigger

  • Text input showing formatted date ("Feb 17, 2026") or placeholder
  • Calendar icon (Lucide) on right
  • Same input styling as AppSelect trigger

Calendar Popover

  • Teleported to <body>, positioned below trigger
  • Header: ChevronLeft/ChevronRight arrows, center "Month Year" label
  • 7-column grid: Mon-Sun header row, day cells
  • Prev/next month padding days in text-text-tertiary
  • Today: ring/border in amber accent
  • Selected: solid amber accent background, white text
  • Hover: bg-bg-elevated
  • Click day: selects, closes popover, updates model

API

<AppDatePicker
  v-model="startDate"
  placeholder="Start date"
  :required="true"
/>

Behavior

  • v-model is YYYY-MM-DD string (same format as native date input)
  • Click trigger opens calendar
  • Click outside closes
  • Keyboard: Arrow keys navigate days, Enter selects, Escape closes
  • Month navigation via chevron buttons

Integration

Pure visual swap — replace each native element with the custom component, keep the same v-model binding. No store or routing changes.

Files created: src/components/AppSelect.vue, src/components/AppDatePicker.vue Files modified: Timer.vue, Projects.vue, Entries.vue, Invoices.vue, Reports.vue

The datetime-local input in Entries.vue edit dialog stays as-is (not in scope).