From 29ff7fd8fd259344160bfe5c9aaa9b3d368c2864 Mon Sep 17 00:00:00 2001 From: lorentz Date: Sun, 3 May 2026 14:48:41 -0400 Subject: [PATCH] docs(02): create phase plan --- .planning/ROADMAP.md | 4 +- .../02-mobile-shell-more-drawer/02-01-PLAN.md | 754 ++++++++++++++++++ .../02-mobile-shell-more-drawer/02-02-PLAN.md | 406 ++++++++++ 3 files changed, 1163 insertions(+), 1 deletion(-) create mode 100644 .planning/phases/02-mobile-shell-more-drawer/02-01-PLAN.md create mode 100644 .planning/phases/02-mobile-shell-more-drawer/02-02-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 24c188d..dda8edf 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -57,7 +57,9 @@ Decimal phases appear between their surrounding integers in numeric order. 4. Tapping Sign out in the drawer signs the user out and lands them on `/auth/sign-in` 5. `app/mobile/nav/page.tsx` no longer exists; visiting `/mobile/nav` does not render the old standalone nav page 6. Page content scrolls under the sticky header and is not hidden behind the bottom nav (bottom padding accounts for nav height + safe-area inset) -**Plans**: TBD +**Plans**: 2 plans +- [ ] 02-01-PLAN.md — Build mobile shell components (HeaderBar, BottomNav, MoreDrawer) + analyzer placeholder (SHELL-02..04, SHELL-06, NAV-01..03, DRAWER-01..05) +- [ ] 02-02-PLAN.md — Wire new components into app/mobile/layout.tsx, delete app/mobile/nav/page.tsx (SHELL-01, SHELL-05, DRAWER-06) **UI hint**: yes ### Phase 3: Dashboard Restyle diff --git a/.planning/phases/02-mobile-shell-more-drawer/02-01-PLAN.md b/.planning/phases/02-mobile-shell-more-drawer/02-01-PLAN.md new file mode 100644 index 0000000..618a6c6 --- /dev/null +++ b/.planning/phases/02-mobile-shell-more-drawer/02-01-PLAN.md @@ -0,0 +1,754 @@ +--- +phase: 02-mobile-shell-more-drawer +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - components/mobile/HeaderBar.tsx + - components/mobile/BottomNav.tsx + - components/mobile/MoreDrawer.tsx + - app/mobile/analyzer/page.tsx +autonomous: true +requirements: + - SHELL-02 + - SHELL-03 + - SHELL-04 + - SHELL-06 + - NAV-01 + - NAV-02 + - NAV-03 + - DRAWER-01 + - DRAWER-02 + - DRAWER-03 + - DRAWER-04 + - DRAWER-05 + +must_haves: + truths: + - "components/mobile/HeaderBar.tsx exists and exports a HeaderBar component that renders the WulfMark + 'Pulse' wordmark linking to /mobile/dashboard, a Bell button (aria-label='Notifications', empty onClick), and an avatar-circle button that triggers the MoreDrawer" + - "components/mobile/BottomNav.tsx exists and exports a BottomNav component with 5 cells: 4 routed tabs (Dashboard, Tickets, Finance, Analyzer) and a 5th 'More' button that opens the drawer" + - "components/mobile/MoreDrawer.tsx exists and exports a MoreDrawer component built on shadcn Sheet (side='right') with three sections: Mobile sections (Engagement), Full site (Quotes, Configuration Items, Backup Status, Ticket Digest, Admin/Sync each with ExternalLink icon), Account (current user read-only + Sign out)" + - "MoreDrawer's open/close state is controlled via props (open, onOpenChange) so two triggers (header avatar + bottom-nav More button) can share one drawer" + - "BottomNav's active-tab detection uses pathname.startsWith(href) so /mobile/tickets/123 highlights the Tickets tab" + - "MoreDrawer Sign out button calls signOut() then router.push('/auth/sign-in')" + - "app/mobile/analyzer/page.tsx exists as a minimal placeholder so the bottom-nav Analyzer tab does not 404 before Phase 6" + - "TypeScript compiles (npx tsc --noEmit) and Next.js builds (npm run build) successfully" + artifacts: + - path: "components/mobile/HeaderBar.tsx" + provides: "Sticky header — WulfMark+wordmark link, Bell placeholder, avatar trigger for drawer" + contains: "export function HeaderBar" + - path: "components/mobile/BottomNav.tsx" + provides: "Fixed bottom tab bar — 4 tabs + More button" + contains: "export function BottomNav" + - path: "components/mobile/MoreDrawer.tsx" + provides: "shadcn Sheet drawer with three sections" + contains: "export function MoreDrawer" + - path: "app/mobile/analyzer/page.tsx" + provides: "Placeholder route so the new Analyzer tab resolves until Phase 6 ships" + contains: "export default function" + key_links: + - from: "components/mobile/HeaderBar.tsx" + to: "components/branding/wulf-mark.tsx" + via: "WulfMark import (variant='mark' and variant='wordmark')" + pattern: "from ['\"]@/components/branding/wulf-mark['\"]" + - from: "components/mobile/MoreDrawer.tsx" + to: "components/ui/sheet.tsx" + via: "Sheet, SheetContent, SheetTrigger imports" + pattern: "from ['\"]@/components/ui/sheet['\"]" + - from: "components/mobile/MoreDrawer.tsx" + to: "lib/auth-client.ts" + via: "signOut + useSession imports" + pattern: "from ['\"]@/lib/auth-client['\"]" + - from: "components/mobile/BottomNav.tsx" + to: "/mobile/analyzer" + via: "Analyzer tab href" + pattern: "/mobile/analyzer" + - from: "components/mobile/BottomNav.tsx" + to: "MoreDrawer trigger" + via: "onMoreClick prop or onOpenChange invocation" + pattern: "onMoreClick|onOpenChange" +--- + + +Build the three new shell components (HeaderBar, BottomNav, MoreDrawer) and a minimal `/mobile/analyzer` placeholder page, all under `components/mobile/*` and `app/mobile/analyzer/page.tsx`. None of these files are imported by the current shell, so this plan adds files only — the existing `app/mobile/layout.tsx` and `app/mobile/nav/page.tsx` keep working until Plan 02 wires the new pieces in. + +Purpose: Lay down the three reusable shell pieces with literal JSX, controlled drawer state, and route entries so Plan 02 can replace `layout.tsx` in a single small change. + +Output: 4 new files. Build still passes. Existing `/mobile` routes unchanged in behavior. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/ROADMAP.md +@.planning/REQUIREMENTS.md +@.planning/phases/02-mobile-shell-more-drawer/02-CONTEXT.md +@docs/superpowers/specs/2026-05-03-mobile-shell-design.md +@CLAUDE.md +@DESIGN.md +@app/mobile/layout.tsx +@app/mobile/nav/page.tsx +@app/styles/brand.css +@components/ui/sheet.tsx +@components/ui/button.tsx +@components/branding/wulf-mark.tsx +@components/navigation/user-menu.tsx +@lib/auth-client.ts + + + + +From components/branding/wulf-mark.tsx: +```typescript +export function WulfMark(props: { + variant?: 'mark' | 'wordmark'; + className?: string; + alt?: string; + priority?: boolean; +}): JSX.Element; +``` + +From lib/auth-client.ts (Better Auth client): +```typescript +export const signIn, signOut, useSession, getSession; +// useSession() returns { data: session | null, ... } +// session.user has: { name?: string, email?: string, role?: string, image?: string | null } +``` +The `UserMenu` component (components/navigation/user-menu.tsx) shows the canonical pattern: +```ts +const initials = (user.name ?? user.email ?? '?') + .split(/[\s@]/) + .filter(Boolean) + .slice(0, 2) + .map((p) => p[0]?.toUpperCase()) + .join(''); +async function handleSignOut() { + await signOut(); + router.push('/auth/sign-in'); +} +``` +**Reuse this pattern. Do NOT add the shadcn `avatar` primitive — it is not present in `components/ui/` and we do not need it; the initials-circle pattern matches the existing `UserMenu`.** + +From components/ui/sheet.tsx: +```typescript +export function Sheet(props: { open?: boolean; onOpenChange?: (open: boolean) => void; children: ReactNode }); +export function SheetTrigger(props: { asChild?: boolean; children: ReactNode }); +export function SheetContent(props: { side?: "top" | "right" | "bottom" | "left"; className?: string; showCloseButton?: boolean; children: ReactNode }); +export function SheetHeader(props: { className?: string; children: ReactNode }); +export function SheetTitle(props: { className?: string; children: ReactNode }); +export function SheetDescription(props: { className?: string; children: ReactNode }); +export function SheetClose(props: { asChild?: boolean; children: ReactNode }); +``` +A SheetContent **must** contain a SheetTitle (Radix accessibility requirement) — wrap headings in SheetHeader → SheetTitle. Use `SheetDescription` (or visually-hidden description) if needed. + +From components/ui/button.tsx: +```typescript +export function Button(props: ButtonHTMLAttributes & { + variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'; + size?: 'default' | 'sm' | 'lg' | 'icon' | 'icon-sm' | 'icon-lg'; + asChild?: boolean; +}); +``` + +CSS utilities available in `app/styles/brand.css` (already imported by globals.css): +- `pt-safe` → `padding-top: env(safe-area-inset-top)` +- `pb-safe` → `padding-bottom: env(safe-area-inset-bottom)` + + + +This plan **does not** touch: +- `app/mobile/layout.tsx` (Plan 02 rewrites it) +- `app/mobile/nav/page.tsx` (Plan 02 deletes it) +- Anything in `app/mobile/dashboard/*`, `app/mobile/tickets/*`, `app/mobile/finance/*` (out of phase) +- The shadcn `avatar` primitive — do NOT add it; we use the existing initials-circle pattern from `UserMenu`. +- `components/navigation/app-navigation.tsx` — desktop nav, untouched. + + + + + + + Task 1: Create components/mobile/MoreDrawer.tsx (Sheet drawer with 3 sections + Sign out) + components/mobile/MoreDrawer.tsx + + - components/ui/sheet.tsx (Sheet/SheetContent/SheetTitle/SheetClose API and side="right" behavior) + - lib/auth-client.ts (verify `signOut` and `useSession` are exported) + - components/navigation/user-menu.tsx (reference for initials pattern + signOut handler) + - app/mobile/nav/page.tsx (reference for the existing DESKTOP_LINKS list to migrate) + - .planning/phases/02-mobile-shell-more-drawer/02-CONTEXT.md (locked: side="right", three sections, Sign out flow) + + +Create the file `components/mobile/MoreDrawer.tsx` with literal contents below (controlled `open`/`onOpenChange` so the same drawer can be triggered from the header avatar AND the bottom-nav More button): + +```tsx +'use client'; + +/* MoreDrawer — phase 02 (DRAWER-01..05). + * + * shadcn Sheet (side="right") with three top-to-bottom sections: + * 1. Mobile sections — Engagement (in-shell route, no ExternalLink hint) + * 2. Full site — desktop-only routes, each with ExternalLink hint + * 3. Account — current user (read-only) + Sign out + * + * Open state is controlled by the parent so the header avatar AND the + * bottom-nav More cell can both trigger this single drawer. */ + +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; +import { + ExternalLink, + FileText, + Server, + HardDrive, + BarChart3, + Settings, + Users, + LogOut, +} from 'lucide-react'; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetDescription, + SheetClose, +} from '@/components/ui/sheet'; +import { useSession, signOut } from '@/lib/auth-client'; +import { toast } from 'sonner'; + +const MOBILE_SECTIONS = [ + { href: '/mobile/engagement', label: 'Engagement', icon: Users }, +]; + +const DESKTOP_LINKS = [ + { href: '/quotes', label: 'Quotes', icon: FileText }, + { href: '/configuration-items', label: 'Configuration Items', icon: Server }, + { href: '/backup-status', label: 'Backup Status', icon: HardDrive }, + { href: '/admin/ticket-digest', label: 'Ticket Digest', icon: BarChart3 }, + { href: '/admin/sync', label: 'Admin / Sync', icon: Settings }, +]; + +interface MoreDrawerProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export function MoreDrawer({ open, onOpenChange }: MoreDrawerProps) { + const router = useRouter(); + const { data: session } = useSession(); + const user = session?.user as + | { name?: string; email?: string; image?: string | null } + | undefined; + + const initials = (user?.name ?? user?.email ?? '?') + .split(/[\s@]/) + .filter(Boolean) + .slice(0, 2) + .map((p) => p[0]?.toUpperCase()) + .join(''); + + async function handleSignOut() { + try { + await signOut(); + router.push('/auth/sign-in'); + } catch (e) { + toast.error('Sign out failed'); + console.error('Sign out failed:', e); + } + } + + return ( + + + + Menu + + Navigation, full-site links, and account actions. + + + + {/* Section 1: Mobile sections (DRAWER-03) */} +
+

