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>
239 lines
11 KiB
Markdown
239 lines
11 KiB
Markdown
---
|
||
name: liquid-glass-form-primitives
|
||
classification: feature
|
||
success_metric: |
|
||
`<Button>`, `<Input>`, `<SearchBar>` primitives ship under
|
||
`components/ui/`; the .btn-primary / .btn-flame / .btn-ember /
|
||
.btn-gold / .input-field / .search-bar utility classes either become
|
||
thin aliases of the new primitives' styling OR are deprecated for #8
|
||
to delete; every consumer of those classes is migrated; focus-rings
|
||
use the new ember-rim tokens; lint + vitest + smoke green.
|
||
skip:
|
||
- ia
|
||
status: in-progress-brief-1-merged
|
||
created: 2026-06-03
|
||
conductor_started: 2026-06-03
|
||
brief_1_merged: 2026-06-03
|
||
depends_on:
|
||
- liquid-glass-design-tokens
|
||
umbrella: liquid-glass-redesign
|
||
---
|
||
|
||
# Convoy: liquid-glass-form-primitives
|
||
|
||
Sub-convoy #3 of the `liquid-glass-redesign` epic. Introduces the
|
||
button, input, and search-bar primitives — the second half of the
|
||
foundational reusable kit (after the modal + surface primitives of #2).
|
||
Parallel-safe with #2 after #1 merges.
|
||
|
||
## Why
|
||
|
||
The repo defines five button utility classes (`.btn-primary`,
|
||
`.btn-flame`, `.btn-ember`, `.btn-gold`, `.btn-secondary`) and two
|
||
input classes (`.input-field`, `.search-bar`) directly in
|
||
`styles/globals.css`. Each uses opaque ember/flame gradient fills + a
|
||
single drop shadow — i.e. the "warm panel" aesthetic the redesign is
|
||
moving away from.
|
||
|
||
Buttons and inputs are the densest interactive surface in the app. If
|
||
every other surface goes glass and these stay opaque, the visual
|
||
hierarchy fights itself.
|
||
|
||
A small primitive set lets callers express *intent* (primary action,
|
||
ghost secondary, ember rim-light hover state) without composing
|
||
Tailwind class strings or wiring ad-hoc `style={{}}` objects.
|
||
|
||
## Scope
|
||
|
||
### In scope — primitives
|
||
|
||
- `components/ui/Button.js` (new). Props:
|
||
- `variant`: `'primary'` (ember rim-light glass pill), `'ghost'`
|
||
(glass with no accent until hover), `'ember'` (solid ember for
|
||
destructive / high-emphasis CTAs), `'gold'` (celebration / rarity
|
||
CTAs), `'icon'` (square glass for header icon-only buttons).
|
||
- `size`: `'sm' | 'md' | 'lg'`.
|
||
- `loading` (boolean) — shows an inline spinner; disables button.
|
||
- `iconLeft`, `iconRight` — slot for SVG icons.
|
||
- Standard `<button>` props (type, onClick, disabled, aria-label,
|
||
...rest).
|
||
- Renders a native `<button>` with the glass styling + focus ring +
|
||
the appropriate ARIA when used as icon-only.
|
||
- `components/ui/Input.js` (new). Props:
|
||
- `label` (required for a11y; visible by default; can be
|
||
`srOnly={true}`).
|
||
- `id` (auto-generated if absent).
|
||
- `error` (string; renders connected via `aria-describedby` per the
|
||
a11y finding in `.convoys/ship-readiness.md` § Role-a11y-auditor:
|
||
*"login/signup form errors are visually red but not connected to
|
||
inputs via `aria-describedby`"* — this primitive CLOSES that
|
||
finding).
|
||
- `description` (optional hint text).
|
||
- Standard `<input>` props.
|
||
- `components/ui/SearchBar.js` (new). Wraps `<Input>` with the
|
||
search-magnifier icon, ⌘K keyboard hint slot, and a dedicated focus
|
||
state (the search bar is currently the most visually distinct input
|
||
in the app).
|
||
- Tests under `test/components/`:
|
||
- `Button.test.js` — variants render, `loading` disables, icon-only
|
||
requires `aria-label`, focus ring visible on `:focus-visible`.
|
||
- `Input.test.js` — label association, error -> `aria-describedby`
|
||
wiring, visually-hidden label via `srOnly`.
|
||
|
||
### In scope — migration sweep
|
||
|
||
Migrate consumers of the legacy classes to the new primitives:
|
||
|
||
- Every page under `pages/**/*.js` that uses `className="btn-primary"`
|
||
/ `"btn-flame"` / `"btn-ember"` / `"btn-gold"` / `"action-btn-primary"`
|
||
/ `"action-btn-secondary"`.
|
||
- Every component under `components/**/*.js` that uses those classes.
|
||
- `input-field` / `search-bar` / `theme-toggle` / `header-icon`
|
||
consumers.
|
||
|
||
The legacy utility classes in `styles/globals.css` are **NOT deleted
|
||
here** — they remain as thin aliases (or stub no-ops) until sub-convoy
|
||
#8 deletes them as a batch. This keeps the diff per PR readable.
|
||
|
||
### Out of scope
|
||
|
||
- Modal / Surface primitives — sub-convoy #2.
|
||
- Layout shell — sub-convoy #4.
|
||
- Card surfaces — sub-convoy #5.
|
||
- New form patterns (multi-step wizards, etc.) — orthogonal scope.
|
||
|
||
## Roles invoked
|
||
|
||
1. `role-architect` — primitive API, brief decomposition (likely 3–4
|
||
briefs by file cluster).
|
||
2. `role-a11y-auditor` — pre-implementation review of Button + Input
|
||
ARIA contracts (especially `<Input>` error association).
|
||
3. `role-design-system-auditor` — sign-off on variant shape +
|
||
focus-ring recipe.
|
||
4. `role-implementer` — multiple briefs.
|
||
5. Post-PR audit fleet.
|
||
|
||
## Architecture (ratified 2026-06-03)
|
||
|
||
**Primitives:**
|
||
- `components/ui/Button.js` — `forwardRef`. Variants: `primary` (ember gradient w/ ember-rim-pronounced + rim-light-inner; hover scales 1.02; active scales 0.98), `secondary` (glass-surface-high + rim-subtle), `danger` (#dc2626), `ghost` (transparent w/ ember-tinted hover). Sizes: sm/md/lg. Built-in `loading` (aria-busy + spinner replaces leading icon), `disabled` (opacity 0.5 + pointer-events-none), `leadingIcon` + `trailingIcon`, ember focus-visible ring.
|
||
- `components/ui/Input.js` — `forwardRef`. Glass-surface-high background, ember focus ring, supports `label` (semantic htmlFor/id pairing), `error` (red border + red message + aria-invalid + aria-describedby), `helperText` (mutually exclusive with error), `leadingIcon` (decorative pointer-events-none), `trailingAction` (interactive). All native input props pass through.
|
||
- `components/ui/SearchBar.js` — `forwardRef`. Wraps Input with leading search icon, conditional clear button (renders only when value non-empty AND onClear provided). Defaults type="search", placeholder "Search…".
|
||
|
||
**Test plan:** `test/components/ui-primitives.test.js` — 10 cases. Button: children/onClick, loading state (aria-busy + disabled), disabled suppresses click, all 4 variants render. Input: label/htmlFor pairing, error sets aria-invalid + describedby + visible message, helperText path with no error. SearchBar: search-type input + placeholder, clear button conditional on value + onClear, no clear when onClear missing.
|
||
|
||
## Briefs
|
||
|
||
- **Brief 1 (shipped 2026-06-03):** Primitives + tests + 2 reference page migrations (login.js, signup.js — both smoke-tested critical paths). 7 inputs + 2 submit buttons migrated. Vitest 104/104 green (+10 new primitive tests). Lint 0 errors. Existing `test/pages/login.test.js` assertion ("Sign in to Deck Hearth" button text) preserved.
|
||
- **Brief 2 (queued for follow-up):** Sweep remaining form-bearing surfaces — profile/settings pages, deck-builder text inputs, scanner search field, card-editor admin form, all collection-cluster modal forms (Brief 2 here lands AFTER #2's Brief 2 so the modal shell is already in place). Mechanical migration following the login/signup pattern.
|
||
|
||
## Todos
|
||
|
||
- [ ] Architect: primitive API + brief decomposition
|
||
- [ ] A11y auditor: pre-impl ARIA review
|
||
- [ ] Design-system auditor: variant shape + focus-ring recipe
|
||
- [ ] Brief 1 — primitives + tests + migrate 2 reference pages
|
||
(`pages/login.js`, `pages/signup.js` — the most form-dense
|
||
auth surfaces, also covered by smoke spec #2)
|
||
- [ ] Brief 2 — migrate `components/**` consumers (cluster by neighbor)
|
||
- [ ] Brief 3 — migrate `pages/**` consumers (cluster by neighbor)
|
||
- [ ] Post-PR audit per brief
|
||
|
||
## Decisions to ratify (architect)
|
||
|
||
1. **`<Button variant>` set** — proposal: `primary | ghost | ember |
|
||
gold | icon`. Confirm or trim.
|
||
2. **Loading state shape** — inline spinner vs button-shaped
|
||
skeleton. Recommended: inline spinner that replaces `iconLeft` slot.
|
||
3. **Focus ring recipe** — `box-shadow: 0 0 0 3px var(--ember-rim-pronounced)`
|
||
on `:focus-visible` (NOT `:focus` — keep mouse-click focus clean).
|
||
Confirm.
|
||
4. **Input error wiring** — `aria-invalid="true"` + `aria-describedby`
|
||
pointing at the error `<span>`. Confirm.
|
||
5. **`<Input srOnly>` rendering** — `class="sr-only"` on the label,
|
||
not removed from DOM. Required for screen readers.
|
||
6. **Migration approach for ad-hoc inline buttons** — many pages
|
||
compose `<button className="bg-[...] text-[...] ...">` directly with
|
||
no utility class. Architect inventories these during architect pass
|
||
and decides whether to fold into briefs or leave for sub-convoy #8's
|
||
hex sweep.
|
||
|
||
## Acceptance criteria
|
||
|
||
1. `<Button>`, `<Input>`, `<SearchBar>` exist under `components/ui/`.
|
||
2. Tests pass.
|
||
3. Every consumer of the 7 legacy utility classes is migrated OR
|
||
marked for #8.
|
||
4. `<Input>` error states wire `aria-describedby` (closes a11y
|
||
finding).
|
||
5. Lint + vitest + smoke green.
|
||
6. Visual-diff baselines re-seeded per brief.
|
||
|
||
## CI impact
|
||
|
||
| Workflow / job | Behavior |
|
||
| --- | --- |
|
||
| `preview-smoke.yml` | Fires. Smoke spec 2 (`'sign-in page renders'`) defends Button migration on `/login` post-Brief 1. |
|
||
| `visual-diff.yml` | **Fires + LOUD** — buttons appear everywhere. |
|
||
| `lint` | Fires. |
|
||
| `test:` (vitest) | Fires + new Button/Input assertions lock primitive contract. |
|
||
| New grep gates | None for this convoy; #8 may add a `forbidden-legacy-btn-class` grep gate post-cleanup. |
|
||
|
||
## Known constraints
|
||
|
||
- **Tailwind utility classes inside primitive** are fine — the
|
||
primitive IS the abstraction; nothing outside it cares.
|
||
- **No new third-party form library.** Plain `<button>` / `<input>`
|
||
underneath; no Formik, no react-hook-form. Existing forms in the
|
||
codebase manage state with `useState`; that pattern stays.
|
||
- **Theme tokens** — primitives consume ONLY tokens from #1; no
|
||
hardcoded hex.
|
||
|
||
## Multitask dispatch
|
||
|
||
```yaml
|
||
slice_dependencies:
|
||
- brief: 1
|
||
depends_on: []
|
||
files:
|
||
- components/ui/Button.js
|
||
- components/ui/Input.js
|
||
- components/ui/SearchBar.js
|
||
- components/ui/index.js
|
||
- test/components/Button.test.js
|
||
- test/components/Input.test.js
|
||
- pages/login.js
|
||
- pages/signup.js
|
||
- brief: 2
|
||
depends_on: [1]
|
||
files:
|
||
# Architect-curated cluster of component consumers
|
||
- components/CardItem.js
|
||
- components/CardDetailView.js
|
||
- components/CardsPageView.js
|
||
- components/CollectionPageView.js
|
||
- components/CollectionsPageView.js
|
||
- components/ScannerPageView.js
|
||
# ... (architect completes inventory)
|
||
- brief: 3
|
||
depends_on: [1]
|
||
files:
|
||
# Architect-curated cluster of page consumers
|
||
- pages/index.js
|
||
- pages/profile.js
|
||
- pages/settings.js
|
||
- pages/my-cards.js
|
||
- pages/collections.js
|
||
# ... (architect completes inventory)
|
||
```
|
||
|
||
**After Brief 1 merges:** `/multitask role-implementer briefs 2, 3`
|
||
(disjoint file sets if architect partitions correctly).
|
||
|
||
## Out of scope follow-ups
|
||
|
||
- **`forbidden-legacy-btn-class`** grep gate (P3 hygiene) — surface for
|
||
#8's cleanup. Forbid `className="btn-(primary|flame|ember|gold|
|
||
secondary)"` post-migration.
|
||
- **`form-validation-library-adoption`** — react-hook-form vs Zod vs
|
||
homegrown. Out of scope; would be its own architect-led convoy.
|