Files
local-cal/docs/superpowers/specs/2025-05-24-mobile-event-modal-design.md

114 lines
5.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.