Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Dedicated Tasks page (`/tasks`) — task list, NewTaskForm, and End Day button moved out of the dashboard column into a standalone page; starting a day auto-navigates to `/tasks`; day summary displayed on the Tasks page after ending the day; dashboard shows a compact "Day In Progress" card with task count and a "View Tasks" link while a day is active
— `src/pages/TaskList.tsx` (new), `src/pages/Index.tsx`, `src/App.tsx`
- Apple HIG compliance pass for the native iOS app
- **Bottom sheets** — `TaskEditDialog`, `StartDayDialog`, `ArchiveEditDialog`, and `DeleteConfirmationDialog` now render as swipe-to-dismiss vaul `Drawer` sheets on iOS (snap points tuned per dialog complexity) and fall back to the existing centered Radix `Dialog` on web. `DeleteConfirmationDialog` reverses button order on iOS (destructive action above Cancel) per UIAlertController convention.
— `src/components/ui/adaptive-dialog.tsx` (new), `src/components/ui/drawer.tsx`, `src/components/TaskEditDialog.tsx`, `src/components/StartDayDialog.tsx`, `src/components/ArchiveEditDialog.tsx`, `src/components/DeleteConfirmationDialog.tsx`
- **Haptic feedback** — `useHaptics` wraps `@capacitor/haptics`; light impact on tab switches and Edit, medium on Delete, heavy on destructive confirm, success notification on task creation and day archive, error notification on sync failure. No-op on web.
— `src/hooks/useHaptics.ts` (new), `src/components/MobileNav.tsx`, `src/components/TaskItem.tsx`, `src/components/DeleteConfirmationDialog.tsx`, `src/contexts/TimeTrackingContext.tsx`
- **App lifecycle persistence** — `useAppLifecycle` uses `@capacitor/app`'s `appStateChange` event (fires at the Swift layer before WKWebView freezes) instead of `visibilitychange` for the emergency localStorage backup, eliminating the race condition on rapid app backgrounding. Falls back to `visibilitychange` on web.
— `src/hooks/useAppLifecycle.ts` (new), `src/contexts/TimeTrackingContext.tsx`
- **Status bar theming** — `useStatusBar` syncs the iOS status bar text colour (white in dark mode, black in light mode) via `@capacitor/status-bar`; `apple-mobile-web-app-status-bar-style` updated to `black-translucent` so the web view extends behind the status bar region. No-op on web.
— `src/hooks/useStatusBar.ts` (new), `src/App.tsx`, `index.html`
- **iOS navigation header** — desktop `SiteNavigationMenu` is hidden on iOS builds and replaced with `IosPageHeader`: a sticky 17px SF-style title bar with safe-area-inset-top padding, back chevron, and right-side action slot. `ios-build` class added to `<body>` on iOS to prevent double-stacking of safe-area padding.
— `src/components/IosPageHeader.tsx` (new), `src/components/PageLayout.tsx`, `src/main.tsx`, `public/pwa.css`
- **Keyboard avoidance** — `@capacitor/keyboard` configured with `resize: body` so the viewport shrinks above the keyboard. `useKeyboardHeight` hook tracks keyboard height and applies it as `paddingBottom` on `DrawerContent` so form fields inside bottom sheets remain accessible. `scroll-margin-bottom: 24px` added for native scroll-into-view on input focus.
— `src/hooks/useKeyboardHeight.ts` (new), `capacitor.config.ts`, `src/components/ui/drawer.tsx`, `public/pwa.css`
- **Long-press context menus** — `useLongPress` fires a 500 ms hold callback; `TaskItem` wraps cards in a Radix `ContextMenu` (right-click on desktop, long-press on iOS) with Edit and Delete actions. Action buttons hidden on iOS builds where context menus serve as the primary affordance.
— `src/hooks/useLongPress.ts` (new), `src/components/TaskItem.tsx`
- **Page transition animations** — route changes in the iOS build play a subtle 280 ms slide-in from the right (`cubic-bezier(0.25, 0.46, 0.45, 0.94)`), scoped to `@supports (-webkit-touch-callout: none)` so the animation never runs on web.
— `src/App.tsx` (`AnimatedRoutes` component), `public/pwa.css`
- **New Capacitor plugins** installed: `@capacitor/app`, `@capacitor/haptics`, `@capacitor/status-bar`, `@capacitor/keyboard` (all v8.x, matching the existing core/ios versions).

