# Slice 1 Review — `mobile-responsive-parity` (Shared primitives) Reviewer: fresh adversarial pass. Date: 2026-06-26. Scope: primitives only (useIsMobile, MobileCardRow, SheetForm, HoverEditButton extension, mobile-touch-target CSS, App.tsx refactor). No page-level changes. ## Commands run (all green) | Command | Result | |---|---| | `cd frontend && npm run lint` | pass (0 errors; 2 pre-existing warnings in `UsersPage.impl.tsx`, untouched by this slice) | | `cd frontend && npm run build` | pass (tsc + vite; 1975 modules) | | `cd frontend && npm run test` | pass (25 files / 83 tests) | No staged files (`git diff --cached` empty). Unstaged: App.tsx, HoverEditButton.tsx, HoverEditButton.test.tsx, index.css. Untracked: useIsMobile.ts, mobile-card.tsx, mobile-card.test.tsx, sheet-form.tsx, sheet-form.test.tsx. --- ## Correct (with evidence) - **useIsMobile matches design.** `MOBILE_QUERY = "(max-width: 768px)"` is the same query the old inline `App.tsx` code used; SSR guard added (`typeof window !== "undefined"`); listener add/remove correct. `frontend/src/hooks/useIsMobile.ts:4,12-23`. - **App.tsx refactor is behavior-preserving.** The inline `useState`+`useEffect` block is replaced 1:1 by `useIsMobile()`; `Sidebar` still receives the same boolean and renders `null` when mobile (`App.tsx:110`, `isMobile` → `null`); margin-left branch and `MobileDrawer`/`TopBar` untouched. `App.tsx:317-331`. - **HoverEditButton default (`mobile="always"`) is correct and non-regressive.** Default stack `md:opacity-0 md:transition-opacity md:duration-100 md:ease-out md:group-hover:opacity-100` → always visible below `md`, hover-revealed at `md:`+. Legacy `&:hover .rail-edit { opacity: 1 }` CSS in Actions/Settings still resolves (specificity 0,2,0 beats the `md:opacity-0` utility 0,1,0), so desktop hover-reveal is doubly guaranteed. `HoverEditButton.tsx:43-47`. `mobile="hover"` restores the old `opacity-0 … group-hover:opacity-100`. - **mobile-touch-target CSS is correctly scoped.** `@media (max-width: 767px)` aligns exactly with Tailwind `md:` (min-width: 768px); the rule is unlayered plain CSS so it outranks Tailwind's layered `min-h-*` utilities on mobile and is inert at `md:`+. `index.css:113-118`. - **SheetForm layout matches design.** Flex column (`flex h-[100dvh] … flex-col gap-0 p-0`), header `shrink-0`, body `flex-1 overflow-y-auto`, footer `shrink-0` — sticky achieved via flex, not `position: sticky` (correct, given Radix Sheet uses transforms). `h-[100dvh]` not `h-screen`. Close (X) wired to `onCancel`. `showCloseButton={false}` avoids a duplicate Radix close button. `sheet-form.tsx:36-75`. - **SheetForm accessibility.** Uses `SheetTitle` (satisfies Radix Dialog's required title). `sheet-form.tsx:46-48`. - **TypeScript / generics.** `MobileCardRow` as a function declaration is valid in `.tsx` (the `` disambiguation rule only applies to arrow functions). No `any`; `MobileCardField.render: (row: T) => ReactNode`. Build is clean. - **HoverEditButton tests guard the actual mechanism** (class composition), not just rendering — asserts `md:opacity-0`/`md:group-hover:opacity-100` present and standalone `opacity-0` absent for the default, and the inverse for `mobile="hover"`. `HoverEditButton.test.tsx:22-40`. - **SheetForm tests cover behavior**: save, cancel, close→onCancel, isPending disables Save + shows "Saving…". `sheet-form.test.tsx`. --- ## Confirmed issues (must-fix before commit) ### B1 — Duplicate React keys in `MobileCardRow` (all rows share one key) `frontend/src/components/ui/mobile-card.tsx:60` and `:75`: ```tsx rows.map((row, index) => { ... return