From e99a8b44aec5de5f1d8d9975880b531f03d5d6f3 Mon Sep 17 00:00:00 2001 From: Dmytro Stanchiev Date: Sun, 24 May 2026 21:50:02 -0400 Subject: [PATCH] =?UTF-8?q?docs:=20add=20mobile=20event=20modal=20design?= =?UTF-8?q?=20spec=20(Design=20C=20=E2=80=94=20guided=20drawer)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2025-05-24-mobile-event-modal-design.md | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 docs/superpowers/specs/2025-05-24-mobile-event-modal-design.md diff --git a/docs/superpowers/specs/2025-05-24-mobile-event-modal-design.md b/docs/superpowers/specs/2025-05-24-mobile-event-modal-design.md new file mode 100644 index 0000000..4d60eb9 --- /dev/null +++ b/docs/superpowers/specs/2025-05-24-mobile-event-modal-design.md @@ -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 → (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.