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