### Changed

- **Touch targets** — `Button` `size="sm"` raised from `h-9` (36 px) to `h-10` (40 px); mobile CSS now enforces `min-height: 44px` on all non-hidden buttons at ≤768 px (previously commented out).
— `src/components/ui/button.tsx`, `public/pwa.css`
- **Rubber-band scroll bounce** — `overscroll-behavior-y` restored to `auto` on `#root` inside the iOS `@supports` block so the native bounce animation works again (was `contain` globally which suppressed it). vaul drawer elements gain `overscroll-behavior: contain` + `touch-action: pan-y` to prevent scroll bleed through open sheets.
— `public/pwa.css`


- Tasks navigation item added to desktop top nav and mobile bottom nav; mobile nav grid updated to support up to five items for authenticated users
— `src/components/Navigation.tsx`, `src/components/MobileNav.tsx`

Expand Down
37 changes: 32 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# CLAUDE.md - AI Assistant Codebase Guide

**Last Updated:** 2026-04-26
**Version:** 2.2.0
**Last Updated:** 2026-05-14
**Version:** 2.3.0

Timetraked is a React 18 + TypeScript time tracking PWA for freelancers and consultants, with dual storage (localStorage guest mode and optional Supabase cloud sync). A native iOS app is also available via Capacitor.

Expand Down Expand Up @@ -31,7 +31,7 @@ After implementing changes, run lint and tests before considering a task complet
| Forms | React Hook Form + Zod |
| Backend | Supabase (optional) or localStorage |
| PWA | Vite PWA Plugin + Workbox |
| Native iOS | Capacitor 8 (@capacitor/core + @capacitor/ios) |
| Native iOS | Capacitor 8 (@capacitor/core + @capacitor/ios + @capacitor/app + @capacitor/haptics + @capacitor/status-bar + @capacitor/keyboard) |
| Testing | Vitest + React Testing Library + Playwright |

