deckhearth/docs/DESIGN_TOKENS.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

Design tokens — Deck Hearth

Reference for the design-token surface. The canonical product brand is Deck Hearth (post-pick-a-name PR #21, 2026-05-24); the visual direction is Liquid Glass (post-liquid-glass-design-tokens PR TBD, 2026-06-03 — see .convoys/liquid-glass-redesign.md).

Every color token is defined in styles/globals.css and consumed via var(--token-name). Do not hardcode hex in .js files; the post- cleanup-legacy-design-css forbidden-hex-in-jsx lint gate will fail the build (see .convoys/cleanup-legacy-design-css.md for the planned gate).

Layer overview

Layer Purpose Tokens
Base palette Theme-tuned background + text + accent colors --bg-*, --text-*, --accent-*, --border — pre-existing
Surface (glass) Translucent panel fills --glass-surface-{low,mid,high}
Blur + saturate backdrop-filter ingredients --glass-blur-{low,mid,high}, --glass-saturate
Rim-light Edge definition on glass surfaces --rim-light-{inner,outer}
Ember rim Brand-accent rings for interactive primaries --ember-rim-color (RGB triple), --ember-rim-{subtle,pronounced}
Elevation Shadow stacks --elevation-{flat,ambient,pronounced}
Modal scrim Backdrop fill behind modals --modal-scrim

Surface tokens

Three-step legibility ramp. Higher number = more opaque.

Token Light value Dark value Use
--glass-surface-low rgba(254, 252, 248, 0.55) rgba(26, 15, 10, 0.55) Modal panels (inside a scrim), card detail, inline glass sub-panels
--glass-surface-mid rgba(254, 252, 248, 0.68) rgba(26, 15, 10, 0.68) Sidebar rail, header strip, mobile bottom-bar
--glass-surface-high rgba(254, 252, 248, 0.82) rgba(26, 15, 10, 0.82) Popovers, dropdowns, tooltips

Always paired with backdrop-filter: blur(...) saturate(var(--glass-saturate)).

Blur + saturate tokens

Token Value Use
--glass-blur-low 12px Inline glass panels with adjacent flat surfaces
--glass-blur-mid 20px Default panel + nav
--glass-blur-high 32px Modal scrim — heavy, immersive
--glass-saturate 140% Vibrancy boost on all glass surfaces

Theme-independent (same value in both themes).

Rim-light tokens

The hairline edges that define a glass surface against the background.

Token Light value Dark value Use
--rim-light-inner inset 0 1px 0 0 rgba(255, 255, 255, 0.65) inset 0 1px 0 0 rgba(255, 248, 240, 0.12) Top-edge inner highlight (light cast onto glass from above)
--rim-light-outer 0 0 0 1px rgba(45, 24, 16, 0.08) 0 0 0 1px rgba(255, 248, 240, 0.06) Hairline outer ring

Consumed inside box-shadow: lists, typically together: box-shadow: var(--rim-light-inner), var(--rim-light-outer), var(--elevation-ambient);.

Ember rim tokens

Brand-accent rings. Pronounced on interactive primaries; subtle on ambient surfaces.

Token Light value Dark value Use
--ember-rim-color 216, 67, 21 (RGB triple) same Composable: rgba(var(--ember-rim-color), 0.42) for custom alphas
--ember-rim-subtle inset 0 0 0 1px rgba(216, 67, 21, 0.35) inset 0 0 0 1px rgba(216, 67, 21, 0.40) Ambient surfaces (nav rail rest state, header)
--ember-rim-pronounced inset 0 0 0 1px rgba(216, 67, 21, 0.55), 0 0 16px 0 rgba(216, 67, 21, 0.30) inset 0 0 0 1px rgba(216, 67, 21, 0.65), 0 0 16px 0 rgba(216, 67, 21, 0.35) Interactive primaries (buttons, focused inputs, selected cards)

Dark theme rim alphas are slightly higher to compensate for ember orange reading less vibrant on dark backgrounds (eye-perception correction).

Elevation tokens

Token Light value Dark value Use
--elevation-flat none none Explicit "no shadow" (inline glass sub-panels)
--elevation-ambient 0 4px 12px -2px rgba(45, 24, 16, 0.08), 0 2px 4px -1px rgba(45, 24, 16, 0.04) 0 4px 12px -2px rgba(0, 0, 0, 0.40), 0 2px 4px -1px rgba(0, 0, 0, 0.30) Default elevated surfaces (popovers, sidebar)
--elevation-pronounced 0 24px 48px -12px rgba(45, 24, 16, 0.20), 0 12px 24px -6px rgba(45, 24, 16, 0.10), 0 4px 8px -2px rgba(45, 24, 16, 0.06) 0 24px 48px -12px rgba(0, 0, 0, 0.55), 0 12px 24px -6px rgba(0, 0, 0, 0.40), 0 4px 8px -2px rgba(0, 0, 0, 0.25) Modal panels, card detail full view

Light theme uses warm-brown-tinted shadows (rgba(45, 24, 16, ...)) for thematic consistency. Dark theme uses pure black for crisp depth.

Modal scrim token

Token Light value Dark value
--modal-scrim rgba(45, 24, 16, 0.35) rgba(0, 0, 0, 0.55)

Combined with backdrop-filter: blur(var(--glass-blur-high)) (32px) at the <Modal> primitive level (sub-convoy #2 ships that primitive).

Composite recipes

Six common compositions. These are documentation patterns — they're NOT new CSS variables. Primitive authors compose them as shown.

Recipe CSS composition
Modal scrim background: var(--modal-scrim); backdrop-filter: blur(var(--glass-blur-high)) saturate(var(--glass-saturate));
Modal panel background: var(--glass-surface-low); backdrop-filter: blur(var(--glass-blur-mid)) saturate(var(--glass-saturate)); box-shadow: var(--rim-light-inner), var(--rim-light-outer), var(--elevation-pronounced);
Sidebar rail background: var(--glass-surface-mid); backdrop-filter: blur(var(--glass-blur-mid)) saturate(var(--glass-saturate)); box-shadow: var(--rim-light-inner), var(--rim-light-outer);
Popover background: var(--glass-surface-high); backdrop-filter: blur(var(--glass-blur-mid)) saturate(var(--glass-saturate)); box-shadow: var(--rim-light-inner), var(--ember-rim-subtle), var(--elevation-ambient);
Glass button (primary, hover) background: var(--glass-surface-high); backdrop-filter: blur(var(--glass-blur-low)) saturate(var(--glass-saturate)); box-shadow: var(--rim-light-inner), var(--ember-rim-pronounced);
Glass card detail background: var(--glass-surface-low); backdrop-filter: blur(var(--glass-blur-mid)) saturate(var(--glass-saturate)); box-shadow: var(--rim-light-inner), var(--rim-light-outer), var(--elevation-pronounced);

Contrast measurements (WCAG 2.2 AA target)

Glass surfaces composited over the default --bg-primary (best case). For the "glass over busy card art" worst case, see § "When NOT to use glass" below.

Light theme

Glass token vs --text-primary (#2d1810) vs --text-secondary (#5d4037) AA pass?
--glass-surface-low (α=0.55) 15.18 : 1 6.86 : 1 AAA / AA
--glass-surface-mid (α=0.68) 15.18 : 1 6.86 : 1
--glass-surface-high (α=0.82) 15.18 : 1 6.86 : 1
Fallback (α=0.92) 15.18 : 1 6.86 : 1

Dark theme

Glass token vs --text-primary (#fff8f0) vs --text-secondary (#d7c4b0) AA pass?
--glass-surface-low (α=0.55) 17.84 : 1 11.42 : 1 AAA / AAA
--glass-surface-mid (α=0.68) 17.84 : 1 11.42 : 1
--glass-surface-high (α=0.82) 17.84 : 1 11.42 : 1
Fallback (α=0.92) 17.84 : 1 11.42 : 1

Ember rim (WCAG SC 1.4.11 — non-text contrast, AA = 3:1)

Rim token Light contrast Dark contrast AA pass?
--ember-rim-subtle 3.18 : 1 4.41 : 1
--ember-rim-pronounced 4.92 : 1 6.18 : 1

When NOT to use glass

The contrast measurements above assume glass over --bg-primary. Glass over arbitrary card grids or vibrant card images is not guaranteed-legible. Four rules:

  1. --glass-surface-low — ONLY use inside a --modal-scrim (the scrim pre-darkens / pre-blurs the page; contrast becomes predictable).
  2. --glass-surface-mid — Use over surfaces that are themselves flat (sidebar rails over the page background; NOT over card grids).
  3. --glass-surface-high — Use for popovers, but ensure the popover's contents would hit 4.5:1 against --bg-primary directly. At 0.82α the surface is functionally a tinted-flat panel.
  4. Never place body text on a glass surface positioned over a card grid without an opaque inner panel.

Per-card grid performance budget

backdrop-filter is GPU-expensive. Stacked instances on long card grids hurt scroll performance.

  • Card grid items (components/CardItem.js): NO backdrop-filter. Solid --bg-secondary background + cheap shadow + rarity glow.
  • Card grid containers (the wrapping panel): MAY use glass.
  • Card detail view (components/CardDetailView.js): full glass.
  • Card3D hover preview: keeps 3D transform; uses --elevation-pronounced + --ember-rim-pronounced; no backdrop-filter.

This is the contract sub-convoy #5 (liquid-glass-card-surfaces) implements. See .convoys/liquid-glass-card-surfaces.md for detail.

Browser support + fallback

backdrop-filter is supported in all evergreen browsers:

Browser Support
Safari 18+ (macOS, iOS) Native
Chrome / Edge 76+ Native
Firefox 103+ Native
Safari 917 -webkit-backdrop-filter prefix needed
Chrome 1775, Firefox <103, IE 11 Unsupported — fallback fires

Global support >97% (caniuse 2026-06-03). Fallback fires on <3% of sessions and collapses the surface ramp to 0.92 / 0.95 / 0.98 — visually similar to flat panels but preserves ramp ordering.

Reduced motion

This document does not document animations — those are governed by docs/MOTION_SYSTEM.md (shipped by sub-convoy #7, .convoys/motion-system-pass.md). When that doc lands, it MUST respect @media (prefers-reduced-motion: reduce) for every animation.

Deprecations

  • fire-glow-bg (page-level background animation in styles/globals.css lines ~755757) — scheduled for deletion by motion-system-pass (sub-convoy #7). Replacement: localized ember-float accent on landing hero only. Do NOT consume fire-glow-bg in new code.
  • --accent-blue / --accent-purple / --accent-pink aliases (styles/globals.css lines ~116118, ~146149) — scheduled for deletion by cleanup-legacy-design-css (sub-convoy #8). They alias flame/ember/gold and are pre-Deck-Hearth-era debt. Do NOT consume in new code; use the canonical --accent-flame / --accent-ember / --accent-gold directly.
  • .gradient-text-blue / .gradient-text-purple / .glow-blue / .glow-purple / .glow-pink — same; deletion in #8.
  • .fire-glow / .ember-glow — utility classes superseded by --ember-rim-{subtle,pronounced}. Deletion in #8.
  • .convoys/liquid-glass-redesign.md — umbrella epic.
  • .convoys/liquid-glass-design-tokens.md — this token surface.
  • .convoys/liquid-glass-modal-and-surface-primitive.md<GlassSurface> + <Modal> primitives consuming these tokens.
  • .convoys/cleanup-legacy-design-css.md — sweeps deprecations.
  • .convoys/motion-system-pass.md — motion taxonomy (parallel scope).