+ Mobile sections +

+
+ {MOBILE_SECTIONS.map(({ href, label, icon: Icon }) => ( + + + + {label} + + + ))} +
+
+ + {/* Section 2: Full site (DRAWER-04) */} +
+

+ Full site +

+
+ {DESKTOP_LINKS.map(({ href, label, icon: Icon }) => ( + + + + {label} + + + + ))} +
+
+ + {/* Section 3: Account (DRAWER-05) */} +
+

+ Account +

+
+ {user && ( +
+ + {initials} + +
+ {user.name && ( +

+ {user.name} +

+ )} + {user.email && ( +

+ {user.email} +

+ )} +
+
+ )} + +
+
+
+
+ ); +} +``` + +Notes: +- `side="right"` — locked decision (CONTEXT.md, DRAWER-02). +- Engagement intentionally has no `ExternalLink` icon (it's an in-shell route per DRAWER-03). +- Quotes/Configuration Items/Backup Status/Ticket Digest/Admin/Sync each carry `ExternalLink` (DRAWER-04). Do NOT include Engagement in the desktop list (it migrated to "Mobile sections"). +- `SheetClose asChild` wraps each link so tapping a row closes the drawer (better UX; Radix Sheet pattern). +- `pb-safe` on Section 3 keeps the Sign out row clear of the home indicator on iOS. +- `` satisfies Radix's a11y requirement when the description is non-visual. +
+ + test -f components/mobile/MoreDrawer.tsx && grep -q "side=\"right\"" components/mobile/MoreDrawer.tsx && grep -q "signOut()" components/mobile/MoreDrawer.tsx && grep -q "/auth/sign-in" components/mobile/MoreDrawer.tsx && grep -q "Mobile sections" components/mobile/MoreDrawer.tsx && grep -q "Full site" components/mobile/MoreDrawer.tsx && grep -q "Account" components/mobile/MoreDrawer.tsx + + + - `test -f components/mobile/MoreDrawer.tsx` exits 0 + - `grep -E "export function MoreDrawer" components/mobile/MoreDrawer.tsx` matches + - `grep -E "side=\"right\"" components/mobile/MoreDrawer.tsx` matches (DRAWER-02) + - `grep -E "/mobile/engagement" components/mobile/MoreDrawer.tsx` matches (DRAWER-03) + - `grep -E "/quotes" components/mobile/MoreDrawer.tsx` matches AND `grep -E "/configuration-items" components/mobile/MoreDrawer.tsx` matches AND `grep -E "/backup-status" components/mobile/MoreDrawer.tsx` matches AND `grep -E "/admin/ticket-digest" components/mobile/MoreDrawer.tsx` matches AND `grep -E "/admin/sync" components/mobile/MoreDrawer.tsx` matches (DRAWER-04) + - `grep -E "ExternalLink" components/mobile/MoreDrawer.tsx` matches (DRAWER-04 hint) + - `grep -E "signOut\(\)" components/mobile/MoreDrawer.tsx` matches AND `grep -E "/auth/sign-in" components/mobile/MoreDrawer.tsx` matches (DRAWER-05) + - `grep -E "open: boolean" components/mobile/MoreDrawer.tsx` matches AND `grep -E "onOpenChange" components/mobile/MoreDrawer.tsx` matches (controlled drawer) + + The drawer file exists, exports `MoreDrawer({ open, onOpenChange })`, contains all three sections with correct routes, calls `signOut()` then `router.push('/auth/sign-in')`, and uses `side="right"`. +
+ + + Task 2: Create components/mobile/HeaderBar.tsx (sticky top header — brand, Bell, avatar) + components/mobile/HeaderBar.tsx + + - components/branding/wulf-mark.tsx (WulfMark prop signature) + - components/navigation/user-menu.tsx (initials pattern reference) + - app/styles/brand.css (confirm `pt-safe` utility exists) + - components/ui/button.tsx (Button variant/size API) + - .planning/phases/02-mobile-shell-more-drawer/02-CONTEXT.md (header decisions: SHELL-02..04, no page title) + + +Create the file `components/mobile/HeaderBar.tsx` with literal contents below. The header takes `onAvatarClick` so the parent layout can wire it to the same drawer state used by `BottomNav`. + +```tsx +'use client'; + +/* HeaderBar — phase 02 (SHELL-02..04). + * + * Sticky top bar inside the /mobile shell. Three slots: + * left: WulfMark + "Pulse" wordmark, linked to /mobile/dashboard + * right: Bell icon button (placeholder, aria-label="Notifications") + * right: compact avatar circle — opens the More drawer (parent owns state) + * + * No page title in the header — pages render their own H1. */ + +import Link from 'next/link'; +import { Bell } from 'lucide-react'; +import { WulfMark } from '@/components/branding/wulf-mark'; +import { useSession } from '@/lib/auth-client'; + +interface HeaderBarProps { + onAvatarClick: () => void; +} + +export function HeaderBar({ onAvatarClick }: HeaderBarProps) { + const { data: session } = useSession(); + const user = session?.user as + | { name?: string; email?: string } + | undefined; + + const initials = (user?.name ?? user?.email ?? '?') + .split(/[\s@]/) + .filter(Boolean) + .slice(0, 2) + .map((p) => p[0]?.toUpperCase()) + .join(''); + + return ( +
+
+ {/* Left: brand mark + wordmark, linked to /mobile/dashboard */} + + + Pulse + + + {/* Right: Bell placeholder, then avatar trigger */} +
+ + + +
+
+
+ ); +} +``` + +Notes: +- `pt-safe` is added on the sticky header so the notch/dynamic-island doesn't overlap content (SHELL-02 + Phase 1 PWA-04). +- `bg-background/95 backdrop-blur` matches CONTEXT.md SHELL-02. +- Bell `onClick` is intentionally empty (SHELL-03 placeholder); future phase wires real notifications. +- Avatar is `h-7 w-7` per SHELL-04 — wrapped in a `h-9 w-9` button to give a 36px touch target. +- No `