---
Expand Down Expand Up @@ -70,8 +70,15 @@ export const MyComponent = () => {
| `src/lib/supabase.ts` | Supabase client configuration and caching |
| `src/config/categories.ts` | Default category definitions |
| `src/config/projects.ts` | Default project definitions |
| `src/components/PageLayout.tsx` | Shared page chrome (title + optional actions slot) |
| `capacitor.config.ts` | Capacitor iOS configuration |
| `src/components/PageLayout.tsx` | Shared page chrome (title + optional actions slot); renders `IosPageHeader` on iOS |
| `src/components/IosPageHeader.tsx` | iOS-only sticky nav bar with safe-area-inset-top, back chevron, and action slot |
| `src/components/ui/adaptive-dialog.tsx` | Renders vaul `Drawer` on iOS, Radix `Dialog` on web |
| `src/hooks/useHaptics.ts` | `@capacitor/haptics` wrapper (light/medium/heavy, success/error) |
| `src/hooks/useStatusBar.ts` | `@capacitor/status-bar` wrapper — syncs bar style with dark/light mode |
| `src/hooks/useAppLifecycle.ts` | `@capacitor/app` appStateChange hook for reliable background persistence |
| `src/hooks/useKeyboardHeight.ts` | `@capacitor/keyboard` reactive height for bottom-sheet form padding |
| `src/hooks/useLongPress.ts` | 500 ms hold detector for context menu trigger on touch |
| `capacitor.config.ts` | Capacitor iOS configuration (Keyboard resize plugin configured here) |
| `.env.ios` | iOS build env (VITE_IOS_BUILD=true, no Supabase) |

---
Expand All @@ -92,6 +99,23 @@ The app ships as both a PWA and a native iOS app via Capacitor 8.
- Routing uses `HashRouter` (required — Capacitor loads from filesystem, not a server)
- CSP includes `capacitor://localhost` for WKWebView asset loading
- Data storage is localStorage-only (no Supabase keys in `.env.ios`)
- Desktop `SiteNavigationMenu` is hidden; `IosPageHeader` renders instead (sticky, safe-area-aware, back chevron)
- All edit/confirm dialogs (`TaskEditDialog`, `StartDayDialog`, `ArchiveEditDialog`, `DeleteConfirmationDialog`) become bottom sheets via `AdaptiveDialog`; on web the existing Radix Dialog renders unchanged
- Haptic feedback fires on every meaningful interaction via `useHaptics`
- `@capacitor/app` `appStateChange` event used for emergency data persistence (more reliable than `visibilitychange`)
- `@capacitor/status-bar` syncs status bar text colour with system dark/light mode
- `@capacitor/keyboard` configured with `resize: body`; `useKeyboardHeight` lifts bottom-sheet content above the keyboard
- Long-press on task cards opens a context menu (Edit / Delete); on-card action buttons are hidden

**Installed Capacitor plugins** (all v8.x):

| Package | Purpose |
| ------- | ------- |
| `@capacitor/core` + `@capacitor/ios` | Core bridge (pre-existing) |
| `@capacitor/app` | Native app lifecycle events (pause/resume) |
| `@capacitor/haptics` | Tactile feedback |
| `@capacitor/status-bar` | Status bar style control |
| `@capacitor/keyboard` | Keyboard height events and viewport resize |

**iOS npm scripts:**

Expand All @@ -114,6 +138,9 @@ When working on iOS/Capacitor projects, remember that `cap sync` overwrites Pack
- Gate any web-only UI (PWA install, auth, sync) behind `import.meta.env.VITE_IOS_BUILD !== "true"`
- Avoid `window.location.reload()` in iOS paths — use `window.location.replace()` to avoid interrupting the Capacitor JS bridge
- Test localStorage-only flow (no Supabase) before marking iOS features complete
- For new dialogs/modals: use `AdaptiveDialog` (`src/components/ui/adaptive-dialog.tsx`) instead of `Dialog` directly — it automatically renders a bottom sheet on iOS
- Add haptic feedback for new interactions via `useHaptics` (`src/hooks/useHaptics.ts`): `lightImpact` for navigation/selection, `mediumImpact` for intent to delete, `heavyImpact` for confirmed destructive actions, `successNotify`/`errorNotify` for outcomes
- All new Capacitor plugin calls should be gated with `Capacitor.isNativePlatform()` or imported dynamically (see existing hooks for the pattern) so the web build never fails at runtime on missing native APIs

---

Expand Down
17 changes: 16 additions & 1 deletion README-EXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,21 @@ Task descriptions support **GitHub Flavored Markdown (GFM)**:

**Native-Like Experience:** Standalone window, app icon, splash screen on launch.

### iOS Native App (Capacitor)

The Capacitor build (`VITE_IOS_BUILD=true`) includes additional Apple HIG enhancements that are inactive in the PWA:

| Feature | Detail |
| ------- | ------- |
| **Bottom sheets** | All edit/confirm dialogs slide up as swipe-to-dismiss sheets instead of centered overlays |
| **Haptic feedback** | Light impact on navigation taps, medium on destructive intent, success/error notifications on outcomes |
| **Status bar theming** | Status bar text colour tracks light/dark mode; content extends behind the status bar via `black-translucent` |
| **iOS navigation header** | Sticky 17 px title bar with safe-area-inset-top padding and back chevron replaces the desktop nav bar |
| **Keyboard avoidance** | Viewport shrinks above the software keyboard; bottom sheet forms scroll above it automatically |
| **Long-press context menus** | Hold a task card to reveal Edit / Delete without on-card buttons cluttering the layout |
| **Page transitions** | Subtle 280 ms slide-in animation on route changes, matching the iOS push-navigation idiom |
| **Rubber-band bounce** | Native scroll bounce restored on the main scroll container |

---

## Authentication & Storage
Expand All @@ -180,7 +195,7 @@ Timetraked uses an **action-triggered save** approach optimized for single-devic

1. **In-Memory First** — changes update React state immediately.
2. **Action Saves** — every task mutation (start, update, delete) and day lifecycle event (start day, end day) triggers an immediate `saveCurrentDay()` call with the freshly computed state, keeping localStorage and Supabase in sync without a debounce delay.
3. **Emergency Backups** — `visibilitychange` (iOS app backgrounding) and `beforeunload` (browser close) write a synchronous localStorage snapshot as a last-resort fallback before JavaScript execution is suspended.
3. **Emergency Backups** — on iOS, `@capacitor/app`'s `appStateChange` event fires at the Swift layer before WKWebView freezes, giving a reliable save window; on web, `visibilitychange` and `beforeunload` write a synchronous localStorage snapshot as a last-resort fallback before JavaScript execution is suspended.
4. **Manual Sync** — the sync button in the navigation saves all data types (tasks, projects, categories, archived days, todos) in one batch, useful after recovering from an error.

When you sign in, your `localStorage` data automatically migrates to Supabase (timestamps compared to prevent overwriting newer data, no data loss). When you sign out, Supabase data syncs back to `localStorage`.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ A Progressive Web App (PWA) for time tracking built with React, TypeScript, and
- **CSV Import** — bring in existing time data from other tools
- **Weekly Report** — AI-generated work summaries (standup, client, or retrospective tone)
- **No Account Required** — full functionality with local storage; optional cloud sync via Supabase
- **PWA + Native iOS** — installable on desktop/mobile; distributed as a native iOS app via Capacitor 8
- **PWA + Native iOS** — installable on desktop/mobile; distributed as a native iOS app via Capacitor 8 with Apple HIG-compliant bottom sheets, haptic feedback, status bar theming, keyboard avoidance, and native page transitions

---

Expand Down
8 changes: 7 additions & 1 deletion capacitor.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@ const config: CapacitorConfig = {
},

plugins: {
// Placeholder — native plugin config goes here in Phase 4 (widget bridge)
Keyboard: {
// Shrink the body when the keyboard appears so fixed-bottom UI (MobileNav,
// bottom sheets) moves up with the keyboard automatically.
resize: 'body',
style: 'default',
resizeOnFullScreen: true
}
}
};

Expand Down
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

<!-- iOS Specific Meta Tags -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />

<!-- Microsoft Specific Meta Tags -->
<meta name="msapplication-TileColor" content="#3b82f6" />
Expand Down
40 changes: 40 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@
"screenshots:headed": "playwright test screenshots.spec.ts --headed"
},
"dependencies": {
"@capacitor/app": "^8.1.0",
"@capacitor/core": "^8.3.1",
"@capacitor/haptics": "^8.0.2",
"@capacitor/ios": "^8.3.1",
"@capacitor/keyboard": "^8.0.3",
"@capacitor/status-bar": "^8.0.2",
"@hookform/resolvers": "^3.10.0",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
Expand Down Expand Up @@ -81,14 +85,14 @@
"@rollup/plugin-terser": ">=0.4.4"
},
"devDependencies": {
"@capacitor/cli": "^8.3.1",
"@eslint/js": "^9.39.4",
"@playwright/test": "^1.56.1",
"@tailwindcss/typography": "^0.5.15",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^14.3.1",
"@types/node": "^22.19.17",
"@types/react": "^18.3.28",
"@capacitor/cli": "^8.3.1",
"@types/react-dom": "^18.3.7",
"@vitejs/plugin-react-swc": "^3.11.0",
"autoprefixer": "^10.4.21",
Expand Down
Loading
Loading