deckhearth/.convoys/liquid-glass-form-primitives.md
varutasu 334612ad79
feat(design-system): Liquid Glass redesign portfolio — foundation + primitives + Layout (#95)
* feat(design-system): Liquid Glass redesign portfolio — foundation + primitive kit + Layout shell

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>

* fix(use-focus-trap): preserve named useFocusTrap export for ScannerPageView

The portfolio squash inadvertently overwrote the pre-existing
lib/use-focus-trap.js (named `export function useFocusTrap(active)`
returning a ref — used by ScannerPageView, line 21) with a default-
only export shaped for the new `<Modal>` primitive. Vercel build
failed: "Export useFocusTrap doesn't exist in target module".

Fix: the file now exports BOTH —
- `useFocusTrap(active)` (named, original) — returns a ref;
  pre-Liquid-Glass call sites (ScannerPageView) keep working.
- `useFocusTrapContainer({ active, containerRef, ... })` (default,
  new) — takes a caller-owned ref so panel refs can forward through
  forwardRef chains (Modal.js consumes this shape).

Both hooks are commented to document which to use when. Modal.js
imports default already, so no change needed there.

Verified: npm run build passes (was failing in CI); lint 0 errors;
vitest 104/104 still green.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 20:12:33 -05:00

11 KiB
Raw Blame History

name classification success_metric skip status created conductor_started brief_1_merged depends_on umbrella
liquid-glass-form-primitives feature `<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.
ia
in-progress-brief-1-merged 2026-06-03 2026-06-03 2026-06-03
liquid-glass-design-tokens
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 34 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.jsforwardRef. 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.jsforwardRef. 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.jsforwardRef. 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 recipebox-shadow: 0 0 0 3px var(--ember-rim-pronounced) on :focus-visible (NOT :focus — keep mouse-click focus clean). Confirm.
  4. Input error wiringaria-invalid="true" + aria-describedby pointing at the error <span>. Confirm.
  5. <Input srOnly> renderingclass="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

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.