# Slice 5 Review — Users + Backups mobile card layout
**Change:** `mobile-responsive-parity` · **Slice:** 5 (Users + Backups tables)
**Reviewer mode:** fresh adversarial · **Date:** 2026-06-26
**Verdict: fix-then-commit** (one real HTML-validity issue; everything else clean)
---
## Commands run (all green)
| Command | Result | Notes |
|---|---|---|
| `npm run lint` | ✅ 0 errors | Only 2 pre-existing `react-hooks/exhaustive-deps` warnings in `UsersPage.impl.tsx` (lines 149/188). Verified identical on `HEAD` (lines 120/159) — **not introduced by this slice**. |
| `npm run build` | ✅ built | `tsc -b` typecheck clean (build runs it). |
| `npm run test` | ✅ 25 files / 102 tests | All pass. |
| `git diff --cached --stat` | empty | No staged files. |
---
## 1. `useIsMobile` hardening — SAFE ✅
`frontend/src/hooks/useIsMobile.ts`: added `typeof window.matchMedia === "function"` to both the lazy `useState` initializer and the `useEffect` guard.
- In real browsers `window.matchMedia` is always a function, so the added predicate is always `true` and **behavior is identical**.
- In jsdom (no native `matchMedia`) the change converts a hard `TypeError` ("window.matchMedia is not a function") into a graceful `false` (desktop). This is strictly safer — it can only turn a crash into a non-crash.
- All existing consumers (`App.tsx`, `Dashboard`, `Media`, `FileBrowser`) already stub `matchMedia` in their test files, so their tests are unaffected. Confirmed no regression path for slices 1–4.
**Conclusion:** safe across all consumers; no regression.
---
## 2. UsersPage desktop Table — preserved EXACTLY ✅
I programmatically extracted the `
…
` block from `HEAD` and from the working tree and diffed them token-for-token.
- The only differences are **line re-wrapping** caused by deeper indentation (e.g. the `checked || selectedUser?.jellyfin_id === row.jellyfin_id` expression and `onClick={() => setSearchParams({ user: row.jellyfin_id })}` wrap onto more lines). Every token, attribute, and child is present in both.
- **Columns preserved (10):** select-all checkbox header, User, Email, Activity, Type (hidden md), Jellyseerr, Role (hidden md), Permissions, Reqs (hidden md), Contact (hidden md).
- **Header checkbox** `toggleVisibleSelection` — preserved.
- **Row checkbox** `toggleUserSelected` + `onClick={(event) => event.stopPropagation()}` + `aria-label` — preserved.
- **Row click** `onClick={() => setSearchParams({ user: row.jellyfin_id })}` — preserved.
- **Avatar** (`AvatarImage`/`AvatarFallback`), username fallback, all `` variants, `data-state="selected"`, `cursor-pointer` — all preserved.
**Conclusion:** the desktop branch is the original table re-indented one level deeper into the `: (` else arm. No prop, column, or handler was dropped. Diff stat (207 ins / 149 del) is dominated by this re-indentation; the true behavioral delta is small (cards branch + `userCardFields` + `useComposeViewport` rename).
---
## 3. Compose hook rename — correct ✅
The file-local 900px `useIsMobile` was renamed `useComposeViewport`; the shared 768px `useIsMobile` (from `hooks/`) now drives the directory-table branch.
- `isComposeMobile` (900px) → used **only** at `UsersPage.impl.tsx:827` for the compose `DialogContent` full-screen class. Breakpoint unchanged (`(max-width: 900px)`).
- `isMobile` (768px) → used **only** at `UsersPage.impl.tsx:511` for the table/card branch.
- Verified no stray references to the old local name remain (`grep` confirms 2 distinct symbols, correctly wired).
---
## 4. Backups cards (3 components) ✅
- **BackupAlertsTable:** mobile branch renders `MobileCardRow` with primary=message + severity/type/created; **Ack action preserved** in the `actions` slot (`mobile-touch-target`, calls `onAcknowledge(a.id)`; hidden when `acknowledged`). Desktop `
` block is byte-identical (diff is purely additive before the `return`).
- **BackupJobsTable:** mobile branch builds `JobCardRow[]` (joins `latestRuns` exactly as the desktop row does) with primary=name + source/schedule/last-status. No per-row action exists in the desktop original, so none is "lost". Desktop table unchanged.
- **BackupRunsTable:** status-filter `