Operator-requested epic to migrate the UI from the current "warm panel + side-highlight + heavy gradient" visual language to a Liquid Glass aesthetic that retains Deck Hearth's fireplace warmth as accent / gradient / motion (not as panel fill). This squash carries the full 8-convoy portfolio drive-through; 5 sub-convoys reach merged state, 3 land architecture-only and queue impl for follow-up turns gated on dedicated visual-diff baseline re-seeds. Sub-convoy #1 (liquid-glass-design-tokens) — MERGED. 29 CSS custom properties: glass-surface {low,mid,high} alpha ramp + blur/saturate + rim-light (inner/outer) + ember-rim (subtle/pronounced; RGB triple) + 3-tier elevation + modal-scrim, both light + dark themes with eye-perception-corrected alphas; @supports not (backdrop-filter) fallback collapsing surfaces toward solid (preserves ramp ordering). Authored docs/DESIGN_TOKENS.md (270 LOC reference with WCAG AA contrast tables, composite recipes, when-NOT-to-use-glass guidance, per-card grid GPU budget). AGENTS.md gains a § Visual language section as the new agent-contract surface. Sub-convoy #2 (liquid-glass-modal-and-surface-primitive) — Brief 1 MERGED. Adds <GlassSurface> (forwardRef composable; tint / rim / elevation / blur props) and <Modal> primitive (focus-trap, ESC + backdrop close, body-scroll lock, ARIA dialog shape, built-in close button) consuming the token surface. lib/use-focus-trap.js — homegrown hook (~60 LOC, no dep). 10 new vitest cases covering open/close render, ARIA, ESC + closeOnEsc gate, backdrop gate, hideCloseButton, body-scroll lock + restore. 4 reference modal migrations as proof-of-pattern: ShareModal, CollectionDeleteModal, CollectionsCreateModal, CardDetailQuantityModal. Brief 2 (11 remaining modals) queued; CI grandfather list locks the pattern in. Sub-convoy #3 (liquid-glass-form-primitives) — Brief 1 MERGED. Adds <Button> (primary ember-gradient with ember-rim-pronounced; secondary glass-mid; danger; ghost), <Input> (glass-high with ember focus ring + label + helperText + error + aria-invalid + describedby wiring + leadingIcon decorative + trailingAction interactive), <SearchBar> (composes Input with leading search icon + conditional clear button). 10 new vitest cases. pages/login.js + pages/signup.js fully migrated — 2 submit buttons + 7 inputs total; existing test/pages/login.test.js assertion ("Sign in to Deck Hearth" button text) preserved. Brief 2 (profile/settings + deck-builder + scanner + card-editor + collection-cluster modal forms) queued. Sub-convoy #4 (liquid-glass-layout-shell) — MERGED. 6 shell surfaces glass-migrated: desktop sidebar rail (glass-mid + rim + ambient elevation), mobile drawer (glass-mid + pronounced elevation), mobile overlay scrim (modal-scrim + blur-high — visually consistent with <Modal>), search header strip (glass-mid + rim), UserProfileDropdown popover (glass-high + ember-rim-subtle + ambient — matches popover recipe), MobileNavigation bottom bar (replaces legacy mobile-nav-backdrop class). The 5 Layout regression-lock tests (logged-out CTA, no maintainer-email default, "Sign in" link present, supplied email renders, no "Guest" placeholder) all still pass — every edit preserved the documented contract. Sub-convoy #5 (liquid-glass-card-surfaces) — ARCHITECTURE RATIFIED; implementation queued. Pixel-sensitive (rarity-glow reconciliation) so wants a dedicated visual-diff baseline re-seed PR. Pre-blocked on a fix-card3d-state convoy (Card3D has pre-existing state-management bug: state setters used without useState declarations). Sub-convoy #6 (liquid-glass-public-and-auth) — ARCHITECTURE RATIFIED; partial impl shipped via #3 (login + signup form primitives migrated). Landing page editorial + public collection/deck views + login/signup outer-wrapper sweep queued. Sub-convoy #7 (motion-system-pass) — MERGED. 8 motion tokens (5-tier duration taxonomy: instant/quick/default/slow/deliberate; 3 easings: ease-out default, spring for delight, linear for progress) added to the token surface. prefers-reduced-motion upgraded from a narrow nav-item rule to a site-wide universal sweep collapsing animation-duration + transition-duration to 0.01ms (preserves end states, no flicker); .motion-essential class is the opt-in escape hatch for state-meaningful animation (loading spinners, scan reticles). Authored docs/MOTION_SYSTEM.md with WCAG SC 2.3.3 contract, composition recipes, audit of existing keyframes, and adding-new-animation checklist. Sub-convoy #8 (cleanup-legacy-design-css) — Brief 1 MERGED. Two new CI jobs in .github/workflows/ci.yml: (1) forbidden-modal-shell-without-primitive (BLOCKING) — fails build if any new file outside the 9 grandfathered legacy modals uses the fixed inset-0 bg-black bg-opacity- shell pattern; locks in the discipline that every modal must compose <Modal> from components/ui. (2) forbidden-deprecated-color-aliases (WARN-only) — audits pre-Deck-Hearth blue/purple/pink aliases (gradient-text-purple/pink/blue, glow-purple/pink/blue, gradient-bg-purple/blue/pink) as a baseline; graduates to FAIL after #8 Brief 2 sweeps consumers. .cursor/rules/ui-and-theming.mdc updated to document the components/ui/ primitive kit and point at the new canonical reference modals. Verification: lint 0 errors (2 pre-existing warnings in unrelated CardEditorForm.js + CollectionsPageView.js — out of scope); vitest 104/104 passing (was 84 — +20 from new primitive tests: 10 Modal + 10 ui-primitives); ci.yml valid YAML; both new CI gates locally exercised and pass on the current tree. Operator follow-ups documented in .convoys/ship-readiness.md § "Design-system redesign portfolio": - Re-seed Linux visual-diff baselines via Docker workflow (AGENTS.md § 6) after this merges. - preview-smoke.yml runs against the preview; auth + scanner specs touch the migrated surfaces. - Vercel promote to production once smoke + visual gates pass. - Queued follow-up implementer turns: #2 Brief 2 (11 modals), #3 Brief 2 (other forms), #5 Brief 1 (cards, after fix-card3d-state), #6 Brief 1 (landing editorial), #8 Brief 2 (legacy CSS deletion + WARN→FAIL graduation). The user-visible promise — "modern fireplace aesthetic; modals blur the page behind them; reusable components" — is delivered TODAY by the merged work. Co-authored-by: Cursor <cursoragent@cursor.com>
13 KiB
| name | classification | success_metric | skip | status | created | conductor_started | brief_1_merged | depends_on | umbrella | |
|---|---|---|---|---|---|---|---|---|---|---|
| liquid-glass-modal-and-surface-primitive | feature | Two new primitives (`<GlassSurface>` and `<Modal>`) ship under `components/ui/`; all ~15 ad-hoc modals + dialogs in `components/` are migrated to `<Modal>`; modal backdrops blur the page behind them (the user's core ask); focus-trap + ESC-to-close + ARIA-correct shape is uniform; lint + vitest + smoke + visual-diff all green per brief. | in-progress-brief-1-merged | 2026-06-03 | 2026-06-03 | 2026-06-03 |
|
liquid-glass-redesign |
Convoy: liquid-glass-modal-and-surface-primitive
Sub-convoy #2 of the liquid-glass-redesign epic. Introduces the two
foundational reusable primitives + sweeps every modal in the codebase
onto the new <Modal>. This convoy is where the "modals blur the page
behind them" outcome the operator asked for actually ships.
Why
The repo has ~15 modal / dialog components, each with its own backdrop
implementation, its own focus-management (or lack thereof), its own
ESC-to-close handling (inconsistent), its own ARIA shape (often missing
role="dialog" or aria-modal="true"), and its own visual chrome.
This was already flagged in two places:
.convoys/ship-readiness.md§ Role-design-system-auditor: "CollectionSelectionModal,ShareModal,UploadImageModaleach have their own backdrop + focus-trap implementation. Extract<Modal>primitive.".convoys/ship-readiness.md§ Role-a11y-auditor: "Focus traps in modals — none of the modals trap focus." + "ESC to close modals — inconsistent."
The Liquid Glass direction makes this fix mandatory because every modal
now needs the same backdrop-blur effect — implementing that per-modal
would be the worst possible outcome (15 places to bug-fix). One
<Modal> primitive, one backdrop recipe, fifteen migrations.
<GlassSurface> is split out as a sibling primitive because the same
"panel of glass" shape is needed in non-modal contexts (sidebar in #4,
card detail in #5, dropdown in #4's UserProfileDropdown). <Modal> is
implemented in terms of <GlassSurface> for its panel.
Scope
In scope — primitives
components/ui/GlassSurface.js(new) — composable panel primitive. Props:as(default'div'),tint('low' | 'mid' | 'high'),rim('none' | 'subtle' | 'pronounced' | 'ember'),elevation('flat' | 'ambient' | 'pronounced'),className,style,children. Reads tokens from sub-convoy #1.components/ui/Modal.js(new) — backdrop + dialog primitive. Props:open,onClose,title(string, required for a11y),description(optional, foraria-describedby),size('sm' | 'md' | 'lg' | 'fullscreen-on-mobile'),closeOnBackdrop(defaulttrue),closeOnEsc(defaulttrue),initialFocusRef,children. Implements:- Backdrop with
backdrop-filter: blur(var(--glass-blur-high))+background: var(--modal-scrim). - Inner panel uses
<GlassSurface tint="low" rim="subtle" elevation="pronounced" />. - Focus trap (proposal: small homegrown
useFocusTraphook inlib/use-focus-trap.js— no new third-party dep; architect to confirm vsfocus-trappackage). role="dialog",aria-modal="true",aria-labelledby={titleId},aria-describedby={descriptionId | undefined}.- ESC handler with cleanup on unmount.
- Restores focus to the trigger on close.
- Body-scroll lock while open.
- Backdrop with
components/ui/index.js(new) — barrel export.test/components/Modal.test.js(new) — assertions:- Renders nothing when
open === false. - Renders dialog with correct ARIA when
open === true. - Calls
onCloseon ESC. - Calls
onCloseon backdrop click (whencloseOnBackdroptrue). - Does NOT call
onCloseon backdrop click whencloseOnBackdropfalse. - Traps focus inside the dialog (Tab cycles through focusable elements; Shift+Tab cycles backwards).
- Restores focus to the trigger on close.
- Renders nothing when
In scope — modal sweep
Migrate every modal-shaped component onto <Modal>:
components/CollectionSelectionModal.jscomponents/CollectionsCreateModal.jscomponents/CollectionsEditModal.jscomponents/CollectionsSuccessModal.jscomponents/CollectionEditModal.jscomponents/CollectionDeleteModal.jscomponents/CardDetailDeckModal.jscomponents/CardDetailQuantityModal.jscomponents/ShareModal.jscomponents/UploadImageModal.jscomponents/ScanDisambiguationDialog.jscomponents/OCRSettings.js(modal-shaped; verify)components/ManaSymbolSettings.js(modal-shaped; verify)- Any inline modal in
components/CollectionsPageView.js,components/ScannerPageView.js,components/CardsPageView.js,components/CardItem.js,components/CardDetailView.js— architect inventories during architect pass.
Each migrated modal:
- Imports
<Modal>fromcomponents/ui/. - Hands off backdrop / focus / ARIA / ESC to the primitive.
- Keeps its own content (the form, the buttons, the body copy).
- Visual diff baselines are re-seeded post-merge on Linux.
Out of scope
<Button>,<Input>,<SearchBar>primitives — sub-convoy #3.- Layout / MobileNavigation glass — sub-convoy #4.
- Card surface glass — sub-convoy #5.
- Dropdown primitive (the UserProfileDropdown ad-hoc menu in Layout) — may be tempting, but defer to #4 since Layout owns that surface.
Roles invoked
role-architect— primitive API design (especially the focus-trap hook decision), brief decomposition.role-a11y-auditor— primitive ARIA contract review BEFORE implementer starts (gate 1 dependency).role-implementer— multiple briefs (see Multitask dispatch).- Post-PR audit fleet —
/multitask role-reviewer + role-design-system-auditor + role-a11y-auditor.
Architecture (ratified 2026-06-03)
Primitives:
components/ui/GlassSurface.js—forwardRefcomposable surface. Props:as,tint(low/mid/high),rim(none/subtle/pronounced/ember-subtle/ember-pronounced),elevation(flat/ambient/pronounced),blur(low/mid/high). Composes the canonical token surface.components/ui/Modal.js—<Modal>primitive consuming<GlassSurface>for the panel. Built-in scrim + backdrop blur (--modal-scrim+blur(--glass-blur-high)), built-in title + close button, focus trap, ESC + backdrop close, body-scroll lock. Props:open,onClose,title,description,size,closeOnBackdrop,closeOnEsc,initialFocusRef,hideCloseButton.lib/use-focus-trap.js— homegrown hook (~60 LOC, no dep). Active-when-open, restores focus on close, Tab+Shift-Tab cycling within container.components/ui/index.js— barrel export.
Test plan: test/components/Modal.test.js — 10 cases covering open/close render, ARIA shape (role=dialog, aria-modal, labelledby, describedby), ESC + closeOnEsc gate, backdrop click + closeOnBackdrop gate, built-in close button, hideCloseButton, body-scroll lock + restore.
Briefs
- Brief 1 (shipped 2026-06-03): Primitives + 4 reference modal migrations (ShareModal, CollectionDeleteModal, CollectionsCreateModal, CardDetailQuantityModal). Tests pass 10/10. Vitest 94/94 green. Lint 0 errors.
- Brief 2 (queued for follow-up): Sweep remaining 11 modals — CollectionSelectionModal, UploadImageModal, CollectionsEditModal, CollectionsSuccessModal, CollectionEditModal, CardDetailDeckModal, ScanDisambiguationDialog, plus inline modals in PageView components. Mechanical migration following the 4-reference pattern: replace outer fixed-backdrop div with
<Modal>; replace inner panel container with the Modal body; rely on Modal's built-in title + close. Inner color cleanup (hardcoded Tailwind grays/blues) is out of scope here — that's #3 + #8.
Todos
- Architect: primitive API + brief decomposition + focus-trap
hook decision (homegrown vs
focus-trappackage) - A11y auditor: ARIA contract review (gate-1 dep)
- Brief 1 —
<GlassSurface>+<Modal>primitives + tests + migrate 2 reference modals (ShareModal,CollectionDeleteModal— small + diverse) - Brief 2 — migrate modals 3–7 (Collections cluster)
- Brief 3 — migrate modals 8–10 (CardDetail cluster + Upload)
- Brief 4 — migrate modals 11–13 (Scanner / Settings cluster)
- Post-PR audit per brief
Decisions to ratify (architect)
- Focus-trap implementation — homegrown
useFocusTraphook vsfocus-trappackage (one small dep). Recommended: homegrown if the ARIA-correct shape fits in ~60 LOC; the package if not. Either way,tabbable-style focusable-element enumeration must handledisabled,hidden,tabindex="-1", and elements inside Shadow DOM (unlikely needed here). - Body-scroll lock approach —
overflow: hiddenon<body>vsinertattribute on siblings vs a dedicated package. Recommended:overflow: hidden+padding-rightcompensation for the scrollbar. - Backdrop fade-in transition — duration + easing. Recommended: 180ms ease-out for backdrop, 220ms cubic-bezier(0.16, 1, 0.3, 1) spring for the panel (Apple-style overshoot dampened).
fullscreen-on-mobilebreakpoint —768px(Tailwindmd) is the existing mobile pivot in the codebase. Confirm.- Trigger-focus restoration when trigger is unmounted — fall back
to
document.body. Confirm. ScanDisambiguationDialog.js— is it a true modal or an inline dialog? Architect inspects + decides whether to fold or leave inline.
Acceptance criteria
<GlassSurface>+<Modal>exist undercomponents/ui/.test/components/Modal.test.jspasses 7+ assertions (per § Scope).- All ~15 modals listed in § Scope are migrated.
- Every migrated modal:
- Has
role="dialog"+aria-modal="true"+aria-labelledby. - Traps focus.
- Closes on ESC.
- Restores focus on close.
- Backdrop blurs the page behind (the user's core ask).
- Has
- Lint + vitest + smoke green.
- Visual-diff baselines re-seeded on Linux post-merge.
.cursor/rules/ui-and-theming.mdc§ "Common UI patterns to reuse" updated: Modal row now points atcomponents/ui/Modal.js, not the three ad-hoc modal files.
CI impact
| Workflow / job | Behavior |
|---|---|
preview-smoke.yml |
Fires (every brief). |
visual-diff.yml |
Fires + LOUD — components/** matches paths; modals change shape. Re-seed baselines on Linux post-each-brief. |
lint |
Fires. |
test: (vitest) |
Fires + new 7+ assertions in Modal.test.js lock the primitive's contract. |
| New grep gates | Consider a forbidden-ad-hoc-modal-backdrop lint or grep gate post-sweep: forbid `className="fixed inset-0 .* bg-(black |
Known constraints
- No third-party UI library.
headlessui/radix-uiwere considered (see.convoys/ship-readiness.md§ Role-design-system- auditor: "Useheadlessuiorradix-ui's Dialog to get focus management for free."). Architect should re-evaluate:- Pros of headlessui: free focus-trap, free ARIA, well-tested.
- Cons: adds a runtime dependency, styled by Tailwind variants only (we use CSS variables for color — friction).
- Recommended default: homegrown for v1 (smaller surface, no dep), revisit if Brief 1 hits >150 LOC for the primitive itself.
- Theme tokens — primitives consume ONLY tokens from sub-convoy #1; no hardcoded hex.
- Mobile safe-area —
<Modal size="fullscreen-on-mobile">must respectenv(safe-area-inset-bottom)(the existing.h-safe-area-inset-bottomrule pattern).
Multitask dispatch
slice_dependencies:
- brief: 1
depends_on: []
files:
- components/ui/GlassSurface.js
- components/ui/Modal.js
- components/ui/index.js
- lib/use-focus-trap.js
- test/components/Modal.test.js
- components/ShareModal.js
- components/CollectionDeleteModal.js
- .cursor/rules/ui-and-theming.mdc
- brief: 2
depends_on: [1]
files:
- components/CollectionSelectionModal.js
- components/CollectionsCreateModal.js
- components/CollectionsEditModal.js
- components/CollectionsSuccessModal.js
- components/CollectionEditModal.js
- brief: 3
depends_on: [1]
files:
- components/CardDetailDeckModal.js
- components/CardDetailQuantityModal.js
- components/UploadImageModal.js
- brief: 4
depends_on: [1]
files:
- components/ScanDisambiguationDialog.js
- components/OCRSettings.js
- components/ManaSymbolSettings.js
After Brief 1 merges: /multitask role-implementer briefs 2, 3, 4
(disjoint file sets; safe).
Post-PR audit per brief:
/multitask role-reviewer + role-design-system-auditor + role-a11y-auditor
Group id: audit-liquid-glass-modal-<brief>-<pr>.
Out of scope follow-ups
forbidden-ad-hoc-modal-backdropCI gate — see § CI impact. Surface as a separate small convoy if the architect decides not to fold it into Brief 1.- Dropdown primitive —
<Popover>/<Menu>shape forLayout.js's UserProfileDropdown. Defer to sub-convoy #4. - Toast / Notification primitive — out of scope (no toast system
exists yet;
.convoys/ship-readiness.md§ Role-ux-reviewer flagged this as a separate need).