# Proposal — Mobile responsive parity

**Change:** `mobile-responsive-parity`
**Phase:** proposal
**Date:** 2026-06-26

## Problem

The Manage frontend ships a responsive **app shell** (hamburger drawer,
`MobileDrawer`, `md:` breakpoint at 768px, correct viewport meta) but the
**content layer** assumes a desktop viewport. Concretely:

1. **Data tables render as literal `<table>` elements with no mobile affordance.**
   Seven tables (Media, FileBrowser, UsersPage, BackupAlertsTable,
   BackupJobsTable, BackupRunsTable, SessionActivityPanel) overflow or clip on a
   375px screen. The Media page's TanStack column-visibility toggle is unusable
   on touch.
2. **Edit forms open in centered `Dialog`s with multi-column grids.** ServicePage
   config, Settings (machines/SSH keys), the message compose dialog, and
   `WidgetConfigDialog` cramp or overflow on phones; save actions drift off-screen.
3. **`HoverEditButton` and row-hover actions do not fire on touch devices.**
   Edit affordances are invisible to phone users.
4. **Touch targets violate mobile accessibility standards.** shadcn defaults
   (32px buttons, dense rows) are below the 44px minimum that WCAG 2.5.5 / Apple
   HIG require for touch.
5. **The Dashboard widget grid does not collapse.** The configurable grid has no
   single-column mobile layout, so a multi-widget dashboard sideways-scrolls or
   clips.

The result: the app **launches** on a phone but cannot be **operated** there.
Several flows (create service, edit widget layout, build media index, manage SSH
keys) are effectively desktop-only.

## Proposal

Make every route fully usable in phone portrait (≥360px) at a single `md:`
(768px) cut. Tablets keep the desktop layout. No desktop-only flows survive.

1. **Hybrid data-table strategy.** The four wide tables (Media, FileBrowser,
   Users, Backups) render a stacked **card per row** below `md`, each card
   picking the 3–5 most important fields. Narrow tables (SessionActivity) keep
   horizontal scroll. The TanStack column-visibility toggle is hidden below `md`
   (the card picks the fields).
2. **Sheet-based edit forms.** Below `md`, ServicePage, Settings, message
   compose, and `WidgetConfigDialog` open inside a full-height `Sheet` (reusing
   the existing primitive) with a sticky header and a sticky save bar — instead
   of the centered `Dialog`.
3. **Replace `HoverEditButton` with an always-visible variant** below `md`. Row
   edit/delete actions surface as small, persistent icon buttons on the right of
   each row/card.
4. **Touch-target audit.** All interactive elements below `md` get a 44px
   minimum hit area (buttons, checkboxes, row taps, badges-as-buttons).
5. **Dashboard mobile layout.** The widget grid collapses to a single column
   below `md`, with a section anchor bar (Observability / Media / Backups /
   Custom) at the top for quick navigation.
6. **Responsive web only.** No PWA, no manifest, no service worker. OIDC keeps
   working in-browser as it does today.
7. **Per-page delivery.** Ship ~9 chained PRs, one per route (plus a primitives
   PR), each ≤400 changed lines, each leaving `npm run lint`, `npm run build`
   (tsc -b + vite build), and `npm run test` green.

## Non-goals

- **No tablet-specific layout.** Tablets use the existing desktop layout at
  `md:` and above.
- **No PWA / installability.** No manifest, service worker, offline mode, or
  standalone display mode. This is a responsive website.
- **No change to polling intervals.** Widget refresh (≈30s) and the
  message-queue poll (5s) keep desktop semantics. (Flagged as a follow-up risk;
  see §Risks.)
- **No new data-table library.** TanStack Table stays; card layouts render from
  the same row data, not from a separate component library.
- **No backend changes.** The API contract is unchanged.
- **No landscape-phone or small-tablet (`sm:`) intermediate layout.** A single
  `md:` cut is the target.
- **No new product features.** This is a presentation-layer parity change.

## Key technical risks

- **TanStack Table → card rendering** is not automatic. Each of the four wide
  tables needs a per-table card variant that picks which fields to show; this is
  where most of the implementation risk and review burden lives.
- **`Sheet` as a form host** is novel in this codebase (currently used only for
  the nav drawer). Sticky header + sticky save bar must work across iOS Safari
  and Chrome Android, including inside the OIDC-triggering keyboard insets.
- **iOS Safari quirks**: viewport `100dvh`, attachment upload from Files,
  `position: sticky` inside transformed ancestors. Each may need targeted fixes.
- **`HoverEditButton` replacement** must not regress the desktop hover-reveal
  aesthetic — only the mobile behavior changes.

## Risks (not blocking, flagged for later)

- **D8 — Polling on battery.** The dashboard (the page most likely to be left
  open on a phone) polls every ~30s per widget plus the 5s queue-status poll.
  Per the decision matrix, intervals stay identical to desktop. Cheapest future
  mitigation: a single `useEffect` on `document.visibilityState` that pauses
  TanStack refetch when the tab is hidden (~10 lines, zero UX cost). Revisit
  after parity ships if battery complaints arise.

## Decision matrix (from grilling)

| # | Decision | Choice |
|---|----------|--------|
| D1 | Parity target | Full parity — no desktop-only flows |
| D2 | Data tables | Hybrid: cards below `md` for the big four; scroll for narrow; toggle hidden |
| D3 | Forms | Full-height `Sheet` below `md`, sticky header + sticky save bar |
| D4 | Touch edit | Always-visible edit button below `md` |
| D5 | Installable | Responsive web only — no PWA |
| D6 | Devices | Phone portrait only, single `md:` (768px) cut |
| D7 | Dashboard | Single-column stack + section anchor bar |
| D8 | Polling | Same intervals as desktop (flagged risk) |
| D9 | Touch targets | 44px minimum below `md` |
| D10 | Testing | Vitest per breakpoint + manual device-mode check |
| D11 | Delivery | Per-page PRs (~9), primitives PR first |
