deckhearth/.convoys/unify-glass-panel-surfaces.md
Randall Stillwell b195395b82 docs(convoys): seed unify-glass-panel-surfaces + cleanup-card-item-list-and-share-modal-palette
Two convoy seeds opened as follow-ups to the 2026-06-04 design pass
(#116 corner-border-light → #117 site-wide sweep → #118 card vibrancy
reduction). Both were called out in #117's PR body as deferred and are
now formally tracked.

## unify-glass-panel-surfaces

Migrates remaining panel-shaped surfaces to the gradient-border
corner-light treatment so the app shares one surface vocabulary.

The audit's key insight: `<GlassSurface>` (`components/ui/GlassSurface.js`)
predates the corner-light pattern. Because `<Modal>`, `<StatCard>`,
and the landing-page feature/collection cards all delegate to it,
upgrading the primitive cascades to ~10 visible surfaces at once.

7 briefs, multitask-parallel after Brief 1 lands:

1. `<GlassSurface>` primitive upgrade — BLOCKING for 3, 4
2. Auth form cards (login.js, signup.js)
3. Floating popovers (mobile drawer, sidebar profile dropdown,
   UserMenu dropdown)
4. BulkSelectionToolbar (currently `bg-white border-gray-200` —
   invisible in dark mode)
5. `.card`-class consumers (4 pages); decision to ratify whether
   to retire `.card` entirely or keep as documented opaque fallback
6. Landing nav bar — wrong pattern; should use existing
   `.page-header-glass` class
7. `forbidden-bespoke-glass-surface` CI grep gate — prevents
   regression after the migration ships

## cleanup-card-item-list-and-share-modal-palette

Targeted palette cleanup for two files whose interiors weren't
addressed in #117:

1. `CardItem.js` list-mode (L183–284) — entirely hardcoded
   Tailwind palette (`bg-purple-50`, `border-gray-200`, `text-gray-{500-900}`,
   `bg-blue-100 text-blue-800` etc.); unreadable / off-brand in dark mode.
2. `ShareModal.js` interior rows — purple avatar circles, gray-50
   permission row (invisible in dark mode), blue-600 Copy-link button,
   gray text labels.

Token-only swap. 2 parallel briefs, no architect / IA / UX needed
(no design decisions — palette to design tokens).

## Sequencing note

The two convoys are independent and can run in parallel. The audit
agent's recommended sequencing (Brief 1 of `unify-glass-panel-surfaces`
first) is encoded in the multitask `slice_dependencies` blocks.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 14:06:53 -05:00

363 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
name: unify-glass-panel-surfaces
classification: feature
success_metric: |
Every panel-shaped surface in the app (modals, popovers, form
cards, dashboard widgets, page-level content cards) renders with
the same gradient-border corner-light treatment that the floating
chrome chips use — at the appropriate intensity tier for its
surface class (full intensity for chrome, subtle for data cards,
flat for opaque GPU-budget-constrained tiles like CardItem grid).
Verified by visual diff + a forbidden grep gate that prevents
reintroduction of bespoke `var(--glass-surface-*)` inline styles
outside the documented exception list.
skip:
- ia
status: open
created: 2026-06-04
depends_on:
- tone-down-card-corner-lights # PR #118 — establishes the subtle token tier
- design-sweep-pass # PR #117 — applies .glass-panel broadly
---
# Convoy: unify-glass-panel-surfaces
Follow-on to the `redesign-v2-from-mockups` umbrella and the
2026-06-04 corner-border-light refinement (PR #116) + design-sweep
(PR #117) + card-vibrancy reduction (PR #118).
## Why
The gradient-border corner-light pattern (introduced in PR #116 and
applied broadly in PR #117) is now the canonical surface treatment
for the app. But the audit run on 2026-06-04 found that several
panel-shaped surfaces still use the previous generation's "flat
translucent fill" pattern — they predate the corner-light work and
were never migrated:
1. **`<GlassSurface>` primitive** — the most consequential gap.
`components/ui/GlassSurface.js` uses a single flat
`var(--glass-surface-${tint})` background with no transparent
border and no corner radials. Because `<Modal>` (and therefore
every modal in the app), `<StatCard>` (dashboard stat tiles),
and the public landing-page feature/collection cards all delegate
to `<GlassSurface>`, the gap cascades broadly. Upgrading the
primitive fixes ~10 visible surfaces in one move.
2. **Auth form cards** (`pages/login.js` L8288,
`pages/signup.js` L227232) — handrolled glass imitation using
`rgba(--bg-secondary-rgb, 0.85)` + `backdrop-blur-sm` (Tailwind,
not the system blur tokens). These are the first surfaces a new
user sees; they should be canonical, not handrolled.
3. **Floating popovers / drawers**
- `components/Layout.js` mobile drawer (L698709)
- `components/Layout.js` sidebar profile dropdown (L8896)
- `components/ui/TopSearchBar.js` UserMenu dropdown (L214222)
All three render a translucent panel over arbitrary page content.
None has the gradient-border treatment. The visual cue that
says "this is an elevated surface" relies entirely on box-shadow,
not on light response.
4. **`BulkSelectionToolbar`** (L47 and L117 dropdown) — uses
hardcoded `bg-white border-gray-200`, invisible in dark mode.
This is a floating toolbar over all content during bulk-select;
should be `.glass-panel-strong`.
5. **`.card`-class consumers** — `pages/profile.js` (×3),
`pages/settings.js` (×2), `pages/community/collections.js`,
`components/CollectionsPageView.js`. The `.card` class
(`styles/globals.css` L757) is a pre-redesign opaque
solid-fill panel. Most consumers should migrate to
`.glass-panel`; the class itself can either be retired or
retained as a documented "opaque fallback" for special cases.
6. **Landing nav bar** (`pages/index.js` L6172) — uses
`var(--glass-surface-mid)` flat + an incorrectly double-wrapped
`inset 0 1px 0` boxShadow. Not a gradient-border candidate
(full-bleed bar; corner lights would be at viewport edges, not
visible). Should use the existing `.page-header-glass` class
instead.
Unifying these means the entire app shares one surface vocabulary.
Future work — new modals, new dashboards, new public pages — picks
up the gradient-border treatment automatically because the
primitives are correct.
## Scope
### In scope
#### Brief 1 — Upgrade `<GlassSurface>` primitive
Add the 4-layer gradient-border pattern to the `<GlassSurface>`
component so its `tint`, `blur`, `rim`, `elevation` props compose
with corner catch-lights. Default to the **subtle** corner-light
tokens (`--corner-light-{warm,cool}-subtle`) since most consumers
are data cards. Add a `cornerLights="chrome" | "subtle" | "none"`
prop so floating chrome can opt up and special opaque tiles
(CardItem grid mode) can opt out.
Consumers automatically upgraded:
- `<Modal>` → corner lights on every modal
- `<StatCard>` → corner lights on dashboard stat tiles
- Landing feature cards (`pages/index.js` L132224, L278345)
**Files:** `components/ui/GlassSurface.js`,
`test/components/ui-primitives.test.js` (add a corner-light
assertion).
**Risk:** MED — the primitive's output structure changes (adds
`border: 1px solid transparent`). Most consumers won't notice, but
any consumer that set a custom `border` via style override could
conflict. Audit needed.
#### Brief 2 — Auth form cards
`pages/login.js` L8288 + `pages/signup.js` L227232: replace
the inline `className="p-8 rounded-2xl shadow-2xl backdrop-blur-sm
border border-opacity-20" style={{ backgroundColor: 'rgba(...)' }}`
shape with `className="glass-panel-strong rounded-2xl p-8"`.
**Files:** `pages/login.js`, `pages/signup.js`.
**Risk:** LOW.
#### Brief 3 — Floating popovers / drawers
Three independent floating surfaces:
- `components/Layout.js` mobile drawer (L698709)
- `components/Layout.js` sidebar profile dropdown (L8896)
- `components/ui/TopSearchBar.js` UserMenu dropdown (L214222)
For each: drop the inline `background: var(--glass-surface-*)` +
`backdropFilter` pair, add `className="glass-panel-strong rounded-2xl"`
or `rounded-xl` to match existing dimensions. Preserve any
additional `boxShadow: 'var(--ember-rim-subtle)'` adornments by
chaining them onto the class's existing box-shadow (via inline
style override).
**Special handling for mobile drawer:** the drawer takes ~1/3 of
the viewport. At full corner-light intensity it would be noisy.
Use `.glass-panel-strong` (which uses the subtle tokens).
**Files:** `components/Layout.js`, `components/ui/TopSearchBar.js`,
`test/components/Layout.test.js` (regression assertions).
**Risk:** LOWMED — popovers appear over arbitrary page content.
#### Brief 4 — `BulkSelectionToolbar`
Replace `bg-white border-gray-200` on the toolbar (L47) and its
"More actions" dropdown (L117) with `.glass-panel-strong rounded-2xl`.
Sweep interior `text-gray-700 hover:bg-gray-100`,
`text-red-600 hover:bg-red-50` to use `var(--text-primary)` /
`nav-item-hover` and tokenized red for destructive actions.
**Files:** `components/BulkSelectionToolbar.js`.
**Risk:** MED — floats over all content during bulk-select mode;
shadow/overflow bleed would be very visible.
#### Brief 5 — `.card`-class consumers
Audit every `<div className="card">` usage. For each, pick the
right migration:
- Most likely: `glass-panel rounded-3xl p-6` (preserves rounded-3xl
+ p-6 from the old class).
- Some may need to stay opaque (e.g. screenshot-friendly profile
card with photo overlay) — keep `.card` and document the
exception in `styles/globals.css`'s comment block.
Decision to ratify: **retire `.card` entirely, OR keep as a
documented opaque-panel alternative?** Architect's call. If kept,
add a comment to `globals.css` explaining when to use which.
**Files:** `pages/profile.js`, `pages/settings.js`,
`pages/community/collections.js`,
`components/CollectionsPageView.js`,
optionally `styles/globals.css` (retire or document).
**Risk:** LOW per page; sweep across 4 files.
#### Brief 6 — Landing nav bar
`pages/index.js` L6172: replace the handrolled translucent header
with `<header className="page-header-glass border-b ...">`. The
`.page-header-glass` class already exists in `globals.css` for
exactly this "full-bleed top band" use case. The current bar also
has a malformed `boxShadow: 'inset 0 1px 0 var(--rim-light-inner)'`
(the token already contains `inset 0 1px 0`; double-wrapping breaks
the cascade). Remove the malformed shadow.
**Files:** `pages/index.js`.
**Risk:** LOW — public page, no auth dependencies.
#### Brief 7 — Forbidden grep gate
Add a `forbidden-bespoke-glass-surface` job to
`.github/workflows/ci.yml` that fails the build if
`var(--glass-surface-(low|mid|high))` appears in JSX inline styles
across `components/**` and `pages/**`, with a curated allowlist
for the documented exceptions (intentional chrome treatment in
Layout/TopSearchBar; `<GlassSurface>` component itself; any
opaque-fallback `.card` consumers ratified in Brief 5).
Prevents regression: future PRs can't reintroduce handrolled glass.
**Files:** `.github/workflows/ci.yml`.
**Risk:** LOW.
### Out of scope
- `<CardItem>` grid-mode (L290303) — intentionally opaque per
AGENTS.md GPU-budget rule.
- `<CardItem>` list-mode token cleanup (L183284) — covered by
the parallel `cleanup-card-item-list-and-share-modal-palette`
convoy.
- Adding any NEW surfaces or panels.
- `.card` rename to `.opaque-panel` — Brief 5 may retire the class
entirely; rename is a separate polish if it survives.
## Roles invoked
1. `role-architect` — surface-by-surface migration plan; ratifies
the `cornerLights` prop shape for `<GlassSurface>` and the
`.card` retire-vs-keep decision; writes Briefs 17.
2. `role-design-system-auditor` — visual regression review on each
brief; confirms no chrome-vs-data-card hierarchy regressions.
3. `role-ux-reviewer` — light pass; ensures auth form contrast +
mobile drawer legibility hold up post-migration.
4. `role-implementer` — one per brief; Briefs 2, 3, 4, 6 are
parallel-safe (disjoint files, no shared component dependency
chain). Brief 1 MUST land first because Briefs 3, 4 may end up
simplifying their inline-style code by using the upgraded
primitive instead.
5. `role-reviewer` — single post-PR review per brief.
6. `role-a11y-auditor` — focus-ring + keyboard nav for new floating
surfaces (Brief 3 popovers especially).
## Todos
- [ ] Architect: surface-by-surface migration plan + `<GlassSurface>`
API spec (the `cornerLights` prop)
- [ ] Design-system auditor: confirm subtle-tokens are the right
default for the upgraded `<GlassSurface>`
- [ ] Brief 1 — `<GlassSurface>` primitive upgrade (BLOCKING for
Briefs 3, 4)
- [ ] Brief 2 — auth form cards
- [ ] Brief 3 — floating popovers (drawer, sidebar profile,
UserMenu)
- [ ] Brief 4 — BulkSelectionToolbar
- [ ] Brief 5 — `.card` consumers; Decision: retire or keep
- [ ] Brief 6 — landing nav bar (`.page-header-glass`)
- [ ] Brief 7 — `forbidden-bespoke-glass-surface` CI gate
- [ ] Post-PR review per brief
- [ ] Visual-diff baseline refresh after Brief 1 lands
## Decisions to ratify
1. **`<GlassSurface>` API shape post-upgrade.** Add `cornerLights`
prop with values `"chrome" | "subtle" | "none"`. Default
`"subtle"`. Architect confirms or proposes alternative.
2. **Retire `.card` class entirely after Brief 5, or keep as
documented opaque fallback?** Conservative: keep + document
("use when the surface must NOT have backdrop-filter — e.g.
inside another modal, screen-reader-critical, or
GPU-budget-constrained"). Aggressive: retire and migrate the
handful of legitimate opaque cases to inline style.
3. **Mobile drawer corner-light intensity.** The drawer is a
large surface. Subtle tokens (matching `.glass-panel-strong`)
are likely right, but the architect should sanity-check
visually before locking it in.
4. **CI gate scope for `forbidden-bespoke-glass-surface`.** Should
it gate ALL `var(--glass-surface-*)` usage in JSX, or only
inline `style={{ background: ... }}` usage? Recommended: only
inline styles, since the tokens still need to be referenceable
in `styles/globals.css`.
## Acceptance criteria
1. Every in-scope surface lists either `glass-panel`,
`glass-panel-strong`, `page-header-glass`, or `<GlassSurface>`
in its className (no inline `var(--glass-surface-*)` background).
2. `<GlassSurface>` primitive's output includes the 4-layer
gradient-border pattern and a `border: 1px solid transparent`.
3. CI's `forbidden-bespoke-glass-surface` job passes; introducing
a new bespoke `var(--glass-surface-low)` inline-style usage in
a scratch commit makes it fail (negative test).
4. Build + lint + 113/113 vitest + Playwright smoke green.
5. Visual diff shows the expected differences (corner catch-lights
appear on modals, dashboard stat tiles, auth cards, dropdowns)
and no unexpected regressions on chrome/cards/CardItem grid.
## CI impact
| Workflow / job | Behavior |
| --- | --- |
| `preview-smoke.yml` | Fires per brief. |
| `visual-diff.yml` | **Fires + baseline refresh required** after Brief 1 lands (the primitive upgrade ripples through Modal/StatCard/landing). |
| `lint` | Fires + new `forbidden-bespoke-glass-surface` gate after Brief 7. |
| `test:` (vitest) | Fires; Brief 1 adds a corner-light assertion to `ui-primitives.test.js`. |
## Known constraints
- **No-go zones honoured** — `components/Layout.js.backup`,
`scripts/add-*.js` graveyard untouched.
- **GPU budget for CardItem grid** — AGENTS.md explicitly
prohibits `backdrop-filter` per card thumbnail (it
multiplies); Brief 1 must NOT default `<GlassSurface>` to
any rendering that would pull CardItem into that prohibition.
## Multitask dispatch
```yaml
slice_dependencies:
- brief: 1
depends_on: []
files:
- components/ui/GlassSurface.js
- test/components/ui-primitives.test.js
- brief: 2
depends_on: []
files:
- pages/login.js
- pages/signup.js
- brief: 3
depends_on: [1] # may simplify by using upgraded primitive
files:
- components/Layout.js
- components/ui/TopSearchBar.js
- test/components/Layout.test.js
- brief: 4
depends_on: [1]
files:
- components/BulkSelectionToolbar.js
- brief: 5
depends_on: []
files:
- pages/profile.js
- pages/settings.js
- pages/community/collections.js
- components/CollectionsPageView.js
- styles/globals.css # if .card is retired / documented
- brief: 6
depends_on: []
files:
- pages/index.js
- brief: 7
depends_on: [1, 2, 3, 4, 5, 6] # gate goes in LAST
files:
- .github/workflows/ci.yml
```
Briefs 2, 5, 6 can run in parallel with Brief 1. Briefs 3, 4 wait
for Brief 1 to land so they can simplify by using the upgraded
primitive. Brief 7 runs LAST so the grep gate doesn't fail the
build on in-flight migrations.
## Out of scope follow-ups (queued)
- **`retire-or-formalize-card-class`** — if Brief 5 Decision 2
keeps `.card` as opaque fallback, a future small convoy can
rename it `.opaque-panel` for clarity. P3.
- **`storybook-adoption-for-glass-surface`** — once the primitive
has the full prop matrix (`tint`, `blur`, `rim`, `elevation`,
`cornerLights`), it's a natural Storybook candidate. P2 DX.