Plan mobile responsive parity (OpenSpec change)

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.
This commit is contained in:
Developer
2026-06-26 11:49:47 +00:00
parent 3d331e4c72
commit 18ee77a4e4
4 changed files with 644 additions and 0 deletions
@@ -0,0 +1,167 @@
# Design — Mobile responsive parity
**Change:** `mobile-responsive-parity`
**Phase:** design
**Date:** 2026-06-26
## Context
Frontend stack recap: React 18 + Vite + TanStack Query + TanStack Table +
Tailwind v4 (CSS `@theme` in `src/index.css`) + shadcn/ui (Radix primitives) +
lucide-react + react-router-dom + react-oidc-context. The app shell
(`App.tsx`) is already responsive via a `md:` (768px) cut and a `MobileDrawer`
`Sheet`. The content layer is not.
This design adds four **shared primitives** and applies them per-page. It does
not introduce new libraries.
## Architecture
### Shared primitives (PR 1)
#### 1. `MobileCardRow<T>` — card renderer for TanStack Table rows
Lives in `src/components/ui/mobile-card.tsx` (new). Generic over the row data
type. Reused by the four wide tables.
```tsx
export interface MobileCardField<T> {
key: string;
label: string;
render: (row: T) => React.ReactNode;
/** When true, render as the card title (bold, larger). Exactly one per card. */
primary?: boolean;
}
export interface MobileCardRowProps<T> {
rows: TData[];
fields: MobileCardField<T>[];
onRowClick?: (row: T) => void;
/** Optional right-aligned action slot (edit/delete icon buttons). */
actions?: (row: T) => React.ReactNode;
}
```
Renders a vertical list of cards. Each card shows the `primary` field as the
title and the remaining fields as a key/value stack. The whole card is a button
when `onRowClick` is set (44px min height).
The consuming page decides which fields to show — this primitive does not pick
them.
#### 2. `useIsMobile()` — single source of truth for the breakpoint
Lives in `src/hooks/useIsMobile.ts` (new). Wraps
`matchMedia("(max-width: 768px)")`, SSR-safe, returns a boolean. Replaces the
inline `window.matchMedia` reads in `App.tsx` and the ad-hoc `usePrefersSmallScreen`
usage in `Media.tsx`. One breakpoint, one hook.
```ts
export function useIsMobile(): boolean {
const [isMobile, setIsMobile] = useState(() =>
typeof window !== "undefined" && window.matchMedia("(max-width: 768px)").matches
);
useEffect(() => {
const mql = window.matchMedia("(max-width: 768px)");
const handler = (e: MediaQueryListEvent) => setIsMobile(e.matches);
mql.addEventListener("change", handler);
return () => mql.removeEventListener("change", handler);
}, []);
return isMobile;
}
```
#### 3. `SheetForm` — full-height form host
Lives in `src/components/ui/sheet-form.tsx` (new). Wraps the shadcn `Sheet`
primitive. Props: `open`, `onOpenChange`, `title`, `onSave`, `onCancel`,
`isPending`, `children`. Renders sticky header (`title` + `X`) and sticky
footer (`Cancel` / `Save`). Body scrolls.
Below `md`, used by ServicePage, Settings, message compose, WidgetConfigDialog.
At `md:` and above, the existing `Dialog` is used unchanged. The choice is made
in the consumer with `useIsMobile()`, not inside `SheetForm`, so the same form
body can be reused across both hosts.
#### 4. `EditActionButton` — touch-aware edit affordance
Replaces `HoverEditButton`'s role (not its file — we extend the existing
component). Add a `mobile="always"` prop (default). Below `md`, the button is
always visible (no hover-gated opacity). At `md:` and above, current
hover-reveal behavior is preserved. Implementation: a `md:opacity-0
md:group-hover:opacity-100` Tailwind stack, i.e. always visible by default,
hidden-then-revealed on hover at `md:` and up.
### Per-page application (PRs 29)
Each wide-table page renders `<MobileCardRow>` below `md` and the existing
`<DataTable>` at/above `md`. The page wires up the field list. Example for
Media:
```tsx
const isMobile = useIsMobile();
const fields: MobileCardField<MediaItem>[] = [
{ key: "title", label: "Title", render: (r) => r.title, primary: true },
{ key: "size", label: "Size", render: (r) => r.size_display },
{ key: "hdr", label: "HDR", render: (r) => (r.is_hdr ? "HDR" : "") },
{ key: "library", label: "Library", render: (r) => r.library_name },
];
return isMobile
? <MobileCardRow rows={rows} fields={fields} onRowClick={onRowClick} actions={(r) => <EditActionButton onClick={...} />} />
: <DataTable columns={columns} data={rows} /* ...existing props */ />;
```
### Touch-target audit (PR 1, applied throughout)
A single `min-h-11 min-w-11` (44px) utility class is applied to interactive
shadcn primitives below `md`. Applied via a `mobile-touch-target` Tailwind
utility class registered in `tailwind.config.cjs` (or as a Tailwind v4 CSS
utility in `src/index.css`). The class adds `min-height: 44px; min-width: 44px`
only below `md`:
```css
@media (max-width: 767px) {
.mobile-touch-target,
.mobile-touch-target::before {
min-height: 44px;
min-width: 44px;
}
}
```
Pages add the class to icon buttons, checkboxes, switches, and row taps during
their per-page PR.
## Breakpoints
- `< 768px` (`isMobile === true`): mobile layout — cards, Sheet forms, always-
visible edit, single-column dashboard, anchor bar.
- `≥ 768px`: existing desktop layout, unchanged.
No `sm:` cut. No `lg:` cut.
## Key technical risks & mitigations
- **TanStack column defs vs. card fields drift.** Each page that renders a card
must declare its mobile fields in one place; tests assert the card shows the
primary field at 375px. If a column is renamed, the card test fails.
- **iOS Safari `100dvh`.** `SheetForm` uses `h-[100dvh]` (not `h-screen`) to
avoid the iOS URL-bar resize jump. Tested manually on iOS Safari.
- **`position: sticky` inside `SheetContent`.** Radix `Sheet` uses transforms;
sticky must be relative to the scroll container inside the sheet body, not the
sheet itself. The sticky header/footer are siblings of the scrolling body
inside a flex column, not sticky-positioned.
- **OIDC redirect after login.** No change: responsive web only, OIDC continues
to redirect within the same browser tab.
## Trade-offs
- **Card layouts duplicate field definitions** (once as TanStack columns, once
as `MobileCardField[]`). Accepted: the alternative (auto-deriving cards from
column defs) produces bad mobile UX because column defs are not ordered by
mobile importance.
- **44px touch targets** slightly increase mobile visual density compared to a
32px design, but meet WCAG 2.5.5. Accepted.
- **`useIsMobile()` per-page render branching** is preferred over CSS-only
`hidden md:block` because the card and table have different data dependencies
(e.g. row click handlers, selection state) and mounting both wastes work.
@@ -0,0 +1,114 @@
# 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 35 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 |
@@ -0,0 +1,137 @@
# 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 35 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.
@@ -0,0 +1,226 @@
# Tasks — Mobile responsive parity
**Change:** `mobile-responsive-parity`
**Phase:** tasks
**Date:** 2026-06-26
## Review workload forecast
| Field | Value |
|-------|-------|
| Estimated changed lines | ~22002800 |
| Chained PRs recommended | Yes (10 slices) |
| Chain strategy | stacked-to-main |
| Slice order | 1 (primitives) → 2 (Dashboard) → 35 (tables) → 68 (forms) → 9 (touch audit) → 10 (docs + verify) |
Each slice is committed separately (user pref). Every slice must leave
`cd frontend && npm run lint && npm run build && npm run test` green. Every
touched page gains a Vitest case asserting behavior at <768px and ≥768px.
---
## Slice 1 — Shared primitives
**Goal:** Land the four building blocks every later slice depends on. No
page-level behavior changes yet.
- [ ] **1.1 `useIsMobile()` hook**
- Files: `frontend/src/hooks/useIsMobile.ts` (new)
- Lines: ~20
- Details: SSR-safe `matchMedia("(max-width: 768px)")` listener per design.
- [ ] **1.2 `MobileCardRow` component**
- Files: `frontend/src/components/ui/mobile-card.tsx` (new), plus a Vitest
spec `frontend/src/components/ui/__tests__/mobile-card.test.tsx`.
- Lines: ~80 + ~60 test
- Details: generic `<T,>`, fields list, `primary` field, optional `onRowClick`
and `actions` slot per design. 44px min card height.
- [ ] **1.3 `SheetForm` component**
- Files: `frontend/src/components/ui/sheet-form.tsx` (new), plus spec.
- Lines: ~70 + ~50 test
- Details: wraps shadcn `Sheet`; sticky header + sticky footer; `h-[100dvh]`;
props per design. Dirty-state confirm on outside click.
- [ ] **1.4 `EditActionButton` — extend `HoverEditButton`**
- Files: `frontend/src/components/HoverEditButton.tsx`
- Lines: ~15
- Details: add `mobile="always" | "hover"` (default `always`). Tailwind:
always visible below `md`, hover-revealed at `md:` and up.
- [ ] **1.5 `mobile-touch-target` utility**
- Files: `frontend/src/index.css` (add utility)
- Lines: ~10
- Details: media-gated 44×44 min hit area per design.
- [ ] **1.6 Replace inline `matchMedia` in `App.tsx`**
- Files: `frontend/src/App.tsx`
- Lines: ~10 removed, ~3 added
- Details: use `useIsMobile()`; preserve current shell behavior exactly.
---
## Slice 2 — Dashboard (R7)
**Goal:** Dashboard collapses to single column + section anchor bar on mobile.
- [ ] **2.1 Single-column grid below `md`**
- Files: `frontend/src/pages/Dashboard.tsx`
- Lines: ~20
- Details: widget list uses `grid grid-cols-1 md:grid-cols-*` (match existing
desktop column count). Respect configured sort order.
- [ ] **2.2 Section anchor bar**
- Files: `frontend/src/pages/Dashboard.tsx`
- Lines: ~40
- Details: group widgets (Observability / Media / Backups / Custom). Anchor
bar `md:hidden`, horizontal scroll of pills, jumps to section by id.
- [ ] **2.3 Tests**
- Files: `frontend/src/pages/__tests__/Dashboard.test.tsx`
- Lines: ~40
- Details: assert single column at 375px, grid at 1280px, anchor bar visible
only at <768px.
---
## Slice 3 — Media table (R3.1, R3.5)
- [ ] **3.1 Mobile fields + card render**
- Files: `frontend/src/pages/Media.tsx`
- Lines: ~60
- Details: card primary = title; fields = size, HDR flag, library, year.
Hide column-visibility toggle below `md`. Preserve pagination controls.
- [ ] **3.2 Tests**
- Files: `frontend/src/pages/__tests__/Media.test.tsx`
- Lines: ~40
---
## Slice 4 — FileBrowser table (R3.1)
- [ ] **4.1 Mobile fields + card render**
- Files: `frontend/src/pages/FileBrowser.impl.tsx`
- Lines: ~60
- Details: card primary = name; fields = size, mtime, type. Preserve
directory-navigation tap target (whole card). Preserve ffprobe/job affordances.
- [ ] **4.2 Tests**
- Files: `frontend/src/pages/__tests__/FileBrowser.test.tsx`
- Lines: ~30
---
## Slice 5 — Users + Backups tables (R3.1)
- [ ] **5.1 UsersPage card**
- Files: `frontend/src/pages/UsersPage.impl.tsx`
- Lines: ~70
- Details: card primary = display name; fields = username, activity badge,
email (if present). Preserve selection checkboxes (44px) and drawer open.
- [ ] **5.2 Backups cards (3 tables)**
- Files: `frontend/src/components/BackupAlertsTable.tsx`,
`frontend/src/components/BackupJobsTable.tsx`,
`frontend/src/components/BackupRunsTable.tsx`
- Lines: ~120 (3 × ~40)
- Details: per-table primary + 3 fields; preserve acknowledge/run actions on
the card.
- [ ] **5.3 Tests**
- Files: existing component test files
- Lines: ~90
---
## Slice 6 — ServicePage form (R4)
- [ ] **6.1 Sheet form below `md`**
- Files: `frontend/src/pages/ServicePage.tsx`
- Lines: ~60
- Details: branch on `useIsMobile()`; reuse form body inside `SheetForm`.
Single-column fields. Preserve save semantics.
- [ ] **6.2 Tests**
- Files: `frontend/src/pages/__tests__/ServicePage.test.tsx` (new or extend)
- Lines: ~50
---
## Slice 7 — Settings form (R4)
- [ ] **7.1 Machines + SSH-key editors in Sheet**
- Files: `frontend/src/pages/Settings.tsx`
- Lines: ~100
- Details: both machine editor and SSH-key editor open in `SheetForm` below
`md`. Validate-on-save preserved.
- [ ] **7.2 Tests**
- Files: `frontend/src/pages/__tests__/Settings.test.tsx`
- Lines: ~40
---
## Slice 8 — Message compose + WidgetConfigDialog (R4)
- [ ] **8.1 Message compose Sheet**
- Files: `frontend/src/pages/UsersPage.impl.tsx`
- Lines: ~60
- Details: compose dialog → `SheetForm` below `md`. HTML body textarea + iOS
Safari attachment upload verified manually.
- [ ] **8.2 WidgetConfigDialog Sheet**
- Files: `frontend/src/components/WidgetConfigDialog.tsx`
- Lines: ~60
- Details: reorder list and per-widget config render inside `SheetForm` below
`md`. Sticky save bar.
- [ ] **8.3 Tests**
- Files: extend existing
- Lines: ~60
---
## Slice 9 — Touch-target audit (R6)
- [ ] **9.1 Apply `mobile-touch-target` across routes**
- Files: all 9 pages + shared components (`SessionActivityPanel`,
`ObservabilityPage`, etc.)
- Lines: ~150 (sprinkled)
- Details: icon buttons, checkboxes, switches, badges-as-buttons, row taps.
Manual device-mode pass at 375px logging violations; fix each.
- [ ] **9.2 Audit log**
- Files: this PR description
- Details: list every element touched with before/after hit-area size.
---
## Slice 10 — Docs + verify
- [ ] **10.1 Update `docs/REQUIREMENTS.md`**
- Files: `docs/REQUIREMENTS.md`
- Lines: ~20
- Details: add a Mobile section documenting the breakpoint, card/Sheet
behavior, 44px policy, and the polling follow-up risk.
- [ ] **10.2 Cross-route manual pass**
- Details: walk all 9 routes at 375px (iPhone 12 Pro preset) and at 1280px.
Confirm no regressions; file follow-ups for any iOS Safari quirks found.
- [ ] **10.3 Verify report**
- Files: `openspec/changes/mobile-responsive-parity/verify-report.md`
- Lines: ~80
- Details: per-AC evidence (AC1AC8), tool versions, manual test notes.
---
## Notes
- Each slice's diff should stay well under 400 changed lines. If a slice (e.g.
Settings at ~100 + 40 test) approaches the budget, split along the natural
sub-section boundary.
- Slices 35 (tables) and 68 (forms) can be reordered or parallelized across
branches if helpful, but each must merge green.
- No slice touches the backend.