Four fixes:
1. Mobile edit fullscreen scroll: the Sheet primitive's
data-[side=bottom]:h-auto was overriding our h-[100dvh] on
SheetForm, preventing scroll. Added data-[side=bottom]:h-[100dvh]
to the SheetForm className to win the specificity battle.
2. Direct-edit close showed list view: when opened via editWidgetId
(the hover edit button), saving or canceling called reset() which
showed the widget list instead of closing the dialog. Now derives
directEdit from editWidgetId — when true, reset() calls onClose()
to close entirely.
3. Copy button missing on mobile: MobileWidgetSections didn't pass
onCopy to its WidgetInstanceCard instances. Now accepts and wires
onCopyWidget, so referenced widgets show the copy/detach button on
mobile too.
4. Service enabled badge stale: ServiceConfigEditor showed
instance.enabled (the initial prop) instead of the local enabled
state. Now reads the local enabled variable so the badge updates
when the user toggles the switch.
128 tests pass; 0 lint errors; build clean.
Five fixes:
1. Dialog mobile scroll: DialogContent now has max-h-[calc(100dvh-2rem)]
overflow-y-auto so dialogs that don't fit on screen can scroll
instead of clipping their footer (and Cancel button) off-screen.
2. isDirty false positive: WidgetConfigDialog's SheetForm used
isDirty={draft !== null} which was true the moment you opened edit
mode, even with no changes. Now stores a draftBaseline at startEdit
time and compares JSON.stringify(draft) !== JSON.stringify(baseline).
The discard-confirmation only appears when something actually changed.
3. Mobile edit button always visible: the widget card's edit button was
opacity-0 group-hover:opacity-100 (hover-only). Changed to
md:opacity-0 md:group-hover:opacity-100 — always visible below md,
hover-reveal at md+.
4. Copy button for referenced widgets: WidgetInstanceCard gains an onCopy
prop. On the Dashboard, referenced widgets get a Copy icon button that
triggers detachRef (creates an independent clone). The edit button on
referenced widgets edits the original (shared config).
5. ConfirmDialog Cancel: fixed by #1 (the Cancel button was off-screen
on mobile dialogs that couldn't scroll).
128 tests pass; lint/build green.
The shared Tabs primitive used data-horizontal:* / data-vertical:* Tailwind
variants, but the component sets data-orientation='horizontal' (not
data-horizontal). Tailwind v4 data-* variants match attribute names, so
data-horizontal:flex-col on the Tabs root never applied -- the TabsList
and TabsContent laid out side-by-side instead of stacking.
Other consumers (TabbedCard, Settings) wrap their tab children in <div>s,
so the broken flex direction was masked. ServicePage puts TabsList and
TabsContent as direct children of <Tabs>, exposing the bug.
Fix: switch every dead variant to data-[orientation=horizontal]:* /
data-[orientation=vertical]:* (the root flex-col, the list h-8/h-fit/
flex-col, the trigger w-full/justify-start, and the active-indicator
after-element positioning). The full orientation system now works as
intended for both horizontal and vertical tabs.
117 tests pass; lint/build green.
Pull the duplicated pagination footer into a single shared component at
frontend/src/components/ui/table-pagination.tsx. Both the desktop
DataTable (which had an internal DataTablePagination driven by a TanStack
table instance) and the Media mobile card list (which had a standalone
MediaMobilePagination driven by raw PaginationState) now consume it.
The shared component takes the raw primitives (pageIndex, pageSize,
pageCount, totalRows, pageSizeOptions, onPaginationChange, optional
className) so it backs both an adapter view (DataTable extracts state
from its table instance and passes table.setPagination) and a direct
state view (Media passes its pagination state directly). Includes the
44px mobile-touch-target on prev/next buttons (previously only on the
Media mobile variant).
Removes ~90 lines of duplication across data-table.tsx and Media.tsx;
adds the focused 122-line shared component. The DataTable Select imports
are dropped (now unused). 122 tests pass; lint/build green.
Refs openspec/changes/mobile-responsive-parity/verify-report.md residual
risk #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.
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).
Below md, the Users directory and the three Backups tables render as
MobileCardRow cards:
- UsersPage: display name primary; username/activity/email fields. Each
card carries a selection checkbox (44px via mobile-touch-target) in the
actions slot with stopPropagation so toggling selection does not open
the drawer; card-body tap still opens the drawer.
- BackupAlertsTable: alert message primary; severity/type/created fields;
Acknowledge action preserved in actions slot.
- BackupJobsTable: job name primary; source/schedule/last-status fields
(joins latestRuns into a JobCardRow).
- BackupRunsTable: run job_id primary; status/duration/size/started fields;
status-filter Select renders above both layouts (preserved on mobile).
Desktop (md+) is byte-for-byte identical for all four components -- the
UsersPage diff is dominated by re-indenting the existing Table into the
isMobile ternary else branch.
Fix from Slice 5 review: MobileCardRow now renders the clickable card as
<div role=button tabIndex=0> with Enter/Space keyboard handling instead
of <button>, so nesting a Radix Checkbox (which renders a <button>) in
the actions slot produces valid HTML. The desktop-parity argument for
<button>-in-<button> did not hold (desktop rows are <tr>, not buttons).
Cross-cutting: useIsMobile hardened with typeof window.matchMedia guard
(safe in real browsers; only changes jsdom crash -> false). The file-local
900px compose hook was renamed useComposeViewport to avoid collision with
the shared 768px useIsMobile.
Tests: BackupJobsTable test file added (was untested), UsersPage mobile
selection round-trip + stopPropagation, mobile card render across all
four components. 105 tests pass; lint/build green.
Refs openspec/changes/mobile-responsive-parity/ (spec R3, tasks slice 5).
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).
- Archive the completed observability-service-registry SDD change into
openspec/changes/archive/ (delivered across 5 slices; only
jellyfin-service-registry remains active).
- Stop ignoring .pi-map.md / .pi-map.index.md so the navigation maps are
versioned alongside the code, and add the regenerated map pairs repo-wide.