` / no page title in header (SHELL-02 explicit). +- We do NOT use the shadcn avatar primitive — initials-circle pattern matches existing `UserMenu`. + + + test -f components/mobile/HeaderBar.tsx && grep -q "sticky top-0" components/mobile/HeaderBar.tsx && grep -q "bg-background/95 backdrop-blur" components/mobile/HeaderBar.tsx && grep -q "/mobile/dashboard" components/mobile/HeaderBar.tsx && grep -q 'aria-label="Notifications"' components/mobile/HeaderBar.tsx && grep -q "WulfMark" components/mobile/HeaderBar.tsx && grep -q "pt-safe" components/mobile/HeaderBar.tsx && grep -q "h-7 w-7" components/mobile/HeaderBar.tsx + + + - `test -f components/mobile/HeaderBar.tsx` exits 0 + - `grep -E "export function HeaderBar" components/mobile/HeaderBar.tsx` matches + - `grep -E "sticky top-0" components/mobile/HeaderBar.tsx` matches AND `grep -E "bg-background/95 backdrop-blur" components/mobile/HeaderBar.tsx` matches AND `grep -E "border-b" components/mobile/HeaderBar.tsx` matches (SHELL-02) + - `grep -E "/mobile/dashboard" components/mobile/HeaderBar.tsx` matches (brand link target, SHELL-02) + - `grep -E "WulfMark" components/mobile/HeaderBar.tsx` matches AND `grep -E "Pulse" components/mobile/HeaderBar.tsx` matches (mark + wordmark, SHELL-02) + - `grep -E "aria-label=\"Notifications\"" components/mobile/HeaderBar.tsx` matches AND `grep -E "Bell" components/mobile/HeaderBar.tsx` matches (SHELL-03) + - `grep -E "h-7 w-7" components/mobile/HeaderBar.tsx` matches (compact avatar, SHELL-04) + - `grep -E "onAvatarClick" components/mobile/HeaderBar.tsx` matches (avatar opens drawer via parent state, SHELL-04) + - `grep -E "pt-safe" components/mobile/HeaderBar.tsx` matches (PWA-04 reuse / safe-area) + - `! grep -E " + HeaderBar renders WulfMark+wordmark linked to /mobile/dashboard, a Bell button with `aria-label="Notifications"` and empty onClick, and an avatar-circle button that calls `onAvatarClick` (parent wires this to the drawer state). + + + + Task 3: Create components/mobile/BottomNav.tsx (5-cell bottom bar — 4 tabs + More) + components/mobile/BottomNav.tsx + + - app/mobile/layout.tsx (current 3-tab pattern; we extend to 4 tabs + More) + - app/styles/brand.css (confirm `pb-safe` utility exists) + - .planning/phases/02-mobile-shell-more-drawer/02-CONTEXT.md (locked: SHELL-06, NAV-01..03 — Dashboard/Tickets/Finance/Analyzer + More) + + +Create the file `components/mobile/BottomNav.tsx`: + +```tsx +'use client'; + +/* BottomNav — phase 02 (SHELL-06, NAV-01..03, DRAWER-01). + * + * Fixed bottom bar with five cells: + * - Dashboard (LayoutDashboard) -> /mobile/dashboard + * - Tickets (Ticket) -> /mobile/tickets + * - Finance (DollarSign) -> /mobile/finance + * - Analyzer (Sparkles) -> /mobile/analyzer + * - More (Menu) -> opens the MoreDrawer (parent state) + * + * Active tab detected via pathname.startsWith(href). Active = text-primary, + * inactive = text-muted-foreground. */ + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import { + LayoutDashboard, + Ticket, + DollarSign, + Sparkles, + Menu, +} from 'lucide-react'; + +const TABS = [ + { href: '/mobile/dashboard', label: 'Dashboard', icon: LayoutDashboard }, + { href: '/mobile/tickets', label: 'Tickets', icon: Ticket }, + { href: '/mobile/finance', label: 'Finance', icon: DollarSign }, + { href: '/mobile/analyzer', label: 'Analyzer', icon: Sparkles }, +] as const; + +interface BottomNavProps { + onMoreClick: () => void; +} + +export function BottomNav({ onMoreClick }: BottomNavProps) { + const pathname = usePathname(); + + return ( + + ); +} +``` + +Notes: +- `max-w-lg mx-auto` keeps the nav width-aligned with the content gutter (SHELL-06 + CONTEXT.md). +- `pb-safe` on the outer `