Commit Graph

3 Commits

Author SHA1 Message Date
Developer 09b9c45665 SheetForm dirty-state confirm + wire isDirty into all form consumers (R4.5)
SheetForm gains an isDirty prop. When true, any close attempt (Cancel
button, header X, Radix overlay click, Escape) opens a 'Discard changes?'
ConfirmDialog instead of discarding unsaved edits. Radix dismiss callbacks
(onEscapeKeyDown, onPointerDownOutside) are intercepted when dirty so the
guard applies uniformly.

All four form consumers now compute and pass isDirty:
- ServicePage: name/enabled/config differ from the persisted instance.
- Settings machine editor: field-by-field draft vs editingMachine
  (create mode is always dirty; secret write-only fields excluded).
- Message compose: subject non-empty, body differs from default, or
  attachments present.
- WidgetConfigDialog: draft !== null (only draft mode is guarded; list
  mode has nothing to discard).

Tests: 3 new SheetForm dirty-guard cases (prompt on cancel, abort discard,
clean close when not dirty) + one focused dirty-guard test per consumer.
122 tests pass; lint/build green.

Refs openspec/changes/mobile-responsive-parity/verify-report.md residual
risk #1.
2026-06-26 15:31:30 +00:00
Developer e805c624b2 Mobile Settings: machine editor SheetForm (Slice 7)
Below md, the machine editor Dialog renders as a SheetForm (triggered by
the same Edit/Add buttons via machineDialogOpen state). The shared
MachineEditor body (fields + SSH validate button) renders inside the
sheet; the ConfirmDialog is a sibling outside. Desktop Dialog is
byte-for-byte identical.

No navigation needed on close -- the Settings page content (tabbed cards,
machine list) is always visible behind the sheet, so there is no stranding
risk (unlike ServicePage where the sheet was the whole page).

Added saveDisabled prop to SheetForm (additive, default false) so the
machine editor can gate Save on required fields (name + host for SSH
mode), matching the desktop DialogFooter confirmDisabled semantics.

Scope note: SSHKeyManager is an inline two-panel layout (SelectionRailCard
+ SectionCard), not a dialog, and already stacks responsively via
grid-cols-1 md:grid-cols-[...]. Wrapping it in SheetForm would break its
always-visible selection rail. Left as-is.

Tests: 3 new mobile cases (SheetForm render, save payload, cancel closes)
+ desktop unchanged. 113 tests pass; lint/build green.

Refs openspec/changes/mobile-responsive-parity/ (spec R4, tasks slice 7).
2026-06-26 13:56:27 +00:00
Developer 688a18af22 Add mobile responsive primitives (Slice 1)
Foundation for the mobile-responsive-parity change. Adds:
- useIsMobile() hook: single source of truth for the md:768px cut (SSR-safe)
- MobileCardRow<T>: stacked card list for wide tables below md, with getRowId
  stable keys, primary field as title, optional onRowClick + actions slot
- SheetForm: full-height form host (h-[100dvh], flex column, sticky header +
  footer via flex not position:sticky) for mobile edit flows
- HoverEditButton: mobile prop (default 'always') -- always visible below md,
  hover-revealed at md+; desktop aesthetic preserved
- .mobile-touch-target CSS utility: 44x44 min hit area below md (WCAG 2.5.5)
- App.tsx refactored to use useIsMobile(); shell behavior unchanged

Tests cover primary/field rendering, onRowClick, actions slot, empty rows,
no-primary, stable keys (no duplicate-key warning), and all SheetForm
interactions. 86 tests pass; lint/build green.

MobileCardRow key strategy: uses getRowId when provided (falls back to index);
per design §trade-offs, fields are declared per-table to prioritize by mobile
importance rather than auto-derived from column defs.

Refs openspec/changes/mobile-responsive-parity/ (design §Shared primitives,
spec R1/R5/R6, tasks slice 1).
2026-06-26 12:09:21 +00:00