# Slice 1 — Shared primitives (worker output) Implemented all 6 sub-tasks of Slice 1 (`mobile-responsive-parity`). No page-level files touched. ## Files changed | File | Status | Lines | |------|--------|------| | `frontend/src/hooks/useIsMobile.ts` | new | 31 | | `frontend/src/components/ui/mobile-card.tsx` | new | 112 | | `frontend/src/components/ui/__tests__/mobile-card.test.tsx` | new | 73 | | `frontend/src/components/ui/sheet-form.tsx` | new | 101 | | `frontend/src/components/ui/__tests__/sheet-form.test.tsx` | new | 96 | | `frontend/src/components/HoverEditButton.tsx` | modified | +24 / -8 | | `frontend/src/components/__tests__/HoverEditButton.test.tsx` | modified | +19 | | `frontend/src/index.css` | modified | +16 | | `frontend/src/App.tsx` | modified | +2 / -13 | **Total: ~492 lines** (79 tracked diff + 413 new files). Slightly over the 400-line budget; the overrun is entirely test coverage (mobile-card 73 + sheet-form 96 + hover-edit 19 = 188 test lines) which is the safety net for all 9 downstream page slices. Implementation-only lines are ~304, within budget. Flagging for parent decision; I did not trim tests since they guard later slices. ## Validation ``` cd frontend && npm run lint → 0 errors, 2 pre-existing warnings (UsersPage.impl.tsx, unrelated) cd frontend && npm run build → ✓ built in 3.49s (tsc -b + vite) cd frontend && npm run test → 83 passed (25 files) ``` - `useIsMobile` — no dedicated test (it's a thin matchMedia wrapper exercised by App.tsx integration); the page-slice tests will assert <768px/≥768px behavior. - `MobileCardRow` — 4 tests (primary+fields render, onRowClick fires, actions slot, non-interactive mode). - `SheetForm` — 5 tests (title+children, onSave, onCancel, isPending disables+labels, close-X calls onCancel). - `HoverEditButton` — 4 tests (existing 2 + default mobile=always tokens + legacy mobile=hover tokens). ## Deviations from design 1. **`MobileCardRow` key strategy**: design pseudocode used `MobileCardRowProps` with `rows: TData[]` (a typo — `TData` undefined). Implemented as `rows: T[]` (correct generic). Also added an optional `className` prop on the outer container — minor additive convenience, not a behavior change. 2. **`MobileCardRow` field rendering**: design said "key/value stack"; I used a `
` with `grid-cols-[auto_1fr]` so labels align across rows. Same semantics, cleaner alignment. 3. **`HoverEditButton` default class**: added `mobile-touch-target` to the button so it meets 44px below md out of the box (consistent with spec R6). Design did not name this class explicitly here but R6/R9 require it on all interactive elements; this primitive is reused by later slices so it should be compliant by default. 4. **`SheetForm` side**: used `side="bottom"` with `h-[100dvh]` for a true full-screen mobile form. Design said "side=bottom or side=right, full screen"; bottom is the more native mobile form factor and avoids the `sm:max-w-sm` cap on side=right from the Sheet primitive. 5. **`SheetForm` close button**: design said "title + close X"; I render the X via the Cancel handler (X = cancel) rather than Radix's `onOpenChange(false)`, so dirty-state confirm logic (spec R4.5) can be centralized in the consumer's `onCancel`. Documented in the component docstring. No other deviations. All shadcn primitives, `cn()`, lucide-react icons, and existing code style (tabs, `data-slot` where relevant) matched. ## skill_resolution `none` — no project/user SKILL.md paths were injected by the parent, and no `.atl/skill-registry.md` was provided. The task was self-contained against the OpenSpec design/tasks docs. ## Residual risks - **jsdom doesn't honor `@media`**: the `useIsMobile` hook returns whatever `window.matchMedia` reports in jsdom (default false). Per-page breakpoint tests in later slices will need to mock `matchMedia` or use the existing resize pattern. Not a Slice 1 blocker. - **`h-[100dvh]` iOS Safari**: per design risk note; needs manual verification on a real iOS device during Slice 10 (manual cross-route pass). The flex-column layout avoids the sticky-inside-transform pitfall. - **`HoverEditButton` consuming pages** (Actions, Settings) use `.rail-edit` hover CSS (`&:hover .rail-edit { opacity: 1 }`). With the new default (`mobile="always"`), the button is visible below md and hover-revealed at md+ — desktop behavior unchanged because the md:-prefixed classes take over at ≥768px. No migration needed on those pages for Slice 1; they keep working as-is. ## Review findings No blockers. One item for the parent reviewer to confirm: - Total diff ~492 lines exceeds the 400-line slice budget by ~92 lines, entirely due to additive tests. Acceptable for a foundational primitives slice, but the parent may prefer to split or trim test prose. ## Manual notes - `git status` confirms nothing is staged; all changes are unstaged/untracked, ready for the parent to review and commit. - The `swap-pane` untracked file at repo root is pre-existing and unrelated; not touched.