18ee77a4e4
Add proposal/spec/design/tasks for full mobile parity across all 9 routes. Decisions: hybrid tables (cards below md for big four), Sheet-based forms, always-visible edit affordance, 44px touch targets, single-column dashboard with anchors, responsive web only (no PWA), phone portrait at md:768px cut. Polling unchanged (risk flagged). Delivery: 10 chained PRs, primitives first.
5.9 KiB
5.9 KiB
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:
- 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. - Edit forms open in centered
Dialogs with multi-column grids. ServicePage config, Settings (machines/SSH keys), the message compose dialog, andWidgetConfigDialogcramp or overflow on phones; save actions drift off-screen. HoverEditButtonand row-hover actions do not fire on touch devices. Edit affordances are invisible to phone users.- 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.
- 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.
- 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 belowmd(the card picks the fields). - Sheet-based edit forms. Below
md, ServicePage, Settings, message compose, andWidgetConfigDialogopen inside a full-heightSheet(reusing the existing primitive) with a sticky header and a sticky save bar — instead of the centeredDialog. - Replace
HoverEditButtonwith an always-visible variant belowmd. Row edit/delete actions surface as small, persistent icon buttons on the right of each row/card. - Touch-target audit. All interactive elements below
mdget a 44px minimum hit area (buttons, checkboxes, row taps, badges-as-buttons). - 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. - Responsive web only. No PWA, no manifest, no service worker. OIDC keeps working in-browser as it does today.
- 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), andnpm run testgreen.
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 singlemd: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.
Sheetas 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: stickyinside transformed ancestors. Each may need targeted fixes. HoverEditButtonreplacement 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
useEffectondocument.visibilityStatethat 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 |