# Mobile Event Edit Modal — Design Spec > Date: 2025-05-24 > Status: Approved ## Problem The `EventDialog` renders a Radix `Dialog` at all breakpoints. On mobile it opens as a vertically-centred overlay that is taller than the viewport, leaving the sticky footer below the fold and making form fields hard to scroll to. The current internal `isMobile` patches (single-column grid, stacked footer) do not fix the core issue: the container itself is unsuitable for mobile. ## Approved Approach: Design C — Desktop Dialog + Mobile Guided Drawer ### Desktop (≥ 768 px) No change. The existing `Dialog` with all three sections visible on one scrollable form stays as-is. The visual treatment (28 px title, mono section kickers, `bg-card` panel, `max-w-2xl`) is preserved. ### Mobile (< 768 px) Replace the modal with a Vaul/shadcn `Drawer` that breaks the same form into **three guided steps**, each rendered in the drawer body with a sticky footer. The top sheet slides up from the bottom with a handle and snaps to ~86 % of viewport height. #### Steps | Step | Title | Fields | |------|-------|--------| | 1 | Event Details | title, description, location, URL | | 2 | Schedule | all-day, start, end, duration chips | | 3 | Recurrence | recurrence picker | #### Step footer buttons | Position | Step 1 | Step 2 | Step 3 | |----------|--------|--------|--------| | Left (secondary) | Cancel | Back | Back | | Right (primary) | Next → | Next → | Save | "Next" validates only fields that **belong to the current step** before advancing. "Save" performs full validation across all steps before submitting. #### Progress indicator Three slim bars below the drawer header (full-width at `px-4`). The active step bar uses `foreground`, inactive bars use `muted`. #### AI draft banner When `dialogSource === "ai"` and `editingId` is null the warning banner appears at the **top of each step body**, not once at the top of a single long form. This surfaces the review reminder exactly when the user is looking at that section. #### Error locality Validation errors are displayed under their fields within their owning step. If `onSave` triggers and any step has an error, the drawer jumps to the **lowest-numbered step that contains an error**. ## Component Architecture ### New component: `useEventDialogStep` A thin hook inside `event-dialog.tsx` that owns mobile step state: ```ts const [step, setStep] = useState<1 | 2 | 3>(1) const advance = () => setStep(s => Math.min(s + 1, 3) as 1|2|3) const retreat = () => setStep(s => Math.max(s - 1, 1) as 1|2|3) const reset = () => setStep(1) ``` Reset is called alongside `handleOpenChange(false)` so re-opening always starts at step 1. ### Drawer installation ```bash pnpm dlx shadcn@latest add drawer ``` Adds `src/components/ui/drawer.tsx` (Vaul wrapper) following the project's existing shadcn conventions. ### `EventDialog` refactor ``` EventDialog ├── useIsMobile() → isDesktop branch ├── isDesktop=true → (unchanged) └── isDesktop=false → ├── DrawerContent │ ├── handle bar │ ├── DrawerHeader (title, step badge, progress bars) │ ├── AI draft banner (if applicable) │ ├── step body (one of three field groups, scrollable) │ └── sticky DrawerFooter (Back/Cancel | Next/Save) └── step state from useEventDialogStep ``` The three field group JSX blocks are extracted into private helper components `DetailsStep`, `ScheduleStep`, `RecurrenceStep`. These are pure-presentational and accept `control`, `register`, `errors`, `watch` values as props — no form ownership of their own. ### Shared form instance `useForm` stays at the `EventDialog` level, shared across all steps. No per-step forms, no field re-registration on step change. ## Behaviour Details - **Drawer scroll**: the step body is a `ScrollArea` (or `overflow-y-auto` div) inside the drawer; the footer is `position: sticky bottom-0` outside the scroll container. - **Keyboard / virtual keyboard**: Vaul handles `window.visualViewport` resize natively so fields scroll into view on iOS without extra work. - **Closing**: tapping the overlay or swiping the drawer down triggers `handleOpenChange(false)` (same logic as the existing dialog close). - **No `isMobile` patches inside DialogContent**: once the drawer handles mobile, the internal `isMobile` guards in `DialogContent`, `DialogFooter`, and the grid class conditional inside `event-dialog.tsx` are removed to keep the desktop path clean. ## Files Changed | File | Change | |------|--------| | `src/components/ui/drawer.tsx` | **New** — shadcn Drawer component (Vaul) | | `src/components/event-dialog.tsx` | Refactor: add step state, conditional Drawer render on mobile | | `src/components/ui/dialog.tsx` | Remove `isMobile` fork from `DialogContent` and `DialogFooter` | ## Out of Scope - Tablet breakpoints (768–1024 px): treated as desktop. - Animations beyond Vaul's built-in slide-up. - Drag-to-resize the drawer (Vaul snap points are a future enhancement). - Any changes to desktop dialog layout or field design.