114 lines
5.0 KiB
Markdown
114 lines
5.0 KiB
Markdown
# 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 (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.
|