docs(02): create phase plan
This commit is contained in:
parent
e67c51ec7c
commit
29ff7fd8fd
3 changed files with 1163 additions and 1 deletions
|
|
@ -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
|
||||
|
|
|
|||
754
.planning/phases/02-mobile-shell-more-drawer/02-01-PLAN.md
Normal file
754
.planning/phases/02-mobile-shell-more-drawer/02-01-PLAN.md
Normal file
|
|
@ -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"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.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
|
||||
|
||||
<interfaces>
|
||||
<!-- Key types and exports the executor will use. Extracted so executor does not need to re-grep the codebase. -->
|
||||
|
||||
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)`
|
||||
</interfaces>
|
||||
|
||||
<scope_boundary>
|
||||
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.
|
||||
</scope_boundary>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Create components/mobile/MoreDrawer.tsx (Sheet drawer with 3 sections + Sign out)</name>
|
||||
<files>components/mobile/MoreDrawer.tsx</files>
|
||||
<read_first>
|
||||
- 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)
|
||||
</read_first>
|
||||
<action>
|
||||
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 (
|
||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||
<SheetContent side="right" className="w-80 sm:max-w-sm flex flex-col">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Menu</SheetTitle>
|
||||
<SheetDescription className="sr-only">
|
||||
Navigation, full-site links, and account actions.
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
{/* Section 1: Mobile sections (DRAWER-03) */}
|
||||
<div className="px-4">
|
||||
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2">
|
||||
Mobile sections
|
||||
</p>
|
||||
<div className="rounded-2xl border divide-y overflow-hidden">
|
||||
{MOBILE_SECTIONS.map(({ href, label, icon: Icon }) => (
|
||||
<SheetClose asChild key={href}>
|
||||
<Link
|
||||
href={href}
|
||||
className="flex items-center gap-3 px-4 py-3 hover:bg-accent transition-colors"
|
||||
>
|
||||
<Icon className="w-4 h-4 text-muted-foreground shrink-0" />
|
||||
<span className="text-sm flex-1">{label}</span>
|
||||
</Link>
|
||||
</SheetClose>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 2: Full site (DRAWER-04) */}
|
||||
<div className="px-4">
|
||||
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2">
|
||||
Full site
|
||||
</p>
|
||||
<div className="rounded-2xl border divide-y overflow-hidden">
|
||||
{DESKTOP_LINKS.map(({ href, label, icon: Icon }) => (
|
||||
<SheetClose asChild key={href}>
|
||||
<Link
|
||||
href={href}
|
||||
className="flex items-center gap-3 px-4 py-3 hover:bg-accent transition-colors"
|
||||
>
|
||||
<Icon className="w-4 h-4 text-muted-foreground shrink-0" />
|
||||
<span className="text-sm flex-1">{label}</span>
|
||||
<ExternalLink className="w-3.5 h-3.5 text-muted-foreground shrink-0" />
|
||||
</Link>
|
||||
</SheetClose>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 3: Account (DRAWER-05) */}
|
||||
<div className="px-4 mt-auto pb-safe">
|
||||
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2">
|
||||
Account
|
||||
</p>
|
||||
<div className="rounded-2xl border overflow-hidden">
|
||||
{user && (
|
||||
<div className="flex items-center gap-3 px-4 py-3 border-b">
|
||||
<span className="inline-flex h-8 w-8 items-center justify-center rounded-full bg-primary/15 text-primary text-xs font-semibold shrink-0">
|
||||
{initials}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
{user.name && (
|
||||
<p className="text-sm font-medium leading-tight truncate">
|
||||
{user.name}
|
||||
</p>
|
||||
)}
|
||||
{user.email && (
|
||||
<p className="text-xs text-muted-foreground truncate" title={user.email}>
|
||||
{user.email}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSignOut}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 text-destructive hover:bg-destructive/10 transition-colors"
|
||||
>
|
||||
<LogOut className="w-4 h-4 shrink-0" />
|
||||
<span className="text-sm">Sign out</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
- `<SheetDescription className="sr-only">` satisfies Radix's a11y requirement when the description is non-visual.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>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</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `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)
|
||||
</acceptance_criteria>
|
||||
<done>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"`.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Create components/mobile/HeaderBar.tsx (sticky top header — brand, Bell, avatar)</name>
|
||||
<files>components/mobile/HeaderBar.tsx</files>
|
||||
<read_first>
|
||||
- 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)
|
||||
</read_first>
|
||||
<action>
|
||||
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 (
|
||||
<header className="sticky top-0 z-30 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 border-b pt-safe">
|
||||
<div className="flex items-center justify-between px-4 h-14">
|
||||
{/* Left: brand mark + wordmark, linked to /mobile/dashboard */}
|
||||
<Link
|
||||
href="/mobile/dashboard"
|
||||
className="flex items-center gap-2 -ml-1 px-1 rounded-md hover:bg-accent/50 transition-colors"
|
||||
aria-label="Pulse — go to Dashboard"
|
||||
>
|
||||
<WulfMark variant="mark" className="h-6 w-auto" />
|
||||
<span className="font-bold text-base tracking-tight">Pulse</span>
|
||||
</Link>
|
||||
|
||||
{/* Right: Bell placeholder, then avatar trigger */}
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { /* SHELL-03: placeholder — no menu, no badge */ }}
|
||||
aria-label="Notifications"
|
||||
className="inline-flex items-center justify-center h-9 w-9 rounded-md hover:bg-accent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
<Bell className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={onAvatarClick}
|
||||
aria-label="Open menu"
|
||||
className="inline-flex items-center justify-center h-9 w-9 rounded-md hover:bg-accent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
<span className="inline-flex h-7 w-7 items-center justify-center rounded-full bg-primary/15 text-primary text-[11px] font-semibold">
|
||||
{initials}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
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 `<h1>` / no page title in header (SHELL-02 explicit).
|
||||
- We do NOT use the shadcn avatar primitive — initials-circle pattern matches existing `UserMenu`.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>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</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `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 "<h1" components/mobile/HeaderBar.tsx` exits 0 (no page title in header, SHELL-02 explicit)
|
||||
</acceptance_criteria>
|
||||
<done>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).</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Create components/mobile/BottomNav.tsx (5-cell bottom bar — 4 tabs + More)</name>
|
||||
<files>components/mobile/BottomNav.tsx</files>
|
||||
<read_first>
|
||||
- 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)
|
||||
</read_first>
|
||||
<action>
|
||||
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 (
|
||||
<nav
|
||||
aria-label="Primary"
|
||||
className="fixed bottom-0 left-0 right-0 z-30 border-t bg-background pb-safe"
|
||||
>
|
||||
<div className="max-w-lg mx-auto flex h-16">
|
||||
{TABS.map(({ href, label, icon: Icon }) => {
|
||||
const active = pathname?.startsWith(href) ?? false;
|
||||
return (
|
||||
<Link
|
||||
key={href}
|
||||
href={href}
|
||||
aria-current={active ? 'page' : undefined}
|
||||
className={`flex-1 flex flex-col items-center justify-center gap-0.5 text-[11px] transition-colors ${
|
||||
active
|
||||
? 'text-primary'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-5 h-5" aria-hidden="true" />
|
||||
<span>{label}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onMoreClick}
|
||||
aria-label="Open menu"
|
||||
className="flex-1 flex flex-col items-center justify-center gap-0.5 text-[11px] text-muted-foreground hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
<Menu className="w-5 h-5" aria-hidden="true" />
|
||||
<span>More</span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
- `max-w-lg mx-auto` keeps the nav width-aligned with the content gutter (SHELL-06 + CONTEXT.md).
|
||||
- `pb-safe` on the outer `<nav>` so the home indicator inset is reserved (PWA-04 reuse).
|
||||
- `h-16` = 64px nav height; the layout's `<main>` will pad by 64 + safe-area to keep content above the bar (handled in Plan 02).
|
||||
- Active detection is `pathname?.startsWith(href)` (NAV-03).
|
||||
- The More cell is a `<button>`, not a Link — it triggers a controlled drawer via `onMoreClick`.
|
||||
- Icons use `aria-hidden="true"` because the visible label already names the destination.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>test -f components/mobile/BottomNav.tsx && grep -q "/mobile/dashboard" components/mobile/BottomNav.tsx && grep -q "/mobile/tickets" components/mobile/BottomNav.tsx && grep -q "/mobile/finance" components/mobile/BottomNav.tsx && grep -q "/mobile/analyzer" components/mobile/BottomNav.tsx && grep -q "max-w-lg mx-auto" components/mobile/BottomNav.tsx && grep -q "pathname.*startsWith" components/mobile/BottomNav.tsx && grep -q "text-primary" components/mobile/BottomNav.tsx && grep -q "pb-safe" components/mobile/BottomNav.tsx</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `test -f components/mobile/BottomNav.tsx` exits 0
|
||||
- `grep -E "export function BottomNav" components/mobile/BottomNav.tsx` matches
|
||||
- `grep -E "/mobile/dashboard" components/mobile/BottomNav.tsx` matches AND `grep -E "/mobile/tickets" components/mobile/BottomNav.tsx` matches AND `grep -E "/mobile/finance" components/mobile/BottomNav.tsx` matches AND `grep -E "/mobile/analyzer" components/mobile/BottomNav.tsx` matches (NAV-02)
|
||||
- `grep -E "LayoutDashboard" components/mobile/BottomNav.tsx` matches AND `grep -E "\\bTicket\\b" components/mobile/BottomNav.tsx` matches AND `grep -E "DollarSign" components/mobile/BottomNav.tsx` matches AND `grep -E "Sparkles" components/mobile/BottomNav.tsx` matches AND `grep -E "\\bMenu\\b" components/mobile/BottomNav.tsx` matches (NAV-01 + DRAWER-01 icons)
|
||||
- `grep -E "pathname.*startsWith" components/mobile/BottomNav.tsx` matches (NAV-03)
|
||||
- `grep -E "text-primary" components/mobile/BottomNav.tsx` matches AND `grep -E "text-muted-foreground" components/mobile/BottomNav.tsx` matches (NAV-03 active/inactive)
|
||||
- `grep -E "fixed bottom-0" components/mobile/BottomNav.tsx` matches AND `grep -E "border-t" components/mobile/BottomNav.tsx` matches AND `grep -E "max-w-lg mx-auto" components/mobile/BottomNav.tsx` matches (SHELL-06)
|
||||
- `grep -E "pb-safe" components/mobile/BottomNav.tsx` matches (safe-area for home indicator)
|
||||
- `grep -E "onMoreClick" components/mobile/BottomNav.tsx` matches (DRAWER-01 trigger via parent state)
|
||||
</acceptance_criteria>
|
||||
<done>BottomNav exports a 5-cell nav: 4 routed Links (Dashboard, Tickets, Finance, Analyzer) with active-state via `pathname.startsWith(href)`, plus a More button that calls `onMoreClick`.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 4: Create app/mobile/analyzer/page.tsx (placeholder so Analyzer tab does not 404 before Phase 6)</name>
|
||||
<files>app/mobile/analyzer/page.tsx</files>
|
||||
<read_first>
|
||||
- .planning/phases/02-mobile-shell-more-drawer/02-CONTEXT.md (decisions §Routes & files: "Add a placeholder app/mobile/analyzer/page.tsx so the new bottom-nav Analyzer tab doesn't 404 before Phase 6 lands. Minimal 'coming soon' component is sufficient.")
|
||||
- app/mobile/page.tsx (style reference for a minimal mobile page)
|
||||
</read_first>
|
||||
<action>
|
||||
Create `app/mobile/analyzer/page.tsx`:
|
||||
|
||||
```tsx
|
||||
/* Placeholder for /mobile/analyzer.
|
||||
*
|
||||
* Phase 02 only adds the Analyzer tab to the bottom nav — the real feed
|
||||
* lands in Phase 6 (`docs/superpowers/specs/2026-05-03-mobile-shell-design.md`
|
||||
* §6.4). This file exists so tapping the Analyzer tab resolves to a real
|
||||
* route instead of 404. Phase 6 will replace this file with the actual
|
||||
* read-only feed page.
|
||||
*
|
||||
* DO NOT add features, data fetching, or UI beyond the "Coming soon"
|
||||
* card here — Phase 6 owns the real implementation. */
|
||||
|
||||
import { Sparkles } from 'lucide-react';
|
||||
|
||||
export const metadata = {
|
||||
title: 'Analyzer · Pulse',
|
||||
};
|
||||
|
||||
export default function MobileAnalyzerPlaceholder() {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="rounded-2xl border bg-card p-6 flex flex-col items-center text-center gap-3">
|
||||
<div className="h-12 w-12 rounded-2xl bg-primary/10 text-primary flex items-center justify-center">
|
||||
<Sparkles className="h-6 w-6" />
|
||||
</div>
|
||||
<h1 className="text-lg font-semibold">Analyzer feed coming soon</h1>
|
||||
<p className="text-sm text-muted-foreground max-w-xs">
|
||||
The mobile Analyzer feed is on its way. Until then, view full
|
||||
analyses on the desktop Analyzer.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
- This is a server component (no `'use client'` needed) — keeps it cheap.
|
||||
- Deliberately stubbed; Phase 6 (ANL-01..06) replaces this entire file.
|
||||
- No data fetching, no `/api/mobile/analyzer/feed` call — those belong in Phase 6.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>test -f app/mobile/analyzer/page.tsx && grep -q "export default function" app/mobile/analyzer/page.tsx && grep -q "coming soon" app/mobile/analyzer/page.tsx</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `test -f app/mobile/analyzer/page.tsx` exits 0
|
||||
- `grep -E "export default function" app/mobile/analyzer/page.tsx` matches
|
||||
- `grep -iE "coming soon" app/mobile/analyzer/page.tsx` matches (placeholder copy present)
|
||||
- `! grep -E "/api/mobile/analyzer" app/mobile/analyzer/page.tsx` exits 0 (no Phase 6 data fetching)
|
||||
</acceptance_criteria>
|
||||
<done>Visiting `/mobile/analyzer` after build renders a small "coming soon" card; no 404.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 5: Type-check and build to confirm new components compile cleanly without breaking anything</name>
|
||||
<files>(no files written — gate task)</files>
|
||||
<read_first>
|
||||
- components/mobile/HeaderBar.tsx (just authored)
|
||||
- components/mobile/BottomNav.tsx (just authored)
|
||||
- components/mobile/MoreDrawer.tsx (just authored)
|
||||
- app/mobile/analyzer/page.tsx (just authored)
|
||||
</read_first>
|
||||
<action>
|
||||
Run `npx tsc --noEmit --pretty` and `npm run build` to confirm the four new files compile in the existing project. The current `app/mobile/layout.tsx` and `app/mobile/nav/page.tsx` are untouched, so existing routes must still build.
|
||||
|
||||
If `tsc` reports errors, fix them in the offending file(s) and rerun until both pass. Common issues to expect:
|
||||
- Missing import → re-add the import
|
||||
- `any` cast on `session.user` → keep the typed cast pattern from `UserMenu.tsx`
|
||||
- JSX-runtime / `JSX` namespace not found → not expected (tsconfig has it); if it appears, leave it for the executor to investigate
|
||||
|
||||
Do **not** modify any other files in this task.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npx tsc --noEmit --pretty && npm run build</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `npx tsc --noEmit --pretty` exits 0
|
||||
- `npm run build` exits 0
|
||||
- The four new files exist (re-confirmed) and no existing file was modified by this task: `git status --short components/mobile app/mobile/analyzer` shows only the four new files, no modifications to anything else
|
||||
</acceptance_criteria>
|
||||
<done>TypeScript and Next.js build both pass with the four new files in place; existing routes unchanged.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Browser → drawer Sign out | Calls `signOut()` on the existing Better Auth client; an authenticated session already exists |
|
||||
| Browser → all Link routes | Standard client-side navigation; no new endpoints, no new data |
|
||||
| Browser → header Bell | Empty handler (placeholder per SHELL-03); not a trust boundary in this iteration |
|
||||
|
||||
## STRIDE Threat Register (ASVS-L1 baseline)
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-02-01 | Tampering | MoreDrawer Sign out button | accept | No new endpoint introduced; reuses Better Auth `signOut()` from `lib/auth-client.ts`. CSRF protection is provided by the existing Better Auth cookie + SameSite policy. |
|
||||
| T-02-02 | Information Disclosure | Account section showing user email | accept | Email is already visible in the existing top-bar `UserMenu` on every desktop page; no new data surface or endpoint. Read-only display only. |
|
||||
| T-02-03 | Spoofing | Avatar trigger opens drawer that contains Sign out | mitigate | Drawer state is local React state, not URL-driven; an attacker cannot pre-open the drawer via crafted URL. Sign out always navigates to `/auth/sign-in` server-rendered route, which Better Auth controls. |
|
||||
| T-02-04 | Denial of Service | Bell button placeholder | accept | Empty handler — no fetch, no work, no DOS surface. Phase 7+ will revisit when the real notification list ships (NOTIF-01). |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
After this plan completes:
|
||||
|
||||
1. The four new files exist:
|
||||
- `test -f components/mobile/HeaderBar.tsx`
|
||||
- `test -f components/mobile/BottomNav.tsx`
|
||||
- `test -f components/mobile/MoreDrawer.tsx`
|
||||
- `test -f app/mobile/analyzer/page.tsx`
|
||||
2. `npx tsc --noEmit --pretty` exits 0
|
||||
3. `npm run build` exits 0
|
||||
4. `app/mobile/layout.tsx` is unchanged from start (still imports `LayoutDashboard, Ticket, DollarSign, Menu` only — not `Sparkles`):
|
||||
- `! grep -E "Sparkles" app/mobile/layout.tsx` exits 0 (we have NOT yet wired the new bottom nav — Plan 02 does that)
|
||||
5. `app/mobile/nav/page.tsx` still exists (Plan 02 deletes it)
|
||||
6. No new dependencies added: `git diff package.json package-lock.json` is empty
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- All 5 tasks complete
|
||||
- 4 new files exist (HeaderBar, BottomNav, MoreDrawer, analyzer placeholder)
|
||||
- Each component matches its locked decisions from CONTEXT.md (D-locked: side="right", 5-cell nav, three drawer sections, Bell placeholder, h-7 w-7 avatar, max-w-lg mx-auto bottom nav)
|
||||
- TypeScript + Next build both pass
|
||||
- No existing files modified by this plan (verifiable via `git status --short`)
|
||||
- Plan 02 will pick up these components and wire them into the layout
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/02-mobile-shell-more-drawer/02-01-SUMMARY.md` documenting:
|
||||
- Which requirements this plan addressed (SHELL-02..04, SHELL-06, NAV-01..03, DRAWER-01..05)
|
||||
- The four files created and their roles
|
||||
- Locked decisions honored (Sheet `side="right"`, no shadcn avatar primitive added, Bell empty `onClick`, no page title in header)
|
||||
- Any deviations from the plan (should be none)
|
||||
- Notes for Plan 02 (which props to pass to each component)
|
||||
</output>
|
||||
406
.planning/phases/02-mobile-shell-more-drawer/02-02-PLAN.md
Normal file
406
.planning/phases/02-mobile-shell-more-drawer/02-02-PLAN.md
Normal file
|
|
@ -0,0 +1,406 @@
|
|||
---
|
||||
phase: 02-mobile-shell-more-drawer
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on:
|
||||
- 02-01
|
||||
files_modified:
|
||||
- app/mobile/layout.tsx
|
||||
- app/mobile/nav/page.tsx
|
||||
autonomous: false
|
||||
requirements:
|
||||
- SHELL-01
|
||||
- SHELL-05
|
||||
- DRAWER-06
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "app/mobile/layout.tsx is rewritten to import HeaderBar, BottomNav, MoreDrawer from components/mobile/* and renders them around <main>{children}</main>"
|
||||
- "The layout owns a single React.useState boolean that opens/closes the MoreDrawer; HeaderBar's onAvatarClick and BottomNav's onMoreClick both flip this state to true"
|
||||
- "The <main> content area scrolls and has bottom padding equal to bottom-nav height (h-16 = 64px) plus env(safe-area-inset-bottom) so content does not hide under the nav"
|
||||
- "app/mobile/nav/page.tsx no longer exists — the file is deleted in this same change"
|
||||
- "Visiting /mobile/dashboard, /mobile/tickets, /mobile/finance, and /mobile/analyzer all render inside the new layout (header + bottom nav visible, no 404)"
|
||||
- "TypeScript compiles (npx tsc --noEmit) and Next.js builds (npm run build) successfully"
|
||||
artifacts:
|
||||
- path: "app/mobile/layout.tsx"
|
||||
provides: "New mobile shell wiring HeaderBar + BottomNav + MoreDrawer with shared drawer state"
|
||||
contains: "MoreDrawer"
|
||||
- path: "app/mobile/nav/page.tsx"
|
||||
provides: "DELETED — drawer fully replaces the standalone nav page (DRAWER-06)"
|
||||
deleted: true
|
||||
key_links:
|
||||
- from: "app/mobile/layout.tsx"
|
||||
to: "components/mobile/HeaderBar.tsx"
|
||||
via: "import + render with onAvatarClick"
|
||||
pattern: "from ['\"]@/components/mobile/HeaderBar['\"]"
|
||||
- from: "app/mobile/layout.tsx"
|
||||
to: "components/mobile/BottomNav.tsx"
|
||||
via: "import + render with onMoreClick"
|
||||
pattern: "from ['\"]@/components/mobile/BottomNav['\"]"
|
||||
- from: "app/mobile/layout.tsx"
|
||||
to: "components/mobile/MoreDrawer.tsx"
|
||||
via: "import + render with shared open/onOpenChange state"
|
||||
pattern: "from ['\"]@/components/mobile/MoreDrawer['\"]"
|
||||
- from: "Header avatar AND Bottom-nav More button"
|
||||
to: "MoreDrawer open state"
|
||||
via: "Single useState in app/mobile/layout.tsx"
|
||||
pattern: "useState"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Replace `app/mobile/layout.tsx` with the new shell that wires HeaderBar + BottomNav + MoreDrawer (built in Plan 01) around `<main>{children}</main>`, owning a single shared drawer-open state. Delete `app/mobile/nav/page.tsx` in the same change so the drawer fully replaces the old standalone nav page.
|
||||
|
||||
Purpose: Land SHELL-01 (replace in place), SHELL-05 (scrollable content with bottom-nav-aware padding), and DRAWER-06 (delete the old nav route). After this plan, every `/mobile/*` page renders under the new shell and the four primary tabs + avatar + More all behave per spec.
|
||||
|
||||
Output: Modified `app/mobile/layout.tsx`, deleted `app/mobile/nav/page.tsx`. Build passes. Visual checkpoint confirms the shell renders correctly on at least one mobile route.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/REQUIREMENTS.md
|
||||
@.planning/phases/02-mobile-shell-more-drawer/02-CONTEXT.md
|
||||
@.planning/phases/02-mobile-shell-more-drawer/02-01-PLAN.md
|
||||
@docs/superpowers/specs/2026-05-03-mobile-shell-design.md
|
||||
@CLAUDE.md
|
||||
@app/mobile/layout.tsx
|
||||
@app/mobile/nav/page.tsx
|
||||
@app/styles/brand.css
|
||||
|
||||
<interfaces>
|
||||
<!-- Components consumed by the new layout. All authored in Plan 01. -->
|
||||
|
||||
From components/mobile/HeaderBar.tsx (Plan 01):
|
||||
```typescript
|
||||
export function HeaderBar(props: { onAvatarClick: () => void }): JSX.Element;
|
||||
```
|
||||
|
||||
From components/mobile/BottomNav.tsx (Plan 01):
|
||||
```typescript
|
||||
export function BottomNav(props: { onMoreClick: () => void }): JSX.Element;
|
||||
```
|
||||
|
||||
From components/mobile/MoreDrawer.tsx (Plan 01):
|
||||
```typescript
|
||||
export function MoreDrawer(props: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}): JSX.Element;
|
||||
```
|
||||
|
||||
CSS utilities available in `app/styles/brand.css`:
|
||||
- `pt-safe`, `pb-safe`
|
||||
</interfaces>
|
||||
|
||||
<scope_boundary>
|
||||
This plan **only** touches:
|
||||
- `app/mobile/layout.tsx` (full rewrite)
|
||||
- `app/mobile/nav/page.tsx` (delete)
|
||||
|
||||
Do NOT modify:
|
||||
- The three new components (Plan 01 owns them)
|
||||
- Any page under `app/mobile/dashboard|tickets|finance|analyzer|page.tsx` (out of phase)
|
||||
- `components/navigation/app-navigation.tsx` (desktop nav)
|
||||
- `app/layout.tsx` (root, owned by Phase 1)
|
||||
</scope_boundary>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Rewrite app/mobile/layout.tsx to wire HeaderBar + BottomNav + MoreDrawer with shared state</name>
|
||||
<files>app/mobile/layout.tsx</files>
|
||||
<read_first>
|
||||
- app/mobile/layout.tsx (current 3-tab layout being replaced — read fully so executor knows what's there)
|
||||
- components/mobile/HeaderBar.tsx (Plan 01 output — confirms onAvatarClick prop)
|
||||
- components/mobile/BottomNav.tsx (Plan 01 output — confirms onMoreClick prop)
|
||||
- components/mobile/MoreDrawer.tsx (Plan 01 output — confirms open/onOpenChange props)
|
||||
- app/styles/brand.css (confirm `pt-safe` and `pb-safe` are available)
|
||||
- .planning/phases/02-mobile-shell-more-drawer/02-CONTEXT.md (SHELL-05: bottom padding = nav height + safe-area)
|
||||
</read_first>
|
||||
<action>
|
||||
**Replace the entire contents** of `app/mobile/layout.tsx` with:
|
||||
|
||||
```tsx
|
||||
'use client';
|
||||
|
||||
/* Mobile shell — phase 02 (SHELL-01, SHELL-05).
|
||||
*
|
||||
* Header: <HeaderBar /> (sticky, brand + Bell + avatar)
|
||||
* Body: <main> (scrollable, padded so content clears the bottom nav)
|
||||
* Foot: <BottomNav /> (fixed, 4 tabs + More)
|
||||
* Drawer: <MoreDrawer /> opened from BOTH the header avatar and the More cell.
|
||||
*
|
||||
* The drawer's open state lives here so a single Sheet instance is shared
|
||||
* between the two triggers — no duplicate Sheets, no prop-drilling sagas. */
|
||||
|
||||
import { useState } from 'react';
|
||||
import { HeaderBar } from '@/components/mobile/HeaderBar';
|
||||
import { BottomNav } from '@/components/mobile/BottomNav';
|
||||
import { MoreDrawer } from '@/components/mobile/MoreDrawer';
|
||||
|
||||
export default function MobileLayout({ children }: { children: React.ReactNode }) {
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen bg-background max-w-lg mx-auto">
|
||||
<HeaderBar onAvatarClick={() => setDrawerOpen(true)} />
|
||||
|
||||
{/* SHELL-05: scrollable content area; bottom padding = bottom-nav (h-16
|
||||
= 64px = pb-16) plus the device safe-area inset, so content never
|
||||
hides under the bar. */}
|
||||
<main className="flex-1 overflow-y-auto pb-[calc(theme(spacing.16)+env(safe-area-inset-bottom))]">
|
||||
{children}
|
||||
</main>
|
||||
|
||||
<BottomNav onMoreClick={() => setDrawerOpen(true)} />
|
||||
|
||||
<MoreDrawer open={drawerOpen} onOpenChange={setDrawerOpen} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
- This file replaces the existing 3-tab layout entirely. No legacy imports, no dead code paths.
|
||||
- `'use client'` is required because we use `useState`.
|
||||
- The `pb-[calc(...)]` arbitrary value gives `<main>` enough bottom padding to clear the 64px nav plus the home-indicator inset (SHELL-05 + PWA-04 reuse). Tailwind 4 supports the `calc()` arbitrary value here.
|
||||
- A single `useState` is the entire shared-state mechanism — no Zustand, no Context, no third-party state lib (per CLAUDE.md "no new state libraries").
|
||||
- Both triggers set the same boolean. The `MoreDrawer` itself controls its close (Radix `onOpenChange` fires when overlay is clicked or Esc is pressed) and propagates back through `setDrawerOpen`.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>grep -q "from '@/components/mobile/HeaderBar'" app/mobile/layout.tsx && grep -q "from '@/components/mobile/BottomNav'" app/mobile/layout.tsx && grep -q "from '@/components/mobile/MoreDrawer'" app/mobile/layout.tsx && grep -q "useState" app/mobile/layout.tsx && grep -q "onAvatarClick" app/mobile/layout.tsx && grep -q "onMoreClick" app/mobile/layout.tsx && grep -q "drawerOpen" app/mobile/layout.tsx && grep -q "safe-area-inset-bottom" app/mobile/layout.tsx</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -E "from ['\"]@/components/mobile/HeaderBar['\"]" app/mobile/layout.tsx` matches
|
||||
- `grep -E "from ['\"]@/components/mobile/BottomNav['\"]" app/mobile/layout.tsx` matches
|
||||
- `grep -E "from ['\"]@/components/mobile/MoreDrawer['\"]" app/mobile/layout.tsx` matches
|
||||
- `grep -E "useState" app/mobile/layout.tsx` matches (single shared state)
|
||||
- `grep -E "onAvatarClick" app/mobile/layout.tsx` matches AND `grep -E "onMoreClick" app/mobile/layout.tsx` matches (both triggers wired)
|
||||
- `grep -E "open=" app/mobile/layout.tsx` matches AND `grep -E "onOpenChange=" app/mobile/layout.tsx` matches (drawer controlled)
|
||||
- `grep -E "safe-area-inset-bottom" app/mobile/layout.tsx` matches (SHELL-05 padding for bottom nav clearance)
|
||||
- `grep -E "max-w-lg mx-auto" app/mobile/layout.tsx` matches (CONTEXT.md container width)
|
||||
- `! grep -E "Menu, " app/mobile/layout.tsx` exits 0 (the legacy `Menu`-as-link import from the old layout is gone)
|
||||
- `! grep -E "/mobile/nav" app/mobile/layout.tsx` exits 0 (no link to the deleted standalone nav route)
|
||||
</acceptance_criteria>
|
||||
<done>The layout renders the three new components, owns a single useState for drawer open/close, and pads `<main>` to clear the bottom nav + safe area.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Delete app/mobile/nav/page.tsx (DRAWER-06)</name>
|
||||
<files>app/mobile/nav/page.tsx</files>
|
||||
<read_first>
|
||||
- app/mobile/nav/page.tsx (final read of the file being deleted, so the executor knows what is leaving the codebase)
|
||||
- .planning/phases/02-mobile-shell-more-drawer/02-CONTEXT.md (DRAWER-06 — delete in same change as drawer ships; no redirect)
|
||||
- components/mobile/MoreDrawer.tsx (Plan 01 output — verifies the drawer already covers everything the old page did)
|
||||
</read_first>
|
||||
<action>
|
||||
Delete the file:
|
||||
|
||||
```bash
|
||||
rm app/mobile/nav/page.tsx
|
||||
```
|
||||
|
||||
Then check the directory is empty (or only contains other files we don't care about) and remove it if it became empty:
|
||||
|
||||
```bash
|
||||
# If app/mobile/nav is now empty, remove the directory too.
|
||||
if [ -d app/mobile/nav ] && [ -z "$(ls -A app/mobile/nav)" ]; then
|
||||
rmdir app/mobile/nav
|
||||
fi
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Per CONTEXT.md DRAWER-06: "Recommend NO redirect (just delete) — the URL was never bookmarked-worthy." Visiting `/mobile/nav` after this change yields Next.js's standard 404, which is the desired behavior.
|
||||
- Confirm no other file in the repo references `/mobile/nav` or imports from `app/mobile/nav/...`. Run a quick grep before deletion (the old `app/mobile/layout.tsx` had the only known reference, and Task 1 already removed it).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>! test -f app/mobile/nav/page.tsx</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `! test -f app/mobile/nav/page.tsx` exits 0 (file deleted)
|
||||
- `! grep -r --include="*.ts" --include="*.tsx" "/mobile/nav" app components 2>/dev/null` exits 0 (no remaining references in source)
|
||||
- The deletion shows up in `git status` as a deleted file
|
||||
</acceptance_criteria>
|
||||
<done>`app/mobile/nav/page.tsx` no longer exists; no source file references `/mobile/nav` anywhere.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Type-check and full build to confirm the new shell compiles end-to-end</name>
|
||||
<files>(no files written — gate task)</files>
|
||||
<read_first>
|
||||
- app/mobile/layout.tsx (the file just rewritten)
|
||||
- components/mobile/HeaderBar.tsx (Plan 01)
|
||||
- components/mobile/BottomNav.tsx (Plan 01)
|
||||
- components/mobile/MoreDrawer.tsx (Plan 01)
|
||||
</read_first>
|
||||
<action>
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npx tsc --noEmit --pretty
|
||||
npm run build
|
||||
```
|
||||
|
||||
Both must exit 0. If either fails, fix the offending file and rerun until clean. Common things to check if it fails:
|
||||
|
||||
- Did Task 2 leave a dangling import to the deleted `nav/page.tsx`? (Should be impossible, but grep `/mobile/nav` if a build error names that path.)
|
||||
- Did the `'use client'` directive end up below an import? (Must be the very first line.)
|
||||
- Is the `pb-[calc(theme(spacing.16)+env(safe-area-inset-bottom))]` Tailwind 4 arbitrary value valid? If Tailwind rejects it, fall back to an inline style on the `<main>`: `style={{ paddingBottom: 'calc(4rem + env(safe-area-inset-bottom))' }}` and remove the `pb-[...]` class.
|
||||
|
||||
Do NOT modify any file other than `app/mobile/layout.tsx` to fix build issues.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npx tsc --noEmit --pretty && npm run build</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `npx tsc --noEmit --pretty` exits 0
|
||||
- `npm run build` exits 0
|
||||
- `git status --short app components` shows: 1 modified (`app/mobile/layout.tsx`) and 1 deleted (`app/mobile/nav/page.tsx`); no other unexpected modifications
|
||||
</acceptance_criteria>
|
||||
<done>Type-check + build both pass with the new shell wired and the old nav page deleted.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<name>Task 4: Visual verification of the new mobile shell on a real device or DevTools mobile preview</name>
|
||||
<files>(no files written — human checkpoint)</files>
|
||||
<read_first>
|
||||
- app/mobile/layout.tsx (the file just rewritten — executor confirms what was shipped)
|
||||
- .planning/ROADMAP.md (Phase 2 success criteria #1–6 — these are what the human is verifying)
|
||||
- .planning/REQUIREMENTS.md (SHELL-01..06, NAV-01..03, DRAWER-01..06)
|
||||
</read_first>
|
||||
<action>
|
||||
Pause and surface a checkpoint to the user. Present this exact verification script and wait for the user's "approved" response.
|
||||
|
||||
**What was built (summary for the user):**
|
||||
The new mobile shell is fully wired:
|
||||
- `app/mobile/layout.tsx` rewritten — sticky header (Wulf mark + "Pulse" wordmark, Bell, avatar), scrollable `<main>`, fixed bottom nav (Dashboard / Tickets / Finance / Analyzer / More)
|
||||
- `app/mobile/nav/page.tsx` deleted
|
||||
- `app/mobile/analyzer/page.tsx` placeholder ("Coming soon" card) so the Analyzer tab resolves until Phase 6
|
||||
- `<MoreDrawer />` opens from BOTH the header avatar and the bottom-nav More button, with three sections (Mobile sections / Full site / Account + Sign out)
|
||||
|
||||
All built on existing shadcn primitives, the `WulfMark` component, and Better Auth's `signOut()` — no new state libs, no shadcn avatar primitive added.
|
||||
|
||||
**How to verify (user runs through this on a phone-sized viewport):**
|
||||
|
||||
1. **Start the dev server** if not already running: `npm run dev`. Pulse should start on http://localhost:3100.
|
||||
|
||||
2. **Open the mobile shell in a phone-sized viewport** — Chrome DevTools (F12) → toggle device toolbar (Ctrl+Shift+M / Cmd+Shift+M) → pick "iPhone 15 Pro" or any 390-414px wide device. Visit `http://localhost:3100/mobile/dashboard`.
|
||||
|
||||
3. **Verify the header (SHELL-02..04):**
|
||||
- [ ] Sticky bar at the top with `bg-background/95 backdrop-blur` + bottom border
|
||||
- [ ] Left side: Wulf "W" mark + "Pulse" wordmark; tapping it navigates to `/mobile/dashboard`
|
||||
- [ ] Right side: a Bell icon button next to a small avatar circle (initials)
|
||||
- [ ] No page title text in the header itself
|
||||
- [ ] Bell button is keyboard-focusable (Tab to it, then Space/Enter — should not throw or navigate; it's a placeholder, no menu)
|
||||
- [ ] Tapping the avatar opens the right-side Sheet drawer
|
||||
|
||||
4. **Verify the bottom nav (SHELL-06, NAV-01..03):**
|
||||
- [ ] Fixed bar at the bottom, full width, `border-t bg-background`
|
||||
- [ ] Five cells in order: Dashboard, Tickets, Finance, Analyzer, More
|
||||
- [ ] Active tab uses `text-primary` (Wulf blue); inactive use `text-muted-foreground`
|
||||
- [ ] Tapping each tab routes to its URL: `/mobile/dashboard`, `/mobile/tickets`, `/mobile/finance`, `/mobile/analyzer`
|
||||
- [ ] Tapping a row INSIDE `/mobile/tickets/[id]` (e.g., open any ticket) keeps Tickets highlighted (active detection via `pathname.startsWith`)
|
||||
- [ ] Tapping More opens the same drawer the avatar opens
|
||||
- [ ] Visiting `/mobile/analyzer` shows the "Coming soon" placeholder card (NOT a 404)
|
||||
|
||||
5. **Verify the drawer (DRAWER-01..05):**
|
||||
Open the drawer (avatar OR More).
|
||||
- [ ] Drawer slides in from the right (`side="right"`)
|
||||
- [ ] Section 1 "Mobile sections" — single row: Engagement (no `ExternalLink` hint icon)
|
||||
- [ ] Section 2 "Full site" — five rows: Quotes, Configuration Items, Backup Status, Ticket Digest, Admin / Sync (each row has the `ExternalLink` icon on the right)
|
||||
- [ ] Section 3 "Account" — shows the signed-in user's initials, name, and email; below it, a red "Sign out" button
|
||||
- [ ] Tapping any row inside the drawer navigates AND closes the drawer
|
||||
- [ ] Tapping the X / outside the drawer / pressing Esc closes it
|
||||
- [ ] Sign out: tap it → page navigates to `/auth/sign-in` AND the user is signed out (refreshing brings you to the sign-in page; no auto-redirect to `/mobile`)
|
||||
|
||||
6. **Verify content does not hide under the bottom nav (SHELL-05):**
|
||||
- [ ] On `/mobile/dashboard` (or any mobile page), scroll to the bottom of the content. The last visible content sits ABOVE the bottom nav, not under it.
|
||||
- [ ] On a phone with a home indicator (or in DevTools with iPhone preset), the bottom nav has extra space below for the indicator inset (no overlap).
|
||||
|
||||
7. **Verify the old nav route is gone (DRAWER-06):**
|
||||
- [ ] Visit `http://localhost:3100/mobile/nav` directly. It returns Next.js's 404 page (NOT the old standalone nav UI).
|
||||
|
||||
8. **Quick regression on existing pages:**
|
||||
- [ ] `/mobile/dashboard`, `/mobile/tickets`, `/mobile/finance` all still render their previous content unchanged — only the chrome around them is new.
|
||||
- [ ] `/mobile/tickets/[id]` (open a ticket) still renders inside the new shell.
|
||||
|
||||
**Resume signal:** Reply "approved" once all checks pass. If something is broken or off-spec, describe what you saw and which check failed (e.g., "Drawer opens from the bottom, not the right" or "Bottom nav overlaps the last content row on /mobile/finance"). The executor will fix and re-verify.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>echo "Manual verification — user must reply 'approved' or describe a failure. No automated check applicable; preceding tasks (1-3) verify code-level invariants."</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- User replies "approved" after running the verification script above
|
||||
- All 8 verification sections pass on the user's device/preview
|
||||
- If any check fails, the executor returns to Task 1 or Task 2 to fix and re-runs Task 3 (build) and Task 4 (re-verify) before requesting approval again
|
||||
</acceptance_criteria>
|
||||
<done>User has explicitly replied "approved", confirming the new shell renders correctly on a phone-sized viewport and all 6 ROADMAP success criteria for Phase 2 are met.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Browser → drawer Sign out | Reuses Plan 01's MoreDrawer; calls Better Auth `signOut()` and navigates to `/auth/sign-in` — same trust boundary as the existing top-bar `UserMenu`. |
|
||||
| Browser → all Link routes | All routes already exist or are placeholders (`/mobile/analyzer` placeholder shipped in Plan 01). No new endpoints. |
|
||||
|
||||
## STRIDE Threat Register (ASVS-L1 baseline)
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-02-05 | Tampering | layout.tsx drawer-state useState | accept | Local React state, not URL-driven. An attacker cannot pre-open the drawer via crafted URL. State has no security relevance — it merely toggles UI visibility. |
|
||||
| T-02-06 | Information Disclosure | Deletion of `/mobile/nav` route | accept | The deleted page surfaced no PII beyond what the new drawer surfaces (same email field). Net change: identical surface area. |
|
||||
| T-02-07 | Denial of Service | New shell mounts on every `/mobile/*` request | accept | Layout is lightweight: 1 useState, 3 component imports, no fetches. Cost is negligible vs. the existing layout. |
|
||||
| T-02-08 | Repudiation | Sign out action | mitigate | Better Auth records sign-out in its session table; not a Pulse-introduced repudiation surface. Inherited from `lib/auth-client.ts`. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
After this plan completes:
|
||||
|
||||
1. `app/mobile/layout.tsx` imports HeaderBar, BottomNav, MoreDrawer:
|
||||
- `grep -E "@/components/mobile/HeaderBar" app/mobile/layout.tsx` matches
|
||||
- `grep -E "@/components/mobile/BottomNav" app/mobile/layout.tsx` matches
|
||||
- `grep -E "@/components/mobile/MoreDrawer" app/mobile/layout.tsx` matches
|
||||
2. `app/mobile/nav/page.tsx` does not exist:
|
||||
- `! test -f app/mobile/nav/page.tsx` exits 0
|
||||
3. No source file references `/mobile/nav`:
|
||||
- `! grep -r --include="*.ts" --include="*.tsx" "/mobile/nav" app components 2>/dev/null` exits 0
|
||||
4. Build is clean:
|
||||
- `npx tsc --noEmit --pretty` exits 0
|
||||
- `npm run build` exits 0
|
||||
5. Visual checkpoint passed (Task 4):
|
||||
- Header sticky, brand link goes to `/mobile/dashboard`, Bell focusable with no menu, avatar opens drawer
|
||||
- Bottom nav shows 5 cells with correct icons + routes; active tab uses `text-primary`; `/mobile/tickets/123` highlights Tickets
|
||||
- Drawer (`side="right"`) shows 3 sections; Sign out signs out and lands on `/auth/sign-in`
|
||||
- `/mobile/analyzer` renders the placeholder, NOT a 404
|
||||
- `/mobile/nav` returns 404
|
||||
- Content does not hide under the bottom nav (SHELL-05)
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- All 4 tasks complete (3 auto + 1 visual checkpoint with explicit "approved")
|
||||
- `app/mobile/layout.tsx` is rewritten to use the new components with shared drawer state
|
||||
- `app/mobile/nav/page.tsx` is deleted
|
||||
- Type-check + build both pass
|
||||
- Visual checkpoint approved by user
|
||||
- Phase 2 ROADMAP success criteria #1–6 are all satisfied (header, 5-cell nav, drawer with 3 sections, sign-out flow, /mobile/nav gone, content not hidden under bar)
|
||||
- All 15 phase requirements (SHELL-01..06, NAV-01..03, DRAWER-01..06) are now closed across Plan 01 + Plan 02
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/02-mobile-shell-more-drawer/02-02-SUMMARY.md` documenting:
|
||||
- Which requirements this plan addressed (SHELL-01, SHELL-05, DRAWER-06) and confirmation that combined with Plan 01 all 15 phase requirements are now satisfied
|
||||
- The final wiring (layout owns one `useState`, both triggers share it)
|
||||
- Any deviations from the plan during execution (e.g., if `pb-[calc(...)]` had to fall back to inline style)
|
||||
- Visual checkpoint outcomes (which checks passed, any minor adjustments made)
|
||||
- Notes for Phases 3–7: every mobile page lands inside this shell automatically; pages should NOT add their own header or bottom nav
|
||||
</output>
|
||||
Loading…
Add table
Add a link
Reference in a new issue