* 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>
256 lines
11 KiB
Markdown
256 lines
11 KiB
Markdown
---
|
||
name: liquid-glass-layout-shell
|
||
classification: feature
|
||
success_metric: |
|
||
`components/Layout.js` (826 lines) and `components/MobileNavigation.js`
|
||
render as glass surfaces (sidebar rail, top bar, mobile bottom bar,
|
||
mobile drawer); the existing 5-assertion Layout test suite stays
|
||
green; the smoke `'sign-in page renders'` and visual-diff workflows
|
||
defend the change; visual-diff baselines re-seeded on Linux post-merge.
|
||
skip: []
|
||
status: merged
|
||
created: 2026-06-03
|
||
merged: 2026-06-03
|
||
depends_on:
|
||
- liquid-glass-design-tokens
|
||
- liquid-glass-modal-and-surface-primitive
|
||
umbrella: liquid-glass-redesign
|
||
---
|
||
|
||
# Convoy: liquid-glass-layout-shell
|
||
|
||
Sub-convoy #4 of the `liquid-glass-redesign` epic. This is the
|
||
**highest-blast-radius** PR in the portfolio because Layout is composed
|
||
by every authenticated page (and several anonymous ones —
|
||
`pages/invite/*.js` legitimately render Layout for anonymous visitors).
|
||
Treat with appropriate gating.
|
||
|
||
## Why
|
||
|
||
`components/Layout.js` is the single most-rendered component in the
|
||
app: sidebar nav, top header (search + theme toggle + profile
|
||
dropdown), mobile drawer, and now (per the user's ask) the warm-room
|
||
container that surrounds every page.
|
||
|
||
Under the current design, the sidebar is an opaque wood panel with a
|
||
warm-cream column. Under Liquid Glass, the sidebar becomes a tall
|
||
glass rail: the page content is dimly visible through it, the active
|
||
nav item has an ember rim, and the brand monogram is a glass pill with
|
||
inner ember gradient.
|
||
|
||
`MobileNavigation.js` already has a 16px backdrop-filter on the bottom
|
||
bar (`styles/globals.css` lines 800–819). That's the only place glass
|
||
exists today; this sub-convoy makes it canonical app-wide.
|
||
|
||
## Scope
|
||
|
||
### In scope
|
||
|
||
- `components/Layout.js`:
|
||
- Sidebar — wrapped in `<GlassSurface tint="mid" rim="subtle">`.
|
||
- Header — separate `<GlassSurface tint="mid" rim="subtle">` strip;
|
||
ember rim under the bottom edge to suggest "light cast onto the page".
|
||
- Logo pill — glass with inner ember gradient.
|
||
- `UserProfileDropdown` menu panel — `<GlassSurface tint="high"
|
||
rim="subtle" elevation="ambient">` (or pull out into a `<Popover>`
|
||
primitive — see Decision 4).
|
||
- `UserProfileDropdown` logged-out CTA — preserves the Sign-in link
|
||
+ monogram; `test/components/Layout.test.js` 5 assertions MUST stay
|
||
green.
|
||
- Nav-item active state — current `border-left: 3px solid
|
||
var(--accent-ember)` recipe stays as the *secondary* signal; new
|
||
primary signal is an inset ember rim on the active glass tile.
|
||
- `components/MobileNavigation.js`:
|
||
- Bottom bar — upgrade existing `mobile-nav-backdrop` rule to the
|
||
canonical `--glass-blur-mid` + `--glass-surface-mid` tokens (don't
|
||
re-implement on top — see Risk in umbrella).
|
||
- Mobile drawer — `<GlassSurface tint="low" rim="subtle"
|
||
elevation="pronounced">`.
|
||
- Bottom-bar active state — verify AA contrast (the existing finding
|
||
in `.convoys/ship-readiness.md` § Role-ux-reviewer: *"the
|
||
bottom-bar's active state contrast looks low in light mode"*).
|
||
- `styles/globals.css`:
|
||
- Update `.mobile-nav-backdrop` to reference the new tokens.
|
||
- Update `.theme-toggle`, `.header-icon`, `.logo-container`,
|
||
`.nav-item*` selectors to consume new tokens.
|
||
- **Do NOT delete** legacy color mappings yet — #8 handles that.
|
||
|
||
### Out of scope
|
||
|
||
- Form elements inside Layout (search bar, theme toggle as a `<Button>`,
|
||
profile dropdown items as `<Button variant="ghost">`) — these consume
|
||
primitives from sub-convoy #3; this convoy expects #3 to have shipped
|
||
first (NOT a hard `depends_on:` because the order doesn't strictly
|
||
block, but the resulting visual diff is cleaner if #3 ships first;
|
||
architect ratifies sequencing at gate-1).
|
||
- Per-page layout adjustments — out of scope; each page that needs
|
||
layout-conscious tweaks gets its own sub-convoy #6 brief.
|
||
- `<Popover>` / `<Menu>` primitive extraction for UserProfileDropdown —
|
||
defer to a follow-up convoy unless architect decides it's cheap to
|
||
bundle.
|
||
|
||
## Roles invoked
|
||
|
||
1. `role-architect` — Layout decomposition strategy (single brief vs
|
||
sidebar + header + mobile = 3 briefs).
|
||
2. `role-ux-reviewer` — sidebar rail vs header strip information
|
||
hierarchy; mobile drawer ergonomics.
|
||
3. `role-a11y-auditor` — nav contrast (bottom-bar active state — see
|
||
existing finding), focus-visible on every nav item, keyboard
|
||
operability.
|
||
4. `role-design-system-auditor` — token consumption verification.
|
||
5. `role-implementer` — 1–3 briefs per architect call.
|
||
6. Post-PR audit fleet.
|
||
|
||
## Architecture + Brief 1 (shipped 2026-06-03)
|
||
|
||
Targeted surgical glass migration of the 6 highest-leverage shell
|
||
surfaces; no structural refactor of nav data or routing.
|
||
|
||
**Surfaces converted:**
|
||
1. **Desktop sidebar rail** (`components/Layout.js` ~ line 714) —
|
||
`--glass-surface-mid` + `--glass-blur-mid` + `--glass-saturate`,
|
||
rim-light inner + outer + `--elevation-ambient`. The page background
|
||
visibly cools through the rail.
|
||
2. **Mobile drawer** (`components/Layout.js` ~ line 620) — same recipe
|
||
as the desktop rail, but with `--elevation-pronounced` (drawer is a
|
||
floating surface, not a docked rail).
|
||
3. **Mobile overlay scrim** (`components/Layout.js` ~ line 609) —
|
||
`--modal-scrim` + `--glass-blur-high` + saturate. Now visually
|
||
consistent with the `<Modal>` primitive's scrim.
|
||
4. **Search header strip** (`components/Layout.js` ~ line 794, only
|
||
when `showSearch`) — `--glass-surface-mid` + rim-light. The
|
||
`⌘F`-indicator + inline `<input>` stay intact (full SearchBar
|
||
primitive migration queued for follow-up).
|
||
5. **UserProfileDropdown popover menu** (`components/Layout.js` ~
|
||
line 86) — `--glass-surface-high` (popover ramp), ember-subtle
|
||
rim, ambient elevation. Now matches the popover composite recipe
|
||
in `docs/DESIGN_TOKENS.md`.
|
||
6. **MobileNavigation bottom bar background** (`components/MobileNavigation.js`
|
||
~ line 82) — replaced `mobile-nav-backdrop` legacy class + 0.95-alpha
|
||
rgba with glass-mid + rim-light. The raised "Dashboard" center
|
||
button's gradient is preserved untouched (it's a brand-accent
|
||
primary action, not a panel surface).
|
||
|
||
**Verification:** lint 0 errors; vitest 104/104 green; the 5 Layout
|
||
regression-lock tests (logged-out CTA, no maintainer-email default,
|
||
"Sign in" link present, supplied email renders, no "Guest" placeholder)
|
||
all preserved. No nav structure / routing / hook order changes.
|
||
|
||
**Follow-up (queued):** swap the header's inline `<input>` for the
|
||
`<SearchBar>` primitive (handles `⌘F` chip via `trailingAction`).
|
||
Tracked under `liquid-glass-form-primitives` Brief 2.
|
||
|
||
## Todos
|
||
|
||
- [ ] Architect: decomposition + sequencing call with #3
|
||
- [ ] UX reviewer: sidebar rail vs header strip
|
||
- [ ] A11y auditor: bottom-bar contrast + focus-visible audit
|
||
- [ ] Brief 1 — `components/Layout.js` (or per-architect partition)
|
||
- [ ] Brief 2 — `components/MobileNavigation.js` + CSS rule updates
|
||
- [ ] Post-PR audit per brief
|
||
- [ ] Re-seed Linux visual-diff baselines on merge
|
||
|
||
## Decisions to ratify (architect)
|
||
|
||
1. **Single-PR vs multi-brief.** Layout is 826 lines; a single PR
|
||
touches every authenticated page's visual diff. Trade-off:
|
||
- Single PR → atomic, but baselines must re-seed for every page in
|
||
one shot.
|
||
- Multi-brief (e.g. sidebar / header / mobile) → smaller diffs but
|
||
three baseline re-seeds.
|
||
Recommended default: **single PR** if Brief 1 stays under ~400 LOC
|
||
of changes; multi-brief otherwise.
|
||
2. **Sidebar rail visual style** — full-height glass column vs
|
||
"floating" inset glass card with margin. Recommended: full-height
|
||
column (matches existing nav rail; less reflow).
|
||
3. **Header strip height** — current is 64px desktop / 56px mobile.
|
||
Confirm or re-tune.
|
||
4. **UserProfileDropdown as `<Popover>` primitive** — extract or
|
||
inline. Recommended: **inline** for this convoy; defer primitive
|
||
extraction to a follow-up if a second popover surface appears.
|
||
5. **Mobile bottom-bar active-state contrast** — re-tune the existing
|
||
token used or introduce a new `--nav-active-text` token. Closes the
|
||
ship-readiness finding.
|
||
6. **Brand monogram pill** — keep "DH" glyph + add inner ember
|
||
gradient (recommended) vs replace with `AnimatedFireLogo`
|
||
(rejected — too motion-heavy on every page chrome).
|
||
|
||
## Acceptance criteria
|
||
|
||
1. Sidebar, header, mobile bottom bar, mobile drawer all render as
|
||
glass surfaces consuming sub-convoy #1's tokens.
|
||
2. `test/components/Layout.test.js` 5 assertions all pass.
|
||
3. Smoke spec 2 (`'sign-in page renders'`) passes on the preview.
|
||
4. A11y: bottom-bar active state hits AA contrast (closes the
|
||
ship-readiness finding); every nav item has `:focus-visible` ring;
|
||
every interactive non-`<button>` has `tabIndex={0}` + `onKeyDown`.
|
||
5. Lint + vitest + smoke green.
|
||
6. Linux visual-diff baselines re-seeded post-merge.
|
||
|
||
## CI impact
|
||
|
||
| Workflow / job | Behavior |
|
||
| --- | --- |
|
||
| `preview-smoke.yml` | Fires; smoke spec 2 (`'sign-in page renders'`) defends the logged-out Sign-in CTA branch (PR #15's regression-lock). |
|
||
| `visual-diff.yml` | **Fires + LOUDEST in portfolio** — Layout on every page. Baselines re-seed mandatory post-merge. |
|
||
| `lint` | Fires. |
|
||
| `test:` (vitest) | Fires + 5 Layout assertions defended. |
|
||
| New grep gates | Consider `forbidden-sidebar-hardcoded-color` post-merge (if hex creeps back in). Architect's call. |
|
||
|
||
## Known constraints
|
||
|
||
- **No regression in Layout's logged-out branch.** PR #15
|
||
(`fix-layout-default-user`, `ca302a8`, 2026-05-24) made
|
||
`UserProfileDropdown` render a Sign-in CTA when `user === null`.
|
||
That branch is defended by 5 vitest assertions AND smoke spec 2.
|
||
Both must stay green. The migration to glass is purely visual; the
|
||
branch logic is sacrosanct.
|
||
- **Hook order** — the existing comment at `Layout.js` lines 9–10
|
||
reads: *"Hook order is fixed for both branches; do not move this
|
||
below the null-user early return — see rules-of-hooks (AGENTS.md
|
||
Gotcha #11.5)."* Honour this.
|
||
- **`components/Layout.js.backup`** — the legacy snapshot listed in
|
||
`.cursor/rules/no-go-zones.mdc`. Do not edit.
|
||
- **Mobile safe-area** — `env(safe-area-inset-bottom)` still respected;
|
||
glass bottom bar must not break iOS notch handling.
|
||
- **`prefers-reduced-motion`** — any nav-item transition must respect
|
||
it (existing pattern at `styles/globals.css` lines 261–266).
|
||
|
||
## Multitask dispatch
|
||
|
||
Pre-ratification proposal (architect to revise):
|
||
|
||
```yaml
|
||
slice_dependencies:
|
||
- brief: 1
|
||
depends_on: []
|
||
files:
|
||
- components/Layout.js
|
||
- styles/globals.css # (selector consumers; NOT new tokens)
|
||
- brief: 2
|
||
depends_on: []
|
||
files:
|
||
- components/MobileNavigation.js
|
||
- styles/globals.css # (mobile-nav-backdrop rule)
|
||
```
|
||
|
||
Briefs 1 + 2 share `styles/globals.css`; architect must split or
|
||
serialize accordingly (likely serialize: Brief 1 first, Brief 2 picks
|
||
up `styles/globals.css` in HEAD state after Brief 1).
|
||
|
||
Post-PR audit per brief:
|
||
|
||
```
|
||
/multitask role-reviewer + role-design-system-auditor + role-a11y-auditor
|
||
```
|
||
|
||
## Out of scope follow-ups
|
||
|
||
- **`<Popover>` primitive extraction** — if a second popover surface
|
||
appears in #5 or #6.
|
||
- **Sidebar collapse/expand on desktop** — UX feature, not a redesign
|
||
concern. Surface only if user requests.
|
||
- **Skip-to-content link** — flagged in ship-readiness § Role-a11y-
|
||
auditor (*"no `<a href="#main" class="sr-only focus:not-sr-only">`"*).
|
||
Cheap; fold into this convoy's a11y brief if architect agrees.
|