3.6 KiB
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)
- Timer.vue — Project selector (
selectedProject, nullable, disabled when running) - Timer.vue — Task selector (
selectedTask, nullable, disabled when running or no project) - Projects.vue — Client selector in dialog (
formData.client_id, optional "No client") - Entries.vue — Project filter (
filterProject, nullable "All Projects") - Entries.vue — Project selector in edit dialog (
editForm.project_id, required) - Invoices.vue — Client selector in create form (
createForm.client_id, required)
Native <input type="date"> (6 instances)
- Entries.vue — Start Date filter
- Entries.vue — End Date filter
- Reports.vue — Start Date
- Reports.vue — End Date
- Invoices.vue — Invoice Date (required)
- 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 ChevronDownicon (Lucide) on right, rotates 180deg when open- Disabled state:
opacity-40,cursor-not-allowed
Dropdown Panel
- Teleported to
<body>, positioned viagetBoundingClientRect() bg-bg-surface,border border-border-visible,rounded-xl, shadow- Max-height with overflow-y scroll
- Options: hover
bg-bg-elevated, selected item showsCheckicon 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
Calendaricon (Lucide) on right- Same input styling as AppSelect trigger
Calendar Popover
- Teleported to
<body>, positioned below trigger - Header:
ChevronLeft/ChevronRightarrows, 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-DDstring (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).