docs: add mobile event modal design spec (Design C — guided drawer)

This commit is contained in:
2026-05-24 21:50:02 -04:00
parent abb472c83d
commit e99a8b44ae

View File

@@ -0,0 +1,113 @@
# 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 → <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` (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 (7681024 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.