# Mobile Event Edit Modal Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Replace the mobile-unusable centred `Dialog` on `EventDialog` with a shadcn `Drawer` (Vaul) that guides the user through three steps — Details, Schedule, Recurrence — while preserving the desktop `Dialog` unchanged. **Architecture:** `useIsMobile` drives a conditional render: desktop gets the existing `Dialog`, mobile gets a new `Drawer` that renders one of three step components (`DetailsStep`, `ScheduleStep`, `RecurrenceStep`) at a time. Step state lives in a small inline hook. The single `useForm` instance is shared across all steps so data is never lost. **Tech Stack:** Next.js 15, React 19, React Hook Form 7, shadcn/ui (Vaul `Drawer`), Bun test runner, Biome --- ## File Map | File | Action | Responsibility | |------|--------|----------------| | `src/components/ui/drawer.tsx` | **Create** | shadcn Drawer primitives wrapping Vaul | | `src/components/event-dialog.tsx` | **Modify** | add step hook, `DetailsStep`, `ScheduleStep`, `RecurrenceStep`, Drawer branch | | `src/components/ui/dialog.tsx` | **Modify** | remove `isMobile` forks from `DialogContent` and `DialogFooter` | | `tests/event-dialog.test.tsx` | **Modify** | add tests for step sections, progress bars, AI banner, step footer labels | | `tests/mobile-hook-adoption.test.ts` | **Modify** | add `drawer.tsx` to the hook-driven files list | --- ## Task 1: Install Vaul and add `drawer.tsx` **Files:** - Create: `src/components/ui/drawer.tsx` - Modify: `package.json`, `bun.lock` - [ ] **Step 1: Install Vaul** ```bash bun add vaul ``` Expected: `vaul` appears in `dependencies` in `package.json`. - [ ] **Step 2: Create `src/components/ui/drawer.tsx`** ```tsx "use client"; import * as React from "react"; import { Drawer as DrawerPrimitive } from "vaul"; import { cn } from "@/lib/utils"; function Drawer({ shouldScaleBackground = true, ...props }: React.ComponentProps) { return ( ); } function DrawerTrigger({ ...props }: React.ComponentProps) { return ; } function DrawerPortal({ ...props }: React.ComponentProps) { return ; } function DrawerClose({ ...props }: React.ComponentProps) { return ; } function DrawerOverlay({ className, ...props }: React.ComponentProps) { return ( ); } function DrawerContent({ className, children, ...props }: React.ComponentProps) { return (
{children} ); } function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) { return (
); } function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) { return (
); } function DrawerTitle({ className, ...props }: React.ComponentProps) { return ( ); } function DrawerDescription({ className, ...props }: React.ComponentProps) { return ( ); } export { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerOverlay, DrawerPortal, DrawerTitle, DrawerTrigger, }; ``` - [ ] **Step 3: Verify the file compiles with no import errors** ```bash cd /path/to/project && bun run build 2>&1 | grep -i "drawer" | head -20 ``` Expected: no errors mentioning `drawer.tsx`. - [ ] **Step 4: Commit** ```bash git add src/components/ui/drawer.tsx package.json bun.lock git commit -m "feat: add shadcn Drawer component (Vaul)" ``` --- ## Task 2: Write failing tests for the Drawer branch **Files:** - Modify: `tests/event-dialog.test.tsx` - Modify: `tests/mobile-hook-adoption.test.ts` - [ ] **Step 1: Add source-level assertions to `tests/event-dialog.test.tsx`** Replace the entire file with: ```tsx import { describe, expect, test } from "bun:test"; import { readFileSync } from "node:fs"; import { getEventFormValuesFromEvent } from "@/lib/event-form"; describe("EventDialog public modes", () => { test("accepts AI-prefilled editable initial values through its public props", () => { const initialValues = getEventFormValuesFromEvent({ title: "AI Draft", start: "2026-04-09T10:00:00.000Z", recurrenceRule: "FREQ=WEEKLY;INTERVAL=1;BYDAY=TH", }); expect(initialValues.title).toBe("AI Draft"); expect(initialValues.start).toBe("2026-04-09T10:00:00.000Z"); expect(initialValues.recurrenceRule).toBe("FREQ=WEEKLY;INTERVAL=1;BYDAY=TH"); }); test("dialog content uses console surface classes instead of generic border and shadow", () => { const source = readFileSync("src/components/ui/dialog.tsx", "utf8"); expect(source).toContain("rounded-[10px]"); expect(source).toContain("shadow-xl"); expect(source).not.toContain("rounded-lg border p-6 shadow-lg"); }); test("dialog source uses console section labels and grouped field regions", () => { const source = readFileSync("src/components/event-dialog.tsx", "utf8"); expect(source).toContain("Event details"); expect(source).toContain("Schedule"); expect(source).toContain("Recurrence"); }); test("event-dialog imports Drawer for mobile branch", () => { const source = readFileSync("src/components/event-dialog.tsx", "utf8"); expect(source).toContain("from \"@/components/ui/drawer\""); }); test("event-dialog renders three step components", () => { const source = readFileSync("src/components/event-dialog.tsx", "utf8"); expect(source).toContain("DetailsStep"); expect(source).toContain("ScheduleStep"); expect(source).toContain("RecurrenceStep"); }); test("event-dialog includes step progress bars", () => { const source = readFileSync("src/components/event-dialog.tsx", "utf8"); // Three progress bar divs rendered inside a grid-cols-3 container expect(source).toContain("grid-cols-3"); }); test("event-dialog includes step counter badge", () => { const source = readFileSync("src/components/event-dialog.tsx", "utf8"); // Step badge like "1 / 3" expect(source).toContain("/ 3"); }); test("drawer footer shows Cancel on step 1 and Back on steps 2 and 3", () => { const source = readFileSync("src/components/event-dialog.tsx", "utf8"); expect(source).toContain("Cancel"); expect(source).toContain("Back"); }); test("drawer footer shows Save only on step 3", () => { const source = readFileSync("src/components/event-dialog.tsx", "utf8"); expect(source).toContain("Save"); expect(source).toContain("Next"); }); test("event-dialog resets step to 1 on close", () => { const source = readFileSync("src/components/event-dialog.tsx", "utf8"); expect(source).toContain("setStep(1)"); }); test("dialog.tsx no longer forks on isMobile in DialogContent", () => { const source = readFileSync("src/components/ui/dialog.tsx", "utf8"); // isMobile should not be used to conditionally apply max-w inside DialogContent expect(source).not.toContain("isMobile ? undefined : \"max-w-lg\""); }); test("dialog.tsx no longer forks on isMobile in DialogFooter", () => { const source = readFileSync("src/components/ui/dialog.tsx", "utf8"); // DialogFooter should not branch on isMobile expect(source).not.toContain("isMobile\n\t\t\t\t? \"flex flex-col-reverse gap-2\""); }); }); ``` - [ ] **Step 2: Add `drawer.tsx` to hook-driven file list in `tests/mobile-hook-adoption.test.ts`** In `tests/mobile-hook-adoption.test.ts`, add `"src/components/ui/drawer.tsx"` to the `HOOK_DRIVEN_FILES` array. The drawer itself does not call `useIsMobile` (the caller decides the branch), so only add it to `HOOK_DRIVEN_FILES` — not to `DIRECT_HOOK_FILES`. The test checks that it contains no Tailwind breakpoint prefixes. Replace the `HOOK_DRIVEN_FILES` array: ```ts const HOOK_DRIVEN_FILES = [ "src/app/page.tsx", "src/app/demo/combined-date-picker/page.tsx", "src/components/ai-toolbar.tsx", "src/components/event-dialog.tsx", "src/components/settings-panel.tsx", "src/components/ui/calendar.tsx", "src/components/ui/date-picker.tsx", "src/components/ui/dialog.tsx", "src/components/ui/drawer.tsx", "src/components/ui/input-group.tsx", "src/components/ui/textarea.tsx", "src/lib/ui-shell-contract.ts", ]; ``` - [ ] **Step 3: Run the new tests and confirm they fail for the right reasons** ```bash bun test tests/event-dialog.test.tsx 2>&1 ``` Expected: the four new tests (`imports Drawer`, `renders three step components`, `grid-cols-3`, `/ 3`, `setStep(1)`, `no longer forks`) FAIL because the implementation does not exist yet. The first three existing tests should still PASS. - [ ] **Step 4: Commit the failing tests** ```bash git add tests/event-dialog.test.tsx tests/mobile-hook-adoption.test.ts git commit -m "test: add failing tests for Drawer mobile branch in EventDialog" ``` --- ## Task 3: Extract step sub-components inside `event-dialog.tsx` **Files:** - Modify: `src/components/event-dialog.tsx` These are pure presentational helpers declared at the bottom of the same file. They receive form state as props and render one section's fields. - [ ] **Step 1: Add the `StepProps` interface and `DetailsStep` at the bottom of `event-dialog.tsx`** Below the closing `};` of `EventDialog`, add: ```tsx interface StepProps { control: ReturnType>["control"]; register: ReturnType>["register"]; errors: ReturnType>["formState"]["errors"]; watch: ReturnType>["watch"]; setValue: ReturnType>["setValue"]; isAiDraft: boolean; } function AiDraftBanner() { return (
This draft was generated from natural language. Double-check dates, times, location, recurrence, and links before saving.
); } function DetailsStep({ control, register, errors, isAiDraft }: Omit) { const isMobile = useIsMobile(); return (
{isAiDraft && }
{errors.title && (

{errors.title.message}

)}