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