Mobile Native-Feel Transformation
For Agents
Implementation has shipped. The brainstorm + spec phase moved to the Decision Log section below; the rest of this note describes the as-built mobile architecture. Source tree:
web/src/mobile/. Active topic — additions arriving as more polish ships.
Architecture (As-Built)
web/src/mobile/
├── shell/ # MobileShell, MobileHeader, BottomTabs, SheetStack
├── lib/ # useIsMobile, usePullToRefresh, useSafeArea, formatTime
├── home/ # MobileHome + StatTileGrid, ForecastHero, UpNextCard, ProjectHealthCard
├── plan/ # MobilePlan + NowHeroCard, UpNextBlockCard, LaterTodayList,
│ # DoneTodayCollapse, BlockCreateSheet, BlockEditSheet,
│ # MorningCheckinSheet, StandupNotesSheet, LunchCheckinSheet,
│ # EveningReflectionSheet, EveningChecklistSheet
├── hours/ # MobileHours + WeekStrip, useTimesheetWeek, EntryEditSheet
├── money/ # MobileMoney + BudgetGlanceCard, InvoiceListItem, BudgetDetailSheet
├── more/ # MobileMore + SectionLink
└── home/UpNextCard.tsx, etc.
Routing
App.tsx branches on useIsMobile() (matchMedia('(max-width: 768px)')):
- Mobile branch →
<MobileApp>— wraps<Routes>in<MobileShell>. Same URLs render mobile components for the 5 tabs (/,/standup,/timesheets,/invoices+/budget,/more). - Desktop branch →
<DesktopApp>— existing<AppShell>sidebar layout, zero changes.
AppShell is NOT mounted on mobile
MobileApp deliberately skips AppShell. Mounting it on mobile produced a vaul + Radix Dialog scroll-lock conflict on iOS that broke scroll. The mobile shell composes the equivalent providers itself.
Drawer Pattern (vaul)
@levandor/ui exposes a shadcn-style named-export wrapper around vaul:
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from '@levandor/ui'Not the namespaced form (Drawer.Root, Drawer.Portal, etc). <DrawerContent> already wraps Portal + Overlay and renders its own drag handle — don’t add another portal or handle.
Added as a peer dep to @levandor/ui in commit b2b8b7c.
iOS Gotchas (Codified)
| Gotcha | Workaround |
|---|---|
| Long-press drag selecting text | WebkitTouchCallout: 'none' + userSelect: 'none' on swipeable rows |
| Vertical scroll fights horizontal swipe | touchAction: 'pan-y' on swipeable rows |
| Input zoom-in on focus | Inputs ≥16px text size |
ISO 'YYYY-MM-DDTHH:MM' to UTC drift | Use datetime-local inputs and round-trip through local-time helpers; never .toISOString().slice(0,10) (see debugging-log-crm) |
window.confirm() blocked / off-style in PWA | Replaced with inline two-step: Cancel + Confirm-delete (rose-600). See BlockEditSheet, EntryEditSheet. Commit dcdbec7. |
<object> PDF embed doesn’t render in PWA standalone | Link-out instead |
<select> with appearance-none + Lucide ChevronDown overlay → invisible text | Explicit text-zinc-900 + placeholder:text-zinc-400. Commit 7f4bc84. |
Patterns
lastEditIdRef (sheet close-animation)
vaul’s drawer close animation runs ~250ms. If we drop the data prop on close, the sheet flickers blank. Pattern:
const lastEditIdRef = useRef<string | null>(null)
const [editId, setEditId] = useState<string | null>(null)
useEffect(() => {
if (editId) lastEditIdRef.current = editId
}, [editId])
// Sheet stays mounted while standupId truthy:
<Drawer open={!!editId} onOpenChange={(o) => !o && setEditId(null)}>
<BlockEditSheet blockId={editId ?? lastEditIdRef.current} />
</Drawer>__setPlanQuickAdd typed-global hatch
BottomTabs needs to fire a quick-add action on long-press of the Plan tab, but lives outside MobilePlan’s tree. Solution: typed window global installed by MobilePlan:
declare global {
interface Window {
__setPlanQuickAdd?: (fn: () => void) => void
}
}MobileShell exposes it via context; MobilePlan mounts/unmounts a handler; BottomTabs long-press calls it (no-op if not installed).
usePullToRefresh (iOS-style elastic)
Hook that binds touch listeners with stable effect deps — early version rebound 50+ touch listeners per gesture, breaking iOS PtR. Commit ee1652e fixed the deps.
Time-aware Plan CTAs
In MobilePlan:
activeanddoneOrCancelledcollections are status-only and memoizedupcomingPlannedandlaterare time-dependent and intentionally NOT memoized — they roll forward as time passes within the same render
Phase 5-10 Implementation (18 commits)
Phase 5 — Plan tab
| SHA | Item |
|---|---|
b2b8b7c | T5.1 shadcn Drawer (vaul wrapper) added to @levandor/ui peer deps |
f05fe97 | T5.2 NowHeroCard |
3568efc | T5.3 UpNextBlockCard |
10492a6 | T5.4 BlockCreateSheet + BlockEditSheet (datetime-local + timezone roundtrip) |
38bb774 | T5.5 LaterTodayList swipe-left actions (drag-reorder deferred) |
668df40 | T5.6 DoneTodayCollapse |
9f39ff3 | T5.7 StandupStreamSheet (later replaced — see mobile-day-rituals) |
b14bfd8 | T5.8 MobilePlan composition with lastEditIdRef + __setPlanQuickAdd global |
Phase 6 — Hours tab
| SHA | Item |
|---|---|
c0635e1 | T6.1 WeekStrip (7 day-pills) |
69bad06 | T6.2 MobileHours + useTimesheetWeek + TimesheetWeekRow extension type |
Phase 7 — Money tab
| SHA | Item |
|---|---|
a790871 | T7.1 BudgetGlanceCard |
c76fd78 | T7.2 MobileMoney + InvoiceListItem (built ourselves — InvoiceCard doesn’t exist) + BudgetDetailSheet v1 |
Phase 8-10
| SHA | Item |
|---|---|
2e293c8 | T8.1 MobileMore + SectionLink (CF Access logout) |
2388d77 | T9.1 SearchSheet (per-tab v1 stub via SearchPlaceholder) |
272d350 | T9.2 Sonner top-center on mobile |
e58977e | T10.1 Mock factories |
d546a58 | T10.2 MobileShell test |
04d027b | T10.3 MobileHome/Plan/Hours/Money render+state tests + IntersectionObserver stub |
Audit Pass (post-Phase 10)
| SHA | Audit fix |
|---|---|
60a1acd | Sync pnpm-lock.yaml with vaul peer dep — was blocking CF Pages frozen-lockfile build |
01880a0 | Hours-tab “garbage hours” bug — fixed UTC date drift via new toLocalDateString() util in web/src/lib/date-utils.ts. Same bug in useDashboardSummary.ts. Dedup startOfWeek. 8 unit tests added. |
8e7b0ad | Dedup formatTime from 4 mobile sites into web/src/mobile/lib/formatTime.ts (mobile uses toLocaleTimeString, distinct from desktop UTC version) |
dcdbec7 | Replace window.confirm() with inline two-step in BlockEditSheet + EntryEditSheet |
7da8153 | StandupStreamSheet render-time setState → useEffect for summary sync |
2c5960c | Type __setPlanQuickAdd via declare global + memoize MobilePlan status-only collections |
ee1652e | Stabilize usePullToRefresh effect deps + MobileHeader duplicate-h1 a11y fix |
Mobile UI Redesign (4 commits)
Brand alignment to copper/warm-black — see levandor-brand for tokens.
| SHA | Item |
|---|---|
7f4bc84 | Drawer surface tokens (bg-white + warm scrim) + explicit input/select text colors (the invisible-dropdown-text bug) |
2a43df8 | Unified hero/card/list visual treatment |
60f7fa6 | Refined header, bottom tabs, sheet internals (BottomTabs got 2px-tall active-pill indicator) |
31936a5 | Polish: WeekStrip, StatusFilterPills, settings list (Sign out → rose-600) |
Bug Fixes + Brand Pass (2 commits)
| SHA | Item |
|---|---|
2c82bd0 | Hours card respects selected day (“Today” only when active === today) + wire CurrencyProvider for HUF support (replacing hardcoded EUR Intl.NumberFormat) |
9448637 | Levandor copper brand pass — discovered actual brand in packages/ui/src/styles/globals.css. See levandor-brand. |
Forecast Hero Iterations (2 commits)
See dashboard-forecast for the full computation gotcha.
| SHA | Item |
|---|---|
1697360 | First fix: netThisMonth / netForecast (was apples-to-oranges weekly/monthly) |
6f1fdb5 | Final fix: mirror desktop dashboard hero exactly — no invented ratios |
Day Rituals Rebuild (2 commits)
See mobile-day-rituals and evening-checklist for full descriptions.
| SHA | Item |
|---|---|
b7626d9 | Replaced StandupStreamSheet with 4 phone-native sheets (Morning, Standup notes, Lunch, Evening reflection) |
482e592 | Evening Checklist v1 (manifestation tracker) — both mobile sheet + desktop card |
Evening Checklist Backend (2 commits)
| SHA | Item |
|---|---|
3c3501d | Migration 008 — widened CHECK constraint, seeded 3 prompts. See evening-checklist. |
2849661 | Migration 009 — added 4th “Shit I overspent on” prompt |
Debounced Autosave (2 commits)
See mobile-autosave for full description.
| SHA | Item |
|---|---|
f718425 | New useDebouncedAutosave hook + EveningChecklistSheet/Card refactor |
7d18acb | MorningCheckinSheet + EveningReflectionSheet summary + desktop standup page parent-level orchestration |
Locked Decisions (Brainstorm Phase)
Decision Log
Settled choices from the brainstorm. Don’t relitigate without reason.
| # | Topic | Decision |
|---|---|---|
| 1 | Native depth | Installable PWA — same React codebase, mobile-first; Add-to-Home-Screen, fullscreen, splash, theme color, smooth gestures, offline shell. No Capacitor. No native rewrite. |
| 2 | Hot surfaces | Day plan (/standup), Dashboard (/), Timesheets (/timesheets), Money (Invoices + Budget). Tasks/Kanban explicitly NOT a hot surface. |
| 3 | Target device | iPhone only. iOS Safari focus. iOS conventions: safe-area-inset, status-bar tint, large-title pattern. |
| 4 | Offline scope | Shell only. Service worker precaches build assets; no data caching, no mutation queue. |
| 5 | Visual style | Levandor-branded mobile (copper/warm-black). Layout/gestures iOS-native; visual identity stays Levandor. See levandor-brand. |
| 6 | Cold-start screen | Dashboard. Home tab is the first tab and the default route. |
| 7 | Bottom tabs (5) | Home · Plan · Hours · Money · More |
| 8 | Day-planner shape | Now-focused hybrid. Hero “Now” card → Up Next → Later today → collapsed Done today. |
| 9 | Timesheet shape | Day-first. Week strip on top, tap a day to switch, totals card, project rows. |
| 10 | Strategy | Mobile-first rebuild of hot surfaces. New components in web/src/mobile/. |
Spec & Implementation Plan
- Spec —
docs/superpowers/specs/2026-05-09-mobile-native-feel-design.md(post-pre-mortem-patched) - Pre-mortem (4-judge council, WARN×4, 46 findings) folded into spec body
- Plan executed end-to-end through Phase 10 + multiple audit/polish/brand passes
Related
- levandor-brand — Color tokens + typography
- mobile-day-rituals — Phone-native ritual sheets
- evening-checklist — Manifestation tracker v1
- mobile-autosave — Debounced autosave pattern
- dashboard-forecast — Forecast computation gotcha
- day-planner — Desktop day planner (current implementation)
- debugging-log-crm — Hours UTC bug, brand bugs
- tech-debt — Mobile-related tech debt entries
- budget — Backing data for the Money tab
- security — CF Access auth (carries over to mobile)
- levandor-crm — Project overview