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.
115 lines
5.9 KiB
Markdown
115 lines
5.9 KiB
Markdown
# 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 |
|