diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..d15c110
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,67 @@
+# Changelog
+
+All notable changes to TimeTracker Pro will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
+### Added
+- Native HTML5 time picker component (`TimePicker`) following web standards and a11y best practices
+ - Uses `` for familiar, intuitive UX
+ - **15-minute intervals**: Time selection restricted to :00, :15, :30, and :45 using HTML5 `step` attribute
+ - Automatic native time pickers on mobile devices (iOS/Android)
+ - Keyboard-accessible time inputs on desktop
+ - Full ARIA label support for screen readers
+ - Styled with shadcn/ui design tokens for consistency
+
+### Changed
+- **Improved time selection UX**: Replaced custom scroll-wheel time picker with native HTML5 time inputs
+ - Follows standard web conventions for familiar, intuitive user experience
+ - Better desktop experience with keyboard-accessible inputs
+ - Mobile browsers provide native time pickers automatically
+ - Full accessibility (a11y) support with proper ARIA labels and keyboard navigation
+ - Consistent with existing date input pattern in the application
+ - Eliminates custom scroll logic in favor of browser-native functionality
+ - **Start Day Dialog**: 1 time picker for day start time
+ - **Task Edit Dialog**: 2 time pickers for task start/end times
+ - **Archive Edit Dialog**: 4 time pickers (2 for day start/end, 2 for task start/end)
+- Removed duplicate `generateTimeOptions()` helper functions from all dialog components
+
+### Fixed
+- Resolved merge conflicts in `src/index.css`
+
+## [0.21.1] - 2026-02-06
+
+### Initial Release Features
+- Daily time tracking with start/stop functionality
+- Task management with real-time duration tracking
+- Rich text support with GitHub Flavored Markdown
+- Projects & clients organization with hourly rates
+- Custom categories with color coding
+- Archive system for completed work days
+- Revenue tracking and automatic calculations
+- Invoice generation and export (CSV, JSON)
+- CSV import for existing time data
+- Progressive Web App with offline support
+- Cross-platform compatibility (Windows, Mac, Linux, iOS, Android)
+- Dual storage mode (guest/local or authenticated/cloud sync)
+- Print-friendly archive views
+- Mobile-optimized interface with touch navigation
+- Dark mode support
+- Authentication via Supabase (optional)
+- Real-time sync across devices (when authenticated)
+
+---
+
+## Version History
+
+### Versioning Guidelines
+- **Major** (X.0.0): Breaking changes, major feature overhauls
+- **Minor** (0.X.0): New features, significant improvements, non-breaking changes
+- **Patch** (0.0.X): Bug fixes, minor improvements, documentation updates
+
+### Links
+- [Unreleased Changes](https://github.com/AdamJ/TimeTrackerPro/compare/v0.21.1...HEAD)
+- [Version 0.21.1](https://github.com/AdamJ/TimeTrackerPro/releases/tag/v0.21.1)
diff --git a/CLAUDE.md b/CLAUDE.md
index 58096b2..10e9dde 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,7 +1,7 @@
# CLAUDE.md - AI Assistant Codebase Guide
-**Last Updated:** 2026-02-02
-**Version:** 1.0.2
+**Last Updated:** 2026-02-06
+**Version:** 1.0.4
This document provides comprehensive guidance for AI assistants working with the TimeTracker Pro codebase. It covers architecture, conventions, workflows, and best practices.
@@ -196,6 +196,7 @@ TimeTrackerPro/
├── src/
│ ├── components/ # React components
│ │ ├── ui/ # shadcn/ui base components (49 files)
+│ │ │ └── scroll-time-picker.tsx # Custom scroll-wheel time picker
│ │ ├── ArchiveEditDialog.tsx # Archive entry editing
│ │ ├── ArchiveItem.tsx # Archive display component
│ │ ├── AuthDialog.tsx # Authentication modal
@@ -627,6 +628,81 @@ const MyPage = lazy(() => import("./pages/MyPage"));
} />
```
+### Using the TimePicker Component
+
+The `TimePicker` is a native HTML5 time input wrapped with shadcn/ui styling for consistent appearance and accessibility.
+
+**Component File**: `src/components/ui/scroll-time-picker.tsx`
+
+**Props Interface**:
+```typescript
+interface TimePickerProps {
+ value: string; // "HH:MM" 24-hour format (e.g., "14:30")
+ onValueChange: (value: string) => void;
+ disabled?: boolean;
+ className?: string;
+ id?: string;
+ "aria-label"?: string;
+ "aria-describedby"?: string;
+}
+```
+
+**Usage Example**:
+```typescript
+import { TimePicker } from "@/components/ui/scroll-time-picker";
+import { Label } from "@/components/ui/label";
+import { useState } from "react";
+
+const MyComponent = () => {
+ const [time, setTime] = useState("09:00");
+
+ return (
+
+
+
+
+ );
+};
+```
+
+**Features**:
+- Native HTML5 `` for standard web UX
+- **15-minute intervals**: Time selection restricted to :00, :15, :30, :45 using `step={900}`
+- Mobile browsers display native time pickers automatically
+- Desktop browsers provide keyboard-accessible spinners or typed input
+- Full accessibility with ARIA labels, keyboard navigation, and screen reader support
+- Styled with shadcn/ui design tokens (matches Input component)
+- Supports dark mode via CSS variables
+- Compatible with all modern browsers
+
+**Accessibility (A11y)**:
+- Proper label association via `htmlFor` and `id`
+- ARIA labels for screen readers
+- Keyboard navigable (Tab, Arrow keys, Enter)
+- Focus visible indicators
+- Works with browser's native date/time accessibility features
+
+**Used In**:
+- `StartDayDialog.tsx` - Day start time selection (1 picker)
+- `TaskEditDialog.tsx` - Task start/end time selection (2 pickers)
+- `ArchiveEditDialog.tsx` - Day and task time editing (4 pickers total)
+
+**Design Decision**: Native HTML5 inputs were chosen over custom implementations because:
+1. Standard web pattern users already understand
+2. Automatic mobile optimization (native pickers on iOS/Android)
+3. Built-in accessibility features
+4. Consistent with the app's date input approach
+5. No custom scroll/wheel logic to maintain
+6. Better keyboard navigation
+7. Follows shadcn/ui philosophy of enhancing web standards
+
+**Migration Note**: This component replaced a custom scroll-wheel picker and earlier dropdown approach, providing better UX through browser-native functionality.
+
### Adding a New Context Method
```typescript
@@ -951,6 +1027,7 @@ Before making changes, verify:
### Documentation
- **Main README**: `README.md` - User-facing documentation
+- **Changelog**: `CHANGELOG.md` - Version history and changes
- **CLAUDE.md**: `CLAUDE.md` - This file - AI assistant guide
- **Agent Guidelines**: `AGENTS.md` - Quick agent instructions
- **Archive System**: `docs/ARCHIVING_DAYS.md` - Archive system guide
@@ -986,6 +1063,8 @@ Before making changes, verify:
| Version | Date | Changes |
|---------|------|---------|
+| 1.0.4 | 2026-02-06 | Replaced custom scroll picker with native HTML5 time inputs for better UX and a11y |
+| 1.0.3 | 2026-02-06 | Added ScrollTimePicker component, replaced time dropdowns with scroll-wheel UI |
| 1.0.2 | 2026-02-02 | Added auto-open New Task form feature when day starts |
| 1.0.1 | 2025-11-21 | Updated component list, documentation references, and current state |
| 1.0.0 | 2025-11-18 | Initial CLAUDE.md creation |
diff --git a/README.md b/README.md
index 3183d7a..c4f6529 100644
--- a/README.md
+++ b/README.md
@@ -29,6 +29,7 @@ A modern, feature-rich Progressive Web App (PWA) for time tracking built with Re
**Additional:**
+- [Changelog](#-changelog)
- [License](#-license)
- [Credits](#-credits)
@@ -60,6 +61,7 @@ TimeTracker Pro is a professional time tracking application that helps you monit
- **Daily Time Tracking** - Start/stop your workday with clear boundaries
- **Task Management** - Create, edit, and delete tasks with real-time duration tracking
+- **Intuitive Time Selection** - Native browser time inputs for familiar, accessible time entry
- **Rich Text Support** - Add detailed notes with GitHub Flavored Markdown (tables, lists, formatting)
- **Automatic Calculations** - Duration and revenue calculated automatically
- **Archive System** - Permanent record of all completed work days
@@ -1059,6 +1061,18 @@ const MyPage = lazy(() => import("./pages/MyPage"));
---
+## 📋 Changelog
+
+For a detailed list of changes, new features, and bug fixes, see [CHANGELOG.md](CHANGELOG.md).
+
+**Recent Updates:**
+- Native HTML5 time inputs for intuitive, accessible time selection
+- Consistent UX with date inputs across all dialogs
+- Mobile-optimized with browser-native time pickers
+- Full keyboard navigation and screen reader support
+
+---
+
## 📱 iOS Screenshots
| View | Image |
diff --git a/dev-dist/sw.js b/dev-dist/sw.js
index 7be56bf..e86b285 100644
--- a/dev-dist/sw.js
+++ b/dev-dist/sw.js
@@ -79,7 +79,7 @@ define(['./workbox-21a80088'], (function (workbox) { 'use strict';
*/
workbox.precacheAndRoute([{
"url": "index.html",
- "revision": "0.0lr8hurrrmo"
+ "revision": "0.vtj3k2q5obg"
}], {});
workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
diff --git a/docs/plans/2026-02-06-scroll-time-picker-design.md b/docs/plans/2026-02-06-scroll-time-picker-design.md
new file mode 100644
index 0000000..8b49157
--- /dev/null
+++ b/docs/plans/2026-02-06-scroll-time-picker-design.md
@@ -0,0 +1,45 @@
+# Scroll Time Picker Design
+
+**Date:** 2026-02-06
+**Status:** Approved
+
+## Problem
+
+Time selection uses Select dropdowns with 96 options (15-minute intervals across 24 hours). Users must scroll through a long list to find their desired time. Poor UX especially for times later in the day.
+
+## Solution
+
+Replace Select dropdowns with a custom scroll-wheel time picker (iOS drum-picker style). Three columns: Hour (1-12), Minute (00/15/30/45), Period (AM/PM). CSS scroll-snap locks selection. Produces the same `"HH:MM"` 24-hour string format.
+
+## Component
+
+**File:** `src/components/ui/scroll-time-picker.tsx`
+
+**Props:**
+
+- `value: string` — "HH:MM" 24-hour format
+- `onValueChange: (value: string) => void`
+- `disabled?: boolean`
+- `className?: string`
+
+**Wheels:**
+
+- Hour: 1–12 (scroll-snap)
+- Minute: 00, 15, 30, 45 (scroll-snap)
+- Period: AM, PM (scroll-snap)
+
+**Styling:** shadcn/ui design tokens, dark mode via theme variables.
+
+## Integration Points
+
+1. `StartDayDialog.tsx` — 1 picker (start time)
+2. `TaskEditDialog.tsx` — 2 pickers (start + end)
+3. `ArchiveEditDialog.tsx` — 2 pickers (day start/end) + 2 per task (task start/end)
+
+Drop-in replacement: same value/onValueChange interface as current Select. Remove duplicated `generateTimeOptions()` from all files.
+
+## No Changes To
+
+- Data storage format (Date objects in memory, HH:MM strings in UI)
+- `parseTimeInput()`, `formatTimeForInput()`, `formatTime12Hour()` utilities
+- Task interface or data service layer
diff --git a/docs/plans/2026-02-06-scroll-time-picker-plan.md b/docs/plans/2026-02-06-scroll-time-picker-plan.md
new file mode 100644
index 0000000..354623a
--- /dev/null
+++ b/docs/plans/2026-02-06-scroll-time-picker-plan.md
@@ -0,0 +1,467 @@
+# Scroll Time Picker Implementation Plan
+
+> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
+
+**Goal:** Replace all time-selection Select dropdowns with a scroll-wheel time picker (hour/minute/period columns) for better UX.
+
+**Architecture:** A single reusable `ScrollTimePicker` component in `src/components/ui/scroll-time-picker.tsx` with the same `value`/`onValueChange` interface as the current Select dropdowns. Three CSS scroll-snap columns (Hour 1-12, Minute 00/15/30/45, Period AM/PM). Drop-in replacement across 3 files (4 dialog components).
+
+**Tech Stack:** React, TypeScript, Tailwind CSS, shadcn/ui design tokens
+
+---
+
+## Task 1: Create the ScrollTimePicker component
+
+**Files:**
+
+- Create: `src/components/ui/scroll-time-picker.tsx`
+
+**Step 1: Create the component file**
+
+Create `src/components/ui/scroll-time-picker.tsx` with the following implementation:
+
+```tsx
+import React, { useRef, useEffect, useCallback } from 'react';
+import { cn } from '@/lib/util';
+
+interface ScrollTimePickerProps {
+ value: string; // "HH:MM" 24-hour format
+ onValueChange: (value: string) => void;
+ disabled?: boolean;
+ className?: string;
+}
+
+const HOURS = Array.from({ length: 12 }, (_, i) => i + 1); // 1-12
+const MINUTES = [0, 15, 30, 45];
+const PERIODS = ['AM', 'PM'] as const;
+
+const ITEM_HEIGHT = 40; // px per item
+const VISIBLE_ITEMS = 5; // show 5 items, center is selected
+
+function parse24Hour(value: string): {
+ hour12: number;
+ minute: number;
+ period: 'AM' | 'PM';
+} {
+ const [h, m] = value.split(':').map(Number);
+ const period = h >= 12 ? 'PM' : 'AM';
+ let hour12 = h % 12;
+ hour12 = hour12 === 0 ? 12 : hour12;
+ // Round minute to nearest 15
+ const minute = Math.round(m / 15) * 15 === 60 ? 0 : Math.round(m / 15) * 15;
+ return { hour12, minute, period };
+}
+
+function to24Hour(hour12: number, minute: number, period: 'AM' | 'PM'): string {
+ let h = hour12;
+ if (period === 'AM' && h === 12) h = 0;
+ if (period === 'PM' && h !== 12) h += 12;
+ return `${h.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
+}
+
+interface WheelColumnProps {
+ items: (string | number)[];
+ selectedIndex: number;
+ onSelect: (index: number) => void;
+ disabled?: boolean;
+ formatItem?: (item: string | number) => string;
+}
+
+const WheelColumn: React.FC = ({
+ items,
+ selectedIndex,
+ onSelect,
+ disabled,
+ formatItem = String
+}) => {
+ const containerRef = useRef(null);
+ const isScrollingRef = useRef(false);
+ const scrollTimeoutRef = useRef>();
+
+ // Scroll to selected index on mount and when selectedIndex changes externally
+ useEffect(() => {
+ if (containerRef.current && !isScrollingRef.current) {
+ const scrollTop = selectedIndex * ITEM_HEIGHT;
+ containerRef.current.scrollTo({ top: scrollTop, behavior: 'smooth' });
+ }
+ }, [selectedIndex]);
+
+ const handleScroll = useCallback(() => {
+ if (!containerRef.current) return;
+ isScrollingRef.current = true;
+
+ if (scrollTimeoutRef.current) clearTimeout(scrollTimeoutRef.current);
+ scrollTimeoutRef.current = setTimeout(() => {
+ if (!containerRef.current) return;
+ const scrollTop = containerRef.current.scrollTop;
+ const index = Math.round(scrollTop / ITEM_HEIGHT);
+ const clampedIndex = Math.max(0, Math.min(items.length - 1, index));
+
+ // Snap to position
+ containerRef.current.scrollTo({
+ top: clampedIndex * ITEM_HEIGHT,
+ behavior: 'smooth'
+ });
+
+ isScrollingRef.current = false;
+ if (clampedIndex !== selectedIndex) {
+ onSelect(clampedIndex);
+ }
+ }, 100);
+ }, [items.length, selectedIndex, onSelect]);
+
+ const handleItemClick = (index: number) => {
+ if (disabled) return;
+ if (containerRef.current) {
+ containerRef.current.scrollTo({
+ top: index * ITEM_HEIGHT,
+ behavior: 'smooth'
+ });
+ }
+ onSelect(index);
+ };
+
+ const paddingHeight = Math.floor(VISIBLE_ITEMS / 2) * ITEM_HEIGHT;
+
+ return (
+
+ {/* Selection highlight band */}
+
+ {/* Fade overlays */}
+
+
+
+ {/* Top padding */}
+
+ {items.map((item, index) => (
+
handleItemClick(index)}
+ >
+ {formatItem(item)}
+
+ ))}
+ {/* Bottom padding */}
+
+
+
+ );
+};
+
+export const ScrollTimePicker: React.FC = ({
+ value,
+ onValueChange,
+ disabled = false,
+ className
+}) => {
+ const { hour12, minute, period } = parse24Hour(value || '09:00');
+
+ const hourIndex = HOURS.indexOf(hour12);
+ const minuteIndex = MINUTES.indexOf(minute);
+ const periodIndex = PERIODS.indexOf(period);
+
+ const handleHourChange = (index: number) => {
+ onValueChange(to24Hour(HOURS[index], minute, period));
+ };
+
+ const handleMinuteChange = (index: number) => {
+ onValueChange(to24Hour(hour12, MINUTES[index], period));
+ };
+
+ const handlePeriodChange = (index: number) => {
+ onValueChange(to24Hour(hour12, minute, PERIODS[index]));
+ };
+
+ return (
+
+
= 0 ? hourIndex : 0}
+ onSelect={handleHourChange}
+ disabled={disabled}
+ />
+ :
+ = 0 ? minuteIndex : 0}
+ onSelect={handleMinuteChange}
+ disabled={disabled}
+ formatItem={item => String(item).padStart(2, '0')}
+ />
+ = 0 ? periodIndex : 0}
+ onSelect={handlePeriodChange}
+ disabled={disabled}
+ />
+
+ );
+};
+```
+
+**Step 2: Add scrollbar-hide utility**
+
+Check `src/index.css` or Tailwind config for a `.scrollbar-hide` utility. If it doesn't exist, add to `src/index.css`:
+
+```css
+.scrollbar-hide {
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+}
+.scrollbar-hide::-webkit-scrollbar {
+ display: none;
+}
+```
+
+**Step 3: Verify the component builds**
+
+Run: `npm run build`
+Expected: No TypeScript or build errors
+
+**Step 4: Commit**
+
+```bash
+git add src/components/ui/scroll-time-picker.tsx src/index.css
+git commit -m "feat: add ScrollTimePicker component with scroll-wheel UI"
+```
+
+---
+
+## Task 2: Integrate into StartDayDialog
+
+**Files:**
+
+- Modify: `src/components/StartDayDialog.tsx`
+
+**Step 1: Replace Select with ScrollTimePicker**
+
+In `src/components/StartDayDialog.tsx`:
+
+1. Remove imports: `Select`, `SelectContent`, `SelectItem`, `SelectTrigger`, `SelectValue`
+2. Add import: `import { ScrollTimePicker } from "@/components/ui/scroll-time-picker";`
+3. Remove: `formatTime12Hour` function (lines 42-50)
+4. Remove: `TimeOption` type and `generateTimeOptions` function (lines 52-65)
+5. Remove: `const timeOptions = generateTimeOptions();` (line 75)
+6. Replace the Select block (lines 122-133) with:
+
+```tsx
+
+```
+
+**Step 2: Verify build**
+
+Run: `npm run build`
+Expected: PASS
+
+**Step 3: Commit**
+
+```bash
+git add src/components/StartDayDialog.tsx
+git commit -m "feat: use ScrollTimePicker in StartDayDialog"
+```
+
+---
+
+## Task 3: Integrate into TaskEditDialog
+
+**Files:**
+
+- Modify: `src/components/TaskEditDialog.tsx`
+
+**Step 1: Replace Select time pickers with ScrollTimePicker**
+
+In `src/components/TaskEditDialog.tsx`:
+
+1. Remove Select-related imports (lines 12-18) — only if Select is no longer used (category and project still use Select, so keep them)
+2. Add import: `import { ScrollTimePicker } from "@/components/ui/scroll-time-picker";`
+3. Remove: `TimeOption` type, `formatTime12Hour` function, and `generateTimeOptions` function (lines 128-152)
+4. Remove: `const timeOptions: TimeOption[] = generateTimeOptions();` (line 154)
+5. Replace the Start Time Select block (lines 419-435) with:
+
+```tsx
+ setTimeData(prev => ({ ...prev, startTime: value }))}
+/>
+```
+
+6. Replace the End Time Select block (lines 442-463) with:
+
+```tsx
+ setTimeData(prev => ({ ...prev, endTime: value }))}
+ disabled={!task.endTime}
+/>
+```
+
+7. Keep the Label for End Time as-is (with the "Currently Active" text)
+
+**Step 2: Verify build**
+
+Run: `npm run build`
+Expected: PASS
+
+**Step 3: Commit**
+
+```bash
+git add src/components/TaskEditDialog.tsx
+git commit -m "feat: use ScrollTimePicker in TaskEditDialog"
+```
+
+---
+
+## Task 4: Integrate into ArchiveEditDialog
+
+**Files:**
+
+- Modify: `src/components/ArchiveEditDialog.tsx`
+
+**Step 1: Replace all Select time pickers with ScrollTimePicker**
+
+In `src/components/ArchiveEditDialog.tsx`:
+
+1. Remove Select-related imports (lines 16-22) — category and project Selects in `TaskEditInArchiveDialog` still need them, so keep the imports
+2. Add import: `import { ScrollTimePicker } from "@/components/ui/scroll-time-picker";`
+3. Remove: `TimeOption` type and `generateTimeOptions` function (lines 88-103)
+4. Remove: `const timeOptions = generateTimeOptions();` at line 129
+5. Remove: `const timeOptions = generateTimeOptions();` at line 657
+
+**In ArchiveEditDialog (day start/end):**
+
+6. Replace day Start Time Select (lines 361-377) with:
+
+```tsx
+ setDayData(prev => ({ ...prev, startTime: value }))}
+/>
+```
+
+7. Replace day End Time Select (lines 383-397) with:
+
+```tsx
+ setDayData(prev => ({ ...prev, endTime: value }))}
+/>
+```
+
+**In TaskEditInArchiveDialog (task start/end):**
+
+8. Replace task Start Time Select (lines 841-857) with:
+
+```tsx
+ setTimeData(prev => ({ ...prev, startTime: value }))}
+/>
+```
+
+9. Replace task End Time Select (lines 862-878) with:
+
+```tsx
+ setTimeData(prev => ({ ...prev, endTime: value }))}
+/>
+```
+
+**Step 2: Verify build**
+
+Run: `npm run build`
+Expected: PASS
+
+**Step 3: Commit**
+
+```bash
+git add src/components/ArchiveEditDialog.tsx
+git commit -m "feat: use ScrollTimePicker in ArchiveEditDialog"
+```
+
+---
+
+## Task 5: Final verification and cleanup
+
+**Step 1: Run full lint**
+
+Run: `npm run lint`
+Expected: PASS (no new errors)
+
+**Step 2: Run full build**
+
+Run: `npm run build`
+Expected: PASS
+
+**Step 3: Verify no remaining generateTimeOptions references**
+
+Search codebase for `generateTimeOptions` — should return 0 results.
+
+**Step 4: Manual testing checklist**
+
+- [ ] StartDayDialog: scroll wheel appears, can select time, starts day correctly
+- [ ] TaskEditDialog: both start/end pickers work, end time disabled when task active
+- [ ] ArchiveEditDialog: day start/end pickers work
+- [ ] ArchiveEditDialog TaskEdit: task start/end pickers work
+- [ ] Scroll-snap locks to items properly
+- [ ] Mouse wheel scrolling works
+- [ ] Click-to-select works
+- [ ] Dark mode renders correctly
+- [ ] Mobile viewport works (responsive)
+
+**Step 5: Update CHANGELOG.md**
+
+Add under `[Unreleased]` > `Changed`:
+
+```markdown
+- Replaced time selection dropdowns with scroll-wheel time picker for better UX
+ — `src/components/ui/scroll-time-picker.tsx` (new), `StartDayDialog.tsx`, `TaskEditDialog.tsx`, `ArchiveEditDialog.tsx`
+```
+
+**Step 6: Final commit**
+
+```bash
+git add CHANGELOG.md
+git commit -m "chore: update CHANGELOG for scroll time picker"
+```
diff --git a/package-lock.json b/package-lock.json
index 863ff8c..a897da7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -72,6 +72,7 @@
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react-swc": "^3.5.0",
"autoprefixer": "^10.4.20",
+ "baseline-browser-mapping": "^2.9.19",
"eslint": "^9.9.0",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.9",
@@ -4186,7 +4187,7 @@
"version": "1.13.5",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.5.tgz",
"integrity": "sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ==",
- "dev": true,
+ "devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@@ -4228,7 +4229,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -4245,7 +4245,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -4262,7 +4261,6 @@
"cpu": [
"arm"
],
- "dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -4279,7 +4277,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -4296,7 +4293,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -4313,7 +4309,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -4330,7 +4325,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -4347,7 +4341,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -4364,7 +4357,6 @@
"cpu": [
"ia32"
],
- "dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -4381,7 +4373,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -4395,14 +4386,14 @@
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
- "dev": true,
+ "devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@swc/types": {
"version": "0.1.25",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz",
"integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==",
- "dev": true,
+ "devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@swc/counter": "^0.1.3"
@@ -5531,9 +5522,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
- "version": "2.8.30",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.30.tgz",
- "integrity": "sha512-aTUKW4ptQhS64+v2d6IkPzymEzzhw+G0bA1g3uBRV3+ntkH+svttKseW5IOR4Ed6NUVKqnY7qT3dKvzQ7io4AA==",
+ "version": "2.9.19",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
+ "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
diff --git a/package.json b/package.json
index 74103ad..a110e2f 100644
--- a/package.json
+++ b/package.json
@@ -82,6 +82,7 @@
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react-swc": "^3.5.0",
"autoprefixer": "^10.4.20",
+ "baseline-browser-mapping": "^2.9.19",
"eslint": "^9.9.0",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.9",
diff --git a/src/components/ArchiveEditDialog.tsx b/src/components/ArchiveEditDialog.tsx
index c32f82b..b00d315 100644
--- a/src/components/ArchiveEditDialog.tsx
+++ b/src/components/ArchiveEditDialog.tsx
@@ -1,25 +1,26 @@
-import React, { useState, useEffect } from "react";
+import React, { useState, useEffect } from 'react';
import {
- Dialog,
- DialogContent,
- DialogHeader,
- DialogTitle
-} from "@/components/ui/dialog";
-import { Callout } from "@/components/ui/callout";
-import { InfoCircledIcon } from "@radix-ui/react-icons";
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import { Textarea } from "@/components/ui/textarea";
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
-import { MarkdownDisplay } from "@/components/MarkdownDisplay";
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle
+} from '@/components/ui/dialog';
+import { Callout } from '@/components/ui/callout';
+import { InfoCircledIcon } from '@radix-ui/react-icons';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Textarea } from '@/components/ui/textarea';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import { MarkdownDisplay } from '@/components/MarkdownDisplay';
import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue
-} from "@/components/ui/select";
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue
+} from '@/components/ui/select';
+import { TimePicker } from '@/components/ui/scroll-time-picker';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import {
Table,
@@ -85,23 +86,6 @@ function formatTime12Hour(date: Date | undefined): string {
return `${hours}:${minutes.toString().padStart(2, '0')} ${ampm}`;
}
-type TimeOption = { value: string; label: string };
-function generateTimeOptions(): TimeOption[] {
- const options: TimeOption[] = [];
- for (let hour = 0; hour < 24; hour++) {
- for (let minute = 0; minute < 60; minute += 15) {
- const value = `${hour.toString().padStart(2, '0')}:${minute
- .toString()
- .padStart(2, '0')}`;
- const date = new Date();
- date.setHours(hour, minute, 0, 0);
- const label = formatTime12Hour(date);
- options.push({ value, label });
- }
- }
- return options;
-}
-
export const ArchiveEditDialog: React.FC = ({
day,
isOpen,
@@ -126,7 +110,6 @@ export const ArchiveEditDialog: React.FC = ({
});
const [tasks, setTasks] = useState([]);
- const timeOptions = generateTimeOptions();
// Initialize form data when dialog opens
useEffect(() => {
@@ -168,7 +151,7 @@ export const ArchiveEditDialog: React.FC = ({
const handleSaveDay = async () => {
// Parse the new date from the input (same as StartDayDialog)
- const [year, month, dayOfMonth] = dayData.date.split("-").map(Number);
+ const [year, month, dayOfMonth] = dayData.date.split('-').map(Number);
const selectedDate = new Date(year, month - 1, dayOfMonth);
// Create new start/end times with the selected date but original times
@@ -216,8 +199,8 @@ export const ArchiveEditDialog: React.FC = ({
await updateArchivedDay(day.id, updatedDay);
setIsEditing(false);
} catch (error) {
- console.error("Failed to save archived day:", error);
- alert("Failed to save changes. Please try again.");
+ console.error('Failed to save archived day:', error);
+ alert('Failed to save changes. Please try again.');
}
};
@@ -245,7 +228,7 @@ export const ArchiveEditDialog: React.FC = ({
};
const handleTaskSave = (updatedTask: Task) => {
- const updatedTasks = tasks.map((t) =>
+ const updatedTasks = tasks.map(t =>
t.id === updatedTask.id ? updatedTask : t
);
setTasks(updatedTasks);
@@ -253,7 +236,7 @@ export const ArchiveEditDialog: React.FC = ({
};
const handleTaskDelete = (taskId: string) => {
- const updatedTasks = tasks.filter((t) => t.id !== taskId);
+ const updatedTasks = tasks.filter(t => t.id !== taskId);
setTasks(updatedTasks);
};
@@ -280,7 +263,6 @@ export const ArchiveEditDialog: React.FC = ({
{formatDate(day.startTime)}
-
-
-
+ aria-label="Day start time"
+ />
-
-
+ aria-label="Day end time"
+ />
-
-
-
-
- Edit
- Preview
-
-
-
-
-
- {dayData.notes ? (
-
- ) : (
-
No notes to preview
- )}
-
-
-
-
+
+
+
+
+ Edit
+ Preview
+
+
+
+
+
+ {dayData.notes ? (
+
+ ) : (
+
+ No notes to preview
+
+ )}
+
+
+
+
) : (
@@ -462,14 +428,14 @@ export const ArchiveEditDialog: React.FC
= ({
{tasks.length} total
- {day.notes && (
-
- )}
+ {day.notes && (
+
+ )}
)}
@@ -494,23 +460,25 @@ export const ArchiveEditDialog: React.FC = ({
- {tasks.map((task) => {
+ {tasks.map(task => {
const category = categories.find(
- (c) => c.id === task.category
+ c => c.id === task.category
);
return (
-
-
{task.title}
-
- {task.description && (
-
-
-
- )}
-
-
+
+
{task.title}
+
+ {task.description && (
+
+
+
+ )}
+
+
{category && (
@@ -541,7 +509,9 @@ export const ArchiveEditDialog: React.FC = ({
{formatTime12Hour(task.startTime)}
- {task.endTime ? formatTime12Hour(task.endTime) : '-'}
+ {task.endTime
+ ? formatTime12Hour(task.endTime)
+ : '-'}
{formatDuration(task.duration || 0)}
@@ -623,7 +593,7 @@ export const ArchiveEditDialog: React.FC = ({
/>
)}
-
+
);
};
@@ -654,12 +624,10 @@ const TaskEditInArchiveDialog: React.FC = ({
endTime: ''
});
- const timeOptions = generateTimeOptions();
-
useEffect(() => {
if (isOpen && task) {
const projectId =
- projects.find((p) => p.name === task.project)?.id || 'none';
+ projects.find(p => p.name === task.project)?.id || 'none';
setFormData({
title: task.title || '',
@@ -696,11 +664,11 @@ const TaskEditInArchiveDialog: React.FC = ({
const handleSave = () => {
const selectedProject =
formData.project !== 'none'
- ? projects.find((p) => p.id === formData.project)
+ ? projects.find(p => p.id === formData.project)
: undefined;
const selectedCategory =
formData.category !== 'none'
- ? categories.find((c) => c.id === formData.category)
+ ? categories.find(c => c.id === formData.category)
: undefined;
const newStartTime = parseTimeInput(timeData.startTime, task.startTime);
@@ -739,52 +707,54 @@ const TaskEditInArchiveDialog: React.FC = ({
- setFormData((prev) => ({ ...prev, title: e.target.value }))
+ onChange={e =>
+ setFormData(prev => ({ ...prev, title: e.target.value }))
}
placeholder="Enter task title"
/>
-
-
-
-
- Edit
- Preview
-
-
-
-
-
- {formData.description ? (
-
- ) : (
-
No description to preview
- )}
-
-
-
-
+
+
+
+
+ Edit
+ Preview
+
+
+
+
+
+ {formData.description ? (
+
+ ) : (
+
+ No description to preview
+
+ )}
+
+
+
+