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