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

5.0 KiB
Raw Permalink Blame History

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