5.0 KiB
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:
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
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 → <Dialog> (unchanged)
└── isDesktop=false → <Drawer>
├── 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(oroverflow-y-autodiv) inside the drawer; the footer isposition: sticky bottom-0outside the scroll container. - Keyboard / virtual keyboard: Vaul handles
window.visualViewportresize 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
isMobilepatches inside DialogContent: once the drawer handles mobile, the internalisMobileguards inDialogContent,DialogFooter, and the grid class conditional insideevent-dialog.tsxare 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.