# Spec — Mobile responsive parity **Change:** `mobile-responsive-parity` **Phase:** spec **Date:** 2026-06-26 ## Scope All 9 application routes must be fully operable in phone portrait viewports (≥360px) at a single `md:` (768px) breakpoint. Tablets and wider viewports keep the existing desktop layout unchanged. No product behavior changes; this is a presentation-layer parity change only. ## Requirements ### R1 — Viewport & breakpoint policy - R1.1 The viewport meta stays `width=device-width, initial-scale=1.0` (no zoom lock). User zoom remains enabled. - R1.2 There is exactly one responsive cut: `md:` (768px). Below is "mobile"; at-or-above is "desktop" (existing behavior). - R1.3 No `sm:` intermediate cut is introduced. ### R2 — App shell (already compliant; locked in) - R2.1 Desktop `Sidebar` renders `null` when `isMobile` (`matchMedia("(max-width: 768px)")`). - R2.2 Mobile nav uses the existing `MobileDrawer` (hamburger, `md:hidden`, `Sheet` side=left) with no behavioral change. - R2.3 `TopBar` keeps its existing responsive behavior (version badges hidden on small screens, hamburger visible below `md`). ### R3 — Data tables (hybrid) - R3.1 The four wide tables — **Media** (`pages/Media.tsx`), **FileBrowser** (`pages/FileBrowser.impl.tsx`), **Users** (`pages/UsersPage.impl.tsx`), and the three **Backups** tables (`BackupAlertsTable.tsx`, `BackupJobsTable.tsx`, `BackupRunsTable.tsx`) — render a stacked **card per row** below `md`. - R3.2 Each card shows a primary title plus the 3–5 most important fields for that table (chosen per-table; documented in tasks). All remaining fields are omitted from the mobile card. - R3.3 Row click / selection semantics are preserved on the card (tap target = the whole card where applicable). - R3.4 **SessionActivityPanel** (narrow, 3-column) keeps the `` shape inside a horizontal-scroll container below `md`. - R3.5 The TanStack **column-visibility toggle is hidden below `md`** on every table that uses it (Media). The mobile card picks the fields; the user does not re-show hidden columns on touch. - R3.6 At `md:` and above, all tables render exactly as today. ### R4 — Edit forms (Sheet) - R4.1 Below `md`, these edit flows open in a full-height `Sheet` (side=bottom or side=right, full screen) instead of a centered `Dialog`: - **ServicePage** connection config + secrets - **Settings** machines and SSH-key editors - **Message compose** dialog (`UsersPage.impl.tsx`) - **WidgetConfigDialog** - R4.2 The Sheet form has a sticky header (title + close affordance) and a sticky footer/save bar (Cancel + Save). - R4.3 Form fields stack to a single column inside the Sheet. - R4.4 At `md:` and above, the existing `Dialog`-based forms are unchanged. - R4.5 The Sheet closes on successful save and on explicit cancel; it does not close on outside-click while the form is dirty (confirm prompt). ### R5 — Touch edit affordance - R5.1 `HoverEditButton` gains a `md:` variant: hover-revealed on desktop (unchanged), **always visible** below `md`. - R5.2 Row/card edit and delete actions surface as persistent icon buttons on the right edge below `md`. - R5.3 Desktop hover-reveal aesthetic is not regressed at `md:` and above. ### R6 — Touch targets - R6.1 All interactive elements below `md` have a minimum 44×44px hit area. This includes: buttons, icon buttons, checkboxes, switches, row/card tap targets, and badges that act as buttons. - R6.2 Visual size may remain smaller than 44px (padding-only hit areas are acceptable) as long as the tappable region meets the minimum. - R6.3 At `md:` and above, sizes are unchanged. ### R7 — Dashboard layout - R7.1 The widget grid collapses to a **single column** below `md`. - R7.2 A **section anchor bar** appears at the top of the dashboard below `md`, grouping widgets (e.g. Observability / Media / Backups / Custom) and allowing quick jump-to-section. - R7.3 Widget order respects the user's configured sort order. - R7.4 At `md:` and above, the grid renders exactly as today. ### R8 — Polling (unchanged) - R8.1 Widget refresh intervals and the message-queue poll interval are identical on mobile and desktop. - R8.2 (Follow-up risk, not in scope: pause refetch on `document.visibilityState === "hidden"`. Tracked in proposal §Risks.) ### R9 — No PWA - R9.1 No web manifest, service worker, or standalone display mode is added. - R9.2 OIDC continues to work in-browser; no standalone-mode redirect handling is introduced. ### R10 — Non-regression - R10.1 No desktop layout (≥768px) is visually or functionally regressed. - R10.2 No backend API contract change. - R10.3 No existing test is deleted; mobile-specific tests are additive. ## Acceptance criteria - AC1 Every route listed in `App.tsx` `navItems` (Dashboard, Observability, Media, Files, Backups, Users, Actions, Services, Settings) is fully operable at 375px width in Chrome DevTools device mode (iPhone 12 Pro preset or equivalent). - AC2 Each of the four wide tables shows a card layout at 375px and the table layout at 1280px. - AC3 Each of the four edit forms opens in a Sheet at 375px and a Dialog at 1280px. - AC4 `HoverEditButton` is always visible at 375px and hover-revealed at 1280px. - AC5 A 44px-minimum touch-target audit passes for all interactive elements at 375px. - AC6 The Dashboard renders a single column with an anchor bar at 375px and the existing grid at 1280px. - AC7 `cd frontend && npm run lint && npm run build && npm run test` is green. - AC8 At least one Vitest test per touched page asserts behavior at <768px and ≥768px breakpoints. ## Non-goals - Tablet/landscape/sm: intermediate layout. - PWA, manifest, service worker, offline mode. - Polling-interval changes. - Backend changes. - New data-table library. - New product features.