diff --git a/openspec/changes/mobile-responsive-parity/design.md b/openspec/changes/mobile-responsive-parity/design.md new file mode 100644 index 0000000..f6faa07 --- /dev/null +++ b/openspec/changes/mobile-responsive-parity/design.md @@ -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` — 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 { + 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 { + rows: TData[]; + fields: MobileCardField[]; + 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 2–9) + +Each wide-table page renders `` below `md` and the existing +`` at/above `md`. The page wires up the field list. Example for +Media: + +```tsx +const isMobile = useIsMobile(); +const fields: MobileCardField[] = [ + { 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 + ? } /> + : ; +``` + +### 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. diff --git a/openspec/changes/mobile-responsive-parity/proposal.md b/openspec/changes/mobile-responsive-parity/proposal.md new file mode 100644 index 0000000..ac39c17 --- /dev/null +++ b/openspec/changes/mobile-responsive-parity/proposal.md @@ -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 `` 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 | diff --git a/openspec/changes/mobile-responsive-parity/spec.md b/openspec/changes/mobile-responsive-parity/spec.md new file mode 100644 index 0000000..3259498 --- /dev/null +++ b/openspec/changes/mobile-responsive-parity/spec.md @@ -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 3–5 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 `
` 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. diff --git a/openspec/changes/mobile-responsive-parity/tasks.md b/openspec/changes/mobile-responsive-parity/tasks.md new file mode 100644 index 0000000..3f43078 --- /dev/null +++ b/openspec/changes/mobile-responsive-parity/tasks.md @@ -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 | ~2200–2800 | +| Chained PRs recommended | Yes (10 slices) | +| Chain strategy | stacked-to-main | +| Slice order | 1 (primitives) → 2 (Dashboard) → 3–5 (tables) → 6–8 (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 ``, 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 (AC1–AC8), 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 3–5 (tables) and 6–8 (forms) can be reordered or parallelized across + branches if helpful, but each must merge green. +- No slice touches the backend.