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