diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8e135d9..e83465b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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 `
` 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`
diff --git a/CLAUDE.md b/CLAUDE.md
index 9cba0e5..4a8dbc6 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -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.
@@ -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 |
---
@@ -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) |
---
@@ -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:**
@@ -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
---
diff --git a/README-EXT.md b/README-EXT.md
index c356eb7..eac1af5 100644
--- a/README-EXT.md
+++ b/README-EXT.md
@@ -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
@@ -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`.
diff --git a/README.md b/README.md
index 95c3100..54af6db 100644
--- a/README.md
+++ b/README.md
@@ -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
---
diff --git a/capacitor.config.ts b/capacitor.config.ts
index a65c557..a761423 100644
--- a/capacitor.config.ts
+++ b/capacitor.config.ts
@@ -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
+ }
}
};
diff --git a/index.html b/index.html
index b764c49..24c0ac4 100644
--- a/index.html
+++ b/index.html
@@ -25,7 +25,7 @@
-
+
diff --git a/package-lock.json b/package-lock.json
index 53bc9bc..6cbee5e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,8 +8,12 @@
"name": "timetraked",
"version": "0.45.0",
"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",
@@ -1722,6 +1726,15 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@capacitor/app": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/@capacitor/app/-/app-8.1.0.tgz",
+ "integrity": "sha512-MlmttTOWHDedr/G4SrhNRxsXMqY+R75S4MM4eIgzsgCzOYhb/MpCkA5Q3nuOCfL1oHm26xjUzqZ5aupbOwdfYg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@capacitor/core": ">=8.0.0"
+ }
+ },
"node_modules/@capacitor/cli": {
"version": "8.3.1",
"resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-8.3.1.tgz",
@@ -1764,6 +1777,15 @@
"tslib": "^2.1.0"
}
},
+ "node_modules/@capacitor/haptics": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-8.0.2.tgz",
+ "integrity": "sha512-c2hZzRR5Fk1tbTvhG1jhh2XBAf3EhnIerMIb2sl7Mt41Gxx1fhBJFDa0/BI1IbY4loVepyyuqNC9820/GZuoWQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@capacitor/core": ">=8.0.0"
+ }
+ },
"node_modules/@capacitor/ios": {
"version": "8.3.1",
"resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-8.3.1.tgz",
@@ -1773,6 +1795,24 @@
"@capacitor/core": "^8.3.0"
}
},
+ "node_modules/@capacitor/keyboard": {
+ "version": "8.0.3",
+ "resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-8.0.3.tgz",
+ "integrity": "sha512-27Bv5/2w1Ss2njguBgTS98O0Bb8DRJhAARyzXYib0JlT/n6BrJw/EZ0CokM4C8GFUjFDjJnEKF1Ie01buTMEXQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@capacitor/core": ">=8.0.0"
+ }
+ },
+ "node_modules/@capacitor/status-bar": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-8.0.2.tgz",
+ "integrity": "sha512-WXs8YB8B9eEaPZz+bcdY6t2nForF1FLoj/JU0Dl9RRgQnddnS98FEEyDooQhaY7wivr000j4+SC1FyeJkrFO7A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@capacitor/core": ">=8.0.0"
+ }
+ },
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
diff --git a/package.json b/package.json
index bf42e97..a0ab759 100644
--- a/package.json
+++ b/package.json
@@ -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",
@@ -81,6 +85,7 @@
"@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",
@@ -88,7 +93,6 @@
"@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",
diff --git a/public/pwa.css b/public/pwa.css
index b020bce..6a8f6b7 100644
--- a/public/pwa.css
+++ b/public/pwa.css
@@ -15,11 +15,17 @@ body {
padding-right: var(--safe-area-inset-right);
}
+/* IosPageHeader consumes the safe-area-inset-top itself via paddingTop inline style.
+ Prevent double-stacking when running as a Capacitor iOS build. */
+.ios-build body {
+ padding-top: 0;
+}
+
/* Add padding bottom for mobile nav on mobile devices */
/* Note: iOS-specific padding is handled in the iOS section below */
@media (max-width: 768px) {
body:not(.ios-device) {
- padding-bottom: calc(4rem + var(--safe-area-inset-bottom));
+ padding-bottom: calc(3rem + var(--safe-area-inset-bottom));
}
}
@@ -85,23 +91,24 @@ button,
}
}
-/* Pull-to-refresh prevention (optional, can be enabled if needed) */
+/* Prevent pull-to-refresh on non-iOS browsers */
body {
overscroll-behavior-y: contain;
}
+/* Restore native rubber-band bounce on iOS — body is fixed/overflow:hidden there
+ so this applies to #root (the actual scroll container). */
+@supports (-webkit-touch-callout: none) {
+ #root {
+ overscroll-behavior-y: auto;
+ }
+}
+
/* Improve button touch targets on mobile */
@media (max-width: 768px) {
- /* button:not(.icon-only) { */
- /* min-height: 44px; */
- /* min-width: 44px; */
- /* padding: 0.75rem 1rem; */
- /* } */
-
- /* Larger tap targets for icon buttons */
- button.icon-only {
- min-width: 44px;
+ button:not([aria-hidden="true"]) {
min-height: 44px;
+ min-width: 44px;
}
}
@@ -138,6 +145,15 @@ body {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
+/* Ensure focused inputs scroll into view above the keyboard with enough clearance */
+@supports (-webkit-touch-callout: none) {
+ input:focus,
+ textarea:focus,
+ select:focus {
+ scroll-margin-bottom: 24px;
+ }
+}
+
/* iOS specific fixes */
@supports (-webkit-touch-callout: none) {
@@ -198,6 +214,37 @@ body {
}
}
+/* Prevent scroll from leaking through open bottom sheets to the page beneath */
+[data-vaul-drawer] {
+ touch-action: pan-y;
+ overscroll-behavior: contain;
+}
+
+/* Lift the floating action button above the mobile nav + home indicator on iOS */
+@supports (-webkit-touch-callout: none) {
+ .fab-nav-offset {
+ bottom: calc(4rem + env(safe-area-inset-bottom, 0px)) !important;
+ }
+}
+
+/* iOS page transition: subtle slide-in from the right on route change */
+@supports (-webkit-touch-callout: none) {
+ .page-transition-enter {
+ animation: iosSlideIn 280ms cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
+ }
+
+ @keyframes iosSlideIn {
+ from {
+ transform: translateX(30px);
+ opacity: 0.8;
+ }
+ to {
+ transform: translateX(0);
+ opacity: 1;
+ }
+ }
+}
+
/* Android specific fixes */
@media (max-width: 768px) {
diff --git a/src/App.tsx b/src/App.tsx
index 80e2032..335ebbd 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,14 +1,15 @@
import { Toaster } from "@/components/ui/toaster";
import { Toaster as Sonner } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
-import { HashRouter, Routes, Route, Navigate } from "react-router-dom";
+import { HashRouter, Routes, Route, Navigate, useLocation } from "react-router-dom";
import { AuthProvider } from "@/contexts/AuthContext";
import { OfflineProvider } from "@/contexts/OfflineContext";
import { TimeTrackingProvider } from "@/contexts/TimeTrackingContext";
import { useAuth } from "@/hooks/useAuth";
-import { Suspense, lazy } from "react";
+import { Suspense, lazy, useState, useEffect } from "react";
import { InstallPrompt } from "@/components/InstallPrompt";
import { UpdateNotification } from "@/components/UpdateNotification";
+import { useStatusBar } from "@/hooks/useStatusBar";
const isIosBuild = import.meta.env.VITE_IOS_BUILD === "true";
import { MobileNav } from "@/components/MobileNav";
@@ -38,26 +39,50 @@ const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
return <>{children}>;
};
+const AppShell = () => {
+ const [isDark, setIsDark] = useState(
+ () => window.matchMedia("(prefers-color-scheme: dark)").matches
+ );
+ useEffect(() => {
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
+ const handler = (e: MediaQueryListEvent) => setIsDark(e.matches);
+ mq.addEventListener("change", handler);
+ return () => mq.removeEventListener("change", handler);
+ }, []);
+ useStatusBar(isDark);
+ return null;
+};
+
+const AnimatedRoutes = () => {
+ const location = useLocation();
+ return (
+