364 lines
15 KiB
Markdown
364 lines
15 KiB
Markdown
|
|
---
|
|||
|
|
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` L82–88,
|
|||
|
|
`pages/signup.js` L227–232) — 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 (L698–709)
|
|||
|
|
- `components/Layout.js` sidebar profile dropdown (L88–96)
|
|||
|
|
- `components/ui/TopSearchBar.js` UserMenu dropdown (L214–222)
|
|||
|
|
|
|||
|
|
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` L61–72) — 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` L132–224, L278–345)
|
|||
|
|
|
|||
|
|
**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` L82–88 + `pages/signup.js` L227–232: 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 (L698–709)
|
|||
|
|
- `components/Layout.js` sidebar profile dropdown (L88–96)
|
|||
|
|
- `components/ui/TopSearchBar.js` UserMenu dropdown (L214–222)
|
|||
|
|
|
|||
|
|
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:** LOW–MED — 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` L61–72: 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 (L290–303) — intentionally opaque per
|
|||
|
|
AGENTS.md GPU-budget rule.
|
|||
|
|
- `<CardItem>` list-mode token cleanup (L183–284) — 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 1–7.
|
|||
|
|
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.
|