Task: 2f9b64dd-0c26-47b9-b2b8-557bdf506d68 Sheet, banner, skeleton and empty-state primitives.
What changed: web/src/ui adds Sheet, ErrorBanner, Skeleton and EmptyState, exported from the UI index, styled with CSS Modules and design tokens only.
Why: Feature 05 task 3. Sheet is a labelled aria-modal dialog with focus move/trap/restore, Escape and scrim close, inert background, 320 ms rise using --ease-out-soft, and no rise under reduced motion. ErrorBanner is a neutral-100 card with accent-700 text and a Retry button. Skeleton is a surface pill with a 1.2 s pulse disabled under reduced motion. EmptyState uses the Bagel heading font and an optional primary action.
Done-condition: Vitest covers focus trap/restore, Escape, scrim close, Retry callback and reduced-motion behaviour. Local npm lint:colors, typecheck, test (16 files, 74 tests) and build passed; gofmt -l printed nothing. Not verified in a real browser.
Base: feature/64e5bb19-c9a2-4c6a-8f0c-aceae43fb732-frontend-foundation at 02f1dd6491.
Task: 2f9b64dd-0c26-47b9-b2b8-557bdf506d68 Sheet, banner, skeleton and empty-state primitives.
What changed: web/src/ui adds Sheet, ErrorBanner, Skeleton and EmptyState, exported from the UI index, styled with CSS Modules and design tokens only.
Why: Feature 05 task 3. Sheet is a labelled aria-modal dialog with focus move/trap/restore, Escape and scrim close, inert background, 320 ms rise using --ease-out-soft, and no rise under reduced motion. ErrorBanner is a neutral-100 card with accent-700 text and a Retry button. Skeleton is a surface pill with a 1.2 s pulse disabled under reduced motion. EmptyState uses the Bagel heading font and an optional primary action.
Done-condition: Vitest covers focus trap/restore, Escape, scrim close, Retry callback and reduced-motion behaviour. Local npm lint:colors, typecheck, test (16 files, 74 tests) and build passed; gofmt -l printed nothing. Not verified in a real browser.
Base: feature/64e5bb19-c9a2-4c6a-8f0c-aceae43fb732-frontend-foundation at 02f1dd6491a072fee429d6d5243dd33bb6a7d8e8.
Sheet is a portalled modal bottom sheet: role="dialog" with aria-modal
and a required accessible name, focus moved in, trapped and restored on
close, Escape and scrim click call onClose, and the rest of <body> is
inert while open. It rises from translateY(100%) over 320 ms
var(--ease-out-soft) over a scrim fading to 50% neutral-900; under
prefers-reduced-motion it opens in place.
ErrorBanner is a muted neutral-100 Card with grape-700 text and a Retry
Button; Skeleton is a --color-surface pill with a 1.2 s opacity pulse
switched off under reduced motion; EmptyState has a Bagel title, body
and an optional primary Button action.
jsdom never matches prefers-reduced-motion, resolves var() colours to
transparent and does not expand the animation shorthand, so the new
src/test/cssRules.ts reads the CSS Modules' declared rules, the
reduced-motion media block and the hashed @keyframes directly.
Maestro task: 2f9b64dd-0c26-47b9-b2b8-557bdf506d68
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
{"verdict":"CHANGES_REQUIRED","reviewed_sha":"1dd97b123c4cbcab91197eb5b4d925b620baa25e","summary":"The primitives meet the styling and interaction requirements inspected, and color lint passes. Typecheck, Vitest and build could not run because web/node_modules is absent. One Sheet focus failure leaves the done-condition unmet.","findings":[{"id":"MQ-36-01","severity":"high","location":"web/src/ui/Sheet.tsx:16-26,46","failure_case":"If the sheet contains a hidden button before a visible control, opening it does not move focus into the dialog. Tab trapping then cannot work from the opener.","evidence":"focusables() selects button:not([disabled]) without excluding hidden or otherwise unfocusable elements. The opening effect calls focus() on the first match and does not check whether focus moved.","suggested_correction":"Select only controls that can actually receive keyboard focus, fall back to focusing the panel, and add a test with a hidden first control."}]}
Posted unaltered by review_agent. This is transport, not a second review.
## Independent review — Codex `gpt-6-sol`, reasoning high
Verdict: **CHANGES_REQUIRED**
Reviewed SHA: `1dd97b123c4cbcab91197eb5b4d925b620baa25e`
```json
{
"verdict": "CHANGES_REQUIRED",
"reviewed_sha": "1dd97b123c4cbcab91197eb5b4d925b620baa25e",
"summary": "The primitives meet the styling and interaction requirements inspected, and color lint passes. Typecheck, Vitest and build could not run because web/node_modules is absent. One Sheet focus failure leaves the done-condition unmet.",
"findings": [
{
"id": "MQ-36-01",
"severity": "high",
"location": "web/src/ui/Sheet.tsx:16-26,46",
"failure_case": "If the sheet contains a hidden button before a visible control, opening it does not move focus into the dialog. Tab trapping then cannot work from the opener.",
"evidence": "focusables() selects button:not([disabled]) without excluding hidden or otherwise unfocusable elements. The opening effect calls focus() on the first match and does not check whether focus moved.",
"suggested_correction": "Select only controls that can actually receive keyboard focus, fall back to focusing the panel, and add a test with a hidden first control."
}
]
}
```
---
Posted unaltered by `review_agent`. This is transport, not a second review.
Review finding MQ-36-01: a hidden first button was chosen as the initial
focus target and as a Tab trap stop, so focus never entered the dialog.
Filter out controls with a negative tabIndex, a hidden/aria-hidden/inert
ancestor, display: none or visibility: hidden; on open, skip any control
whose focus() does not take, falling back to the dialog panel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
{"verdict":"APPROVED","reviewed_sha":"ef3e7d0e4582d9294376f23a733cc4dc69f38cb7","summary":"Static review found the requested primitives and behaviors, including the fixes for hidden controls and disabled fieldsets in the Sheet focus trap. Reduced-motion tests inspect stylesheet rules. `npm run lint:colors` passed. Typecheck, tests, and build could not be run because web/node_modules is absent; no packages were installed.","findings":[]}
Posted unaltered by review_agent. This is transport, not a second review.
## Independent review — Codex `gpt-6-sol`, reasoning high
Verdict: **APPROVED**
Reviewed SHA: `ef3e7d0e4582d9294376f23a733cc4dc69f38cb7`
```json
{
"verdict": "APPROVED",
"reviewed_sha": "ef3e7d0e4582d9294376f23a733cc4dc69f38cb7",
"summary": "Static review found the requested primitives and behaviors, including the fixes for hidden controls and disabled fieldsets in the Sheet focus trap. Reduced-motion tests inspect stylesheet rules. `npm run lint:colors` passed. Typecheck, tests, and build could not be run because web/node_modules is absent; no packages were installed.",
"findings": []
}
```
---
Posted unaltered by `review_agent`. This is transport, not a second review.
approver_agent
merged commit 732aa92a8c into feature/64e5bb19-c9a2-4c6a-8f0c-aceae43fb732-frontend-foundation2026-10-03 18:23:00 -04:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Task: 2f9b64dd-0c26-47b9-b2b8-557bdf506d68 Sheet, banner, skeleton and empty-state primitives.
What changed: web/src/ui adds Sheet, ErrorBanner, Skeleton and EmptyState, exported from the UI index, styled with CSS Modules and design tokens only.
Why: Feature 05 task 3. Sheet is a labelled aria-modal dialog with focus move/trap/restore, Escape and scrim close, inert background, 320 ms rise using --ease-out-soft, and no rise under reduced motion. ErrorBanner is a neutral-100 card with accent-700 text and a Retry button. Skeleton is a surface pill with a 1.2 s pulse disabled under reduced motion. EmptyState uses the Bagel heading font and an optional primary action.
Done-condition: Vitest covers focus trap/restore, Escape, scrim close, Retry callback and reduced-motion behaviour. Local npm lint:colors, typecheck, test (16 files, 74 tests) and build passed; gofmt -l printed nothing. Not verified in a real browser.
Base: feature/64e5bb19-c9a2-4c6a-8f0c-aceae43fb732-frontend-foundation at
02f1dd6491.Independent review — Codex
gpt-6-sol, reasoning highVerdict: CHANGES_REQUIRED
Reviewed SHA:
1dd97b123c4cbcab91197eb5b4d925b620baa25ePosted unaltered by
review_agent. This is transport, not a second review.Independent review — Codex
gpt-6-sol, reasoning highVerdict: APPROVED
Reviewed SHA:
ef3e7d0e4582d9294376f23a733cc4dc69f38cb7Posted unaltered by
review_agent. This is transport, not a second review.