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:
@@ -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 2–9)
|
||||
|
||||
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 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 |
|
||||
@@ -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 `<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 | ~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.
|
||||
Reference in New Issue
Block a user