docs(convoys): unify-glass-panel-surfaces + cleanup palette — conductor seed + architect plan (#119)
* 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> * docs(convoys): architect plan + 7 briefs for unify-glass-panel-surfaces Appends the ## Architecture section to .convoys/unify-glass-panel-surfaces.md ratifying the 4 open Decisions, and writes 7 implementer-brief files under .convoys/unify-glass-panel-surfaces/. Decisions ratified: D1. <GlassSurface> grows a cornerLights='subtle'|'chrome'|'none' prop, default 'subtle' (4-layer gradient-border per PR #118 recipe). D2. Retire .card entirely; migrate all 8 consumers to .glass-panel. D3. Mobile drawer uses subtle tier (.glass-panel-strong). D4. CI gate scopes to JSX inline-style usage only; 3-entry allowlist for documented chrome exceptions. Briefs (1-7, all <100 LOC each, disjoint files): 1. Upgrade <GlassSurface> primitive (no deps, blocks 3+4) 2. Migrate auth form cards (parallel-safe) 3. Migrate floating popovers (sidebar dropdown, mobile drawer, TopSearchBar UserMenu) 4. Migrate BulkSelectionToolbar + interior token sweep 5. Retire .card class; migrate 8 consumers 6. Migrate landing nav bar to .page-header-glass 7. forbidden-bespoke-glass-surface CI gate (runs LAST) Boot-the-brief check: all 4-layer recipes verified against post-PR-#118 styles/globals.css; all box-shadow preservation chains verified against current Layout.js + TopSearchBar.js; .card deletion verified clean (single rule at L729-733); CI gate shape modeled on existing forbidden-modal-shell-without-primitive job. Architecture is read-only output — no application code touched. Awaiting human gate 1 (plan approval) before implementers run. Co-authored-by: Cursor <cursoragent@cursor.com> * docs(convoys): fix Brief 3 TopSearchBar UserMenu elevation token Self-review recheck against current TopSearchBar.js (L211-222) found that the UserMenu dropdown uses var(--elevation-pronounced), not var(--elevation-ambient) as Brief 3's initial sample showed. Brief 3 now documents the verbatim shape (pronounced + role="menu" + aria-label + overflow-hidden) and the convoy's risk-list note is updated to reflect all three popovers' verbatim chains. No semantic change to the plan; just an accuracy fix the implementer of Brief 3 needs to ship the right code on the first try. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
2986171e6d
commit
66a4d7b721
9 changed files with 1598 additions and 0 deletions
205
.convoys/cleanup-card-item-list-and-share-modal-palette.md
Normal file
205
.convoys/cleanup-card-item-list-and-share-modal-palette.md
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
---
|
||||
name: cleanup-card-item-list-and-share-modal-palette
|
||||
classification: feature
|
||||
success_metric: |
|
||||
No hardcoded Tailwind palette classes (`bg-purple-*`, `bg-blue-*`,
|
||||
`bg-gray-*` 100-900 range, `text-gray-{500,600,700,900}`,
|
||||
`bg-white`, `border-gray-200`, etc.) remain in the interior of
|
||||
`components/CardItem.js` list-mode or `components/ShareModal.js`.
|
||||
Both render correctly in dark theme. Verified by visual diff and
|
||||
a targeted lint sweep.
|
||||
skip:
|
||||
- ia
|
||||
- ux
|
||||
- arch
|
||||
status: open
|
||||
created: 2026-06-04
|
||||
depends_on:
|
||||
- design-sweep-pass # PR #117 — established the token migration pattern
|
||||
---
|
||||
|
||||
# Convoy: cleanup-card-item-list-and-share-modal-palette
|
||||
|
||||
Targeted palette cleanup. Parallel companion to
|
||||
`unify-glass-panel-surfaces` — that convoy migrates panel *surfaces*;
|
||||
this one cleans up the *interior content* (rows, badges, text,
|
||||
buttons) of two specific files whose interiors were not addressed
|
||||
by PR #117 because they're not panel surfaces — they're nested
|
||||
content inside surfaces that were already on the system.
|
||||
|
||||
## Why
|
||||
|
||||
Two files left over after the 2026-06-04 design sweep:
|
||||
|
||||
1. **`components/CardItem.js` list-mode** (L183–284) — the
|
||||
list-view layout (when `viewMode === 'list'`) is entirely
|
||||
hardcoded Tailwind palette. The grid-mode (L290+) was already
|
||||
migrated; list-mode wasn't. `border-gray-200`, `bg-purple-50`,
|
||||
`text-gray-900`, `text-gray-600`, `text-green-600`,
|
||||
`bg-blue-100 text-blue-800` — none of these have dark-mode
|
||||
variants. In dark theme, the list view renders unreadable
|
||||
(light text on light backgrounds) or off-brand (purple/blue
|
||||
badges in an ember palette app).
|
||||
|
||||
2. **`components/ShareModal.js`** interior — the modal SURFACE is
|
||||
correct (delegates to `<Modal>` → `<GlassSurface>`), and the
|
||||
user-search dropdown + email-invite card were updated in PR
|
||||
#117. But the interior rows still have:
|
||||
- `bg-purple-600` avatar circles for current user (L194, L240).
|
||||
- `bg-gray-50` current-user permission row (L238) — invisible
|
||||
in dark mode.
|
||||
- `bg-gray-100 rounded` permission badge (L272) — light-only.
|
||||
- `bg-gray-50 border-gray-300 text-gray-600` share-link input
|
||||
(L287) — invisible in dark mode.
|
||||
- `bg-blue-600 hover:bg-blue-700` Copy-link button (L294) —
|
||||
off-palette (should be ember).
|
||||
- `text-gray-{500,700,900}` text colors throughout — light-only.
|
||||
|
||||
Note ShareModal is the primary collaboration entry point; the
|
||||
permission rows and Copy-link button are visited every time a
|
||||
user invites a collaborator.
|
||||
|
||||
Both files are token-only swaps. No structural changes, no
|
||||
component swaps, no glass-panel migration.
|
||||
|
||||
## Scope
|
||||
|
||||
### In scope
|
||||
|
||||
#### Brief 1 — `CardItem.js` list-mode token sweep
|
||||
|
||||
Replace every hardcoded color in lines ~183–284 with the
|
||||
appropriate `var(--*)` token via inline style. Categories:
|
||||
|
||||
- Background fills: `bg-purple-50` (selected row), `bg-white` →
|
||||
`var(--bg-secondary)` or `var(--bg-tertiary)` per role; selected
|
||||
state uses `var(--accent-ember)` + low alpha as background.
|
||||
- Borders: `border-gray-200` / `border-gray-300` →
|
||||
`var(--border)`. Selected state border `border-purple-600` →
|
||||
`var(--accent-ember)`.
|
||||
- Text: `text-gray-900` → `var(--text-primary)`,
|
||||
`text-gray-600` / `text-gray-500` → `var(--text-secondary)`,
|
||||
`text-green-600` (positive value indicators) →
|
||||
`var(--accent-flame)` or keep `#16a34a` if semantic green
|
||||
matters more than brand.
|
||||
- Badges (`bg-blue-100 text-blue-800` etc.): convert to
|
||||
`var(--bg-tertiary)` + `var(--text-primary)` or
|
||||
`var(--accent-ember)` per badge role (game, rarity, type).
|
||||
- Buttons / icons: `text-purple-600` (selection indicator) →
|
||||
`var(--accent-ember)`.
|
||||
|
||||
Acceptance: visually inspect `CardItem` list-mode in BOTH themes.
|
||||
All text must be legible, all interactive states (hover, selected)
|
||||
must have a visible delta against the row background.
|
||||
|
||||
**Files:** `components/CardItem.js`.
|
||||
**Risk:** LOW — purely cosmetic token swaps; no layout changes.
|
||||
|
||||
#### Brief 2 — `ShareModal.js` interior token sweep
|
||||
|
||||
Lines roughly 230–310. Targets:
|
||||
|
||||
- **Avatar circles** (L194, L240): `bg-purple-600` →
|
||||
`linear-gradient(135deg, var(--accent-ember), var(--accent-flame))`
|
||||
(matches the UserMenu avatar gradient style from
|
||||
`components/ui/TopSearchBar.js`).
|
||||
- **Current-user row container** (L238): `bg-gray-50 rounded-lg`
|
||||
→ `var(--bg-secondary)` + `rounded-xl`, or
|
||||
`var(--bg-tertiary)` for slightly more contrast.
|
||||
- **Permission badge** (L272): `bg-gray-100 rounded` →
|
||||
`var(--bg-tertiary)` + `rounded-xl` + `var(--text-secondary)`.
|
||||
- **Share-link readonly input** (L283–288): `bg-gray-50
|
||||
border-gray-300 text-gray-600` → tokenize against
|
||||
`var(--bg-secondary)`, `var(--border)`, `var(--text-primary)`;
|
||||
alternatively use the `.input-field` class (already in
|
||||
`globals.css`).
|
||||
- **Copy-link button** (L290–298): `bg-blue-600 text-white
|
||||
hover:bg-blue-700` → replace with the `<Button variant="primary">`
|
||||
primitive. The success state currently is
|
||||
`bg-green-100 text-green-800 border border-green-200`; keep the
|
||||
semantic green but use a non-Tailwind path:
|
||||
`style={{ backgroundColor: 'var(--bg-secondary)', color:
|
||||
'var(--accent-flame)' }}` + an ember border, or commit to a
|
||||
custom `.btn-success-flash` utility.
|
||||
- **Text labels** (L201, L222, L231, L246, L266, L267): all
|
||||
`text-gray-{500,700,900}` → tokens.
|
||||
|
||||
Acceptance: ShareModal opens in dark theme and every row is
|
||||
legible; Copy-link button is on the ember palette; permission
|
||||
badges read as ember-system, not Tailwind-default-gray.
|
||||
|
||||
**Files:** `components/ShareModal.js`.
|
||||
**Risk:** LOW.
|
||||
|
||||
### Out of scope
|
||||
|
||||
- Any change to the modal *surface* (handled by
|
||||
`unify-glass-panel-surfaces` Brief 1, which upgrades
|
||||
`<GlassSurface>` and therefore `<Modal>`).
|
||||
- Any structural change (new component, new layout).
|
||||
- The user-search-dropdown + email-invite-card areas of
|
||||
`ShareModal.js` — those were already cleaned up in PR #117.
|
||||
|
||||
## Roles invoked
|
||||
|
||||
1. `role-implementer` — Briefs 1 and 2 are parallel-safe; can run
|
||||
independently.
|
||||
2. `role-design-system-auditor` — single sign-off after both
|
||||
briefs land (token-only sweep; design audit is a sanity check).
|
||||
3. `role-reviewer` — one review per brief.
|
||||
|
||||
(No architect needed — token swaps with no design decisions.
|
||||
No IA, no UX — content/structure unchanged. The Design-system
|
||||
auditor catches any palette regressions.)
|
||||
|
||||
## Todos
|
||||
|
||||
- [ ] Brief 1 — `CardItem.js` list-mode token sweep
|
||||
- [ ] Brief 2 — `ShareModal.js` interior token sweep
|
||||
- [ ] Design-system auditor sign-off
|
||||
- [ ] Post-PR review per brief
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
1. `rg -e "bg-(purple|blue|gray|red)-[0-9]" -e
|
||||
"text-(purple|blue|gray|red)-[0-9]" -e "border-gray-[0-9]"`
|
||||
over `components/CardItem.js` and `components/ShareModal.js`
|
||||
returns no matches (excluding lines that are deliberately
|
||||
semantic — e.g. `bg-red-500` on a destructive Delete button,
|
||||
which architect ratifies per-line during review).
|
||||
2. Both files render correctly in both themes — visible text,
|
||||
visible interactive states.
|
||||
3. Build + lint + 113/113 vitest pass.
|
||||
|
||||
## CI impact
|
||||
|
||||
| Workflow / job | Behavior |
|
||||
| --- | --- |
|
||||
| `preview-smoke.yml` | Fires. |
|
||||
| `visual-diff.yml` | **Fires** — expected diff in CardItem list-view and ShareModal rows. Baseline refresh required. |
|
||||
| `lint` | Fires. |
|
||||
| `test:` (vitest) | Fires. |
|
||||
|
||||
## Known constraints
|
||||
|
||||
- **`CardItem` is in many places** — `pages/cards.js` (grid
|
||||
default), `pages/my-cards.js`, `pages/collection/[id].js`, etc.
|
||||
Brief 1 must NOT touch the grid-mode (L290+), only the
|
||||
list-mode branch.
|
||||
|
||||
## Multitask dispatch
|
||||
|
||||
```yaml
|
||||
slice_dependencies:
|
||||
- brief: 1
|
||||
depends_on: []
|
||||
files:
|
||||
- components/CardItem.js
|
||||
- brief: 2
|
||||
depends_on: []
|
||||
files:
|
||||
- components/ShareModal.js
|
||||
```
|
||||
|
||||
Briefs 1 and 2 are fully parallel — disjoint files, no shared
|
||||
dependencies.
|
||||
648
.convoys/unify-glass-panel-surfaces.md
Normal file
648
.convoys/unify-glass-panel-surfaces.md
Normal file
|
|
@ -0,0 +1,648 @@
|
|||
---
|
||||
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.
|
||||
|
||||
## Architecture
|
||||
|
||||
Run date: 2026-06-04. Architect: role-architect (this convoy).
|
||||
Reads: this convoy file, `AGENTS.md`, `.cursor/rules/*.mdc`,
|
||||
`components/ui/GlassSurface.js`, `components/ui/Modal.js`,
|
||||
`components/ui/StatCard.js`, `components/Layout.js`,
|
||||
`components/ui/TopSearchBar.js`, `components/BulkSelectionToolbar.js`,
|
||||
`pages/login.js`, `pages/signup.js`, `pages/index.js`,
|
||||
`pages/profile.js`, `pages/settings.js`,
|
||||
`pages/community/collections.js`,
|
||||
`components/CollectionsPageView.js`, `styles/globals.css`
|
||||
(`.card`, `.glass-panel`, `.glass-panel-strong`, `.page-header-glass`,
|
||||
the `--corner-light-*-subtle` tokens added in PR #118, the
|
||||
`--bg-secondary-rgb` token used by today's auth cards),
|
||||
`.github/workflows/ci.yml`, `test/components/ui-primitives.test.js`,
|
||||
`test/components/Layout.test.js`.
|
||||
|
||||
### Decisions ratified
|
||||
|
||||
The convoy file's four open Decisions are resolved as follows.
|
||||
Implementer briefs cite these and MUST NOT renegotiate them mid-flight
|
||||
(if a brief discovers a reason to revisit, escalate via the mid-convoy
|
||||
scope-expansion process in `role-architect.md`).
|
||||
|
||||
**D1. `<GlassSurface>` `cornerLights` prop shape.** Add
|
||||
`cornerLights = 'subtle' | 'chrome' | 'none'`, **default `'subtle'`**.
|
||||
- `'subtle'` → applies the 4-layer gradient with
|
||||
`--corner-light-warm-subtle` / `--corner-light-cool-subtle` (matches
|
||||
what `.glass-panel-strong` ships today; PR #118).
|
||||
- `'chrome'` → applies the 4-layer gradient with full-intensity
|
||||
`--corner-light-warm` / `--corner-light-cool`. Used by the Layout
|
||||
sidebar nav-chip and TopSearchBar header — those keep their inline
|
||||
styles for now; this prop value exists so future floating chrome
|
||||
doesn't have to re-handroll the gradient.
|
||||
- `'none'` → no transparent border, no radials, no `--chip-border-base`
|
||||
layer. Identical to today's primitive output. Used for the CardItem
|
||||
grid path and any other GPU-budget-constrained tile that legitimately
|
||||
must skip the gradient-border treatment.
|
||||
|
||||
The gradient-border technique is the verbatim 4-layer recipe from
|
||||
`.glass-panel-strong` (`styles/globals.css` post-PR-#118): a
|
||||
padding-box solid linear-gradient of the fill, two border-box radial
|
||||
gradients for the corner catch-lights (warm 0%↔100%, cool 100%↔0%),
|
||||
and a border-box `var(--chip-border-base)` base layer. Combined with
|
||||
`border: 1px solid transparent` so the gradient renders through the
|
||||
border.
|
||||
|
||||
**D2. `.card` class retire-or-keep.** **Retire the class entirely.**
|
||||
All 8 consumers migrate to `glass-panel rounded-3xl p-{6|4}`. The
|
||||
`.card` block in `styles/globals.css` is deleted. Rationale: a single
|
||||
glass vocabulary across the app is the convoy's success metric;
|
||||
keeping `.card` as a documented opaque fallback creates two parallel
|
||||
panel languages for future agents to choose between, which is exactly
|
||||
the kind of design-system bifurcation the convoy was scoped to
|
||||
eliminate. The GPU-budget concern that motivated keeping `.card` is
|
||||
specific to `<CardItem>` grid-mode (a non-`.card` consumer) — none
|
||||
of the 8 `.card` sites face that constraint (they're profile cards,
|
||||
settings panels, and a single community-page list tile, not a
|
||||
multiplied-per-thumbnail surface).
|
||||
|
||||
Aggressive path; **risk** = one of the 8 sites has a hidden reason
|
||||
to be opaque that the architect didn't catch. Mitigation: Brief 5
|
||||
includes a "if any consumer breaks visually post-migration, revert
|
||||
that one consumer to inline `style={{ background:
|
||||
'var(--bg-secondary)' }}` and document the exception in the brief's
|
||||
post-merge note" escape hatch. That keeps the class deletion in the
|
||||
PR while leaving an explicit per-site fallback. If that escape hatch
|
||||
fires for more than 1 of the 8 consumers, the brief's reviewer is
|
||||
expected to push back and propose holding `.card` after all.
|
||||
|
||||
**D3. Mobile-drawer corner-light intensity.** Use
|
||||
`.glass-panel-strong` (subtle tier). The drawer is a ~256px-wide
|
||||
fixed-position surface that covers ~1/3 of a mobile viewport. At
|
||||
full-intensity (`'chrome'`) the corner radials would dominate the
|
||||
drawer's interior nav text; at subtle they read as "elevated panel"
|
||||
without competing with content. The drawer is also a hidden surface
|
||||
most of the time — full intensity buys nothing for the rare moments
|
||||
it's open.
|
||||
|
||||
**D4. CI gate scope.** Gate **inline-style usage only** —
|
||||
`var(--glass-surface-(low|mid|high))` appearing inside a JSX
|
||||
`style={{ background: ... }}` (or `backgroundColor:`, `background:`
|
||||
in template-string form) under `components/**` or `pages/**`. CSS
|
||||
class definitions in `styles/globals.css` (where the tokens are
|
||||
LEGITIMATELY chained to compose `.glass-panel` / `.glass-panel-strong`
|
||||
/ `.page-header-glass`) are not gated. Allowlist entries: the
|
||||
Layout sidebar nav-chip block (`components/Layout.js` L858-861) and
|
||||
the TopSearchBar header block (the equivalent inline-style block in
|
||||
`components/ui/TopSearchBar.js`) — both intentionally retain
|
||||
`'chrome'`-tier handrolled gradients and were ratified as such in
|
||||
PR #116. The `<GlassSurface>` primitive itself is also allowlisted
|
||||
(it sets the background internally; that's its job). Brief 7 ships
|
||||
this gate as a separate `forbidden-bespoke-glass-surface` job in
|
||||
`ci.yml`, modeled on the existing `forbidden-modal-shell-without-primitive`
|
||||
gate's grep-and-allowlist shape.
|
||||
|
||||
### File plan
|
||||
|
||||
| File | Action | Purpose | Brief |
|
||||
| --- | --- | --- | --- |
|
||||
| `components/ui/GlassSurface.js` | modified | Add `cornerLights` prop (`'subtle' \| 'chrome' \| 'none'`, default `'subtle'`); compose 4-layer gradient + `border: 1px solid transparent` when not `'none'` | 1 |
|
||||
| `test/components/ui-primitives.test.js` | modified | Add 3 corner-light assertions covering the 3 `cornerLights` values | 1 |
|
||||
| `pages/login.js` | modified | Replace L82-88 inline glass imitation with `<div className="glass-panel-strong rounded-2xl p-8">` | 2 |
|
||||
| `pages/signup.js` | modified | Replace L227-232 inline glass imitation with `<div className="glass-panel-strong rounded-2xl p-8">` | 2 |
|
||||
| `components/Layout.js` | modified | Sidebar profile dropdown (L88-96): drop inline background+blur+shadow, add `className="glass-panel-strong rounded-xl"`. Mobile drawer (L698-709): drop inline background+blur+shadow, add `className="glass-panel-strong"` and keep the slide-in transform classes. Sidebar nav-chip block (L858-861) untouched (chrome tier). | 3 |
|
||||
| `components/ui/TopSearchBar.js` | modified | UserMenu dropdown (L214-222): drop inline background+blur+shadow, add `className="glass-panel-strong rounded-xl"`. Header block (chrome tier) untouched. | 3 |
|
||||
| `test/components/Layout.test.js` | modified | Add regression-lock assertion: sidebar profile dropdown and mobile drawer carry `.glass-panel-strong` class. | 3 |
|
||||
| `components/BulkSelectionToolbar.js` | modified | L47 toolbar: replace `bg-white border-gray-200` with `glass-panel-strong rounded-2xl`. L117 dropdown: same swap on `rounded-lg` → `rounded-xl glass-panel-strong`. Sweep interior `text-gray-{600,700}` to `var(--text-primary/secondary)`; `hover:bg-gray-{50,100}` to `nav-item-hover`; `text-red-600 hover:bg-red-50` to `var(--accent-danger)` + ember-tinted hover. | 4 |
|
||||
| `pages/profile.js` | modified | 3 `.card` → `glass-panel rounded-3xl p-6` swaps. | 5 |
|
||||
| `pages/settings.js` | modified | 2 `.card` → `glass-panel rounded-3xl` swaps (one `p-4`, one `p-6`). | 5 |
|
||||
| `pages/community/collections.js` | modified | 1 `.card` → `glass-panel rounded-3xl` swap (preserve `group cursor-pointer hover:shadow-lg transition-all`). | 5 |
|
||||
| `components/CollectionsPageView.js` | modified | 1 `.card` → `glass-panel rounded-3xl` swap (preserve `hover:shadow-xl transition-all cursor-pointer group`). | 5 |
|
||||
| `styles/globals.css` | modified | **Delete** the `.card { ... }` rule (the class block that lives near L757). No other CSS changes. | 5 |
|
||||
| `pages/index.js` | modified | Landing nav bar (L61-72): replace handrolled inline style with `<nav className="page-header-glass border-b">`. Drop the malformed `boxShadow: 'inset 0 1px 0 var(--rim-light-inner)'` (the `.page-header-glass` class already sets the correct inset rim). | 6 |
|
||||
| `.github/workflows/ci.yml` | modified | Add `forbidden-bespoke-glass-surface` job per D4. | 7 |
|
||||
|
||||
### API surface
|
||||
|
||||
N/A. This convoy is pure UI/CSS surface composition. No new routes,
|
||||
no Zod schema additions, no rate-limit changes.
|
||||
|
||||
### Schema diff
|
||||
|
||||
N/A. No DB changes. No `migrations/` files added; no
|
||||
`scripts/add-*.js` follow-up. The schema-map-fresh CI job will not
|
||||
fire on any of these PRs (the conditional in `ci.yml` L47-52 checks
|
||||
`migrations/` / `scripts/add-*` / `scripts/fix-*` /
|
||||
`scripts/setup-neon-db.js` / `docs/SCHEMA_MAP.md` — none touched).
|
||||
|
||||
### Test plan
|
||||
|
||||
| Brief | Unit | Component | Smoke | Visual diff | Notes |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 1 | — | **3 new assertions** in `test/components/ui-primitives.test.js` covering `cornerLights='subtle'` (default, expects `border: 1px solid transparent` in the rendered style attribute), `cornerLights='chrome'` (expects `--corner-light-warm` token reference, not `-subtle`), `cornerLights='none'` (expects no `border` declaration, no gradient layers — same shape as today). | Existing smoke tests cover Modal open/close; nothing new. | **Baseline refresh required** after Brief 1 lands — Modal/StatCard/landing feature cards all gain corner lights. Plan: queue the refresh as a single Linux-baselined commit immediately after Brief 1 merges, before Briefs 3/4 ship. | The most consequential brief; the assertions are the regression lock. |
|
||||
| 2 | — | None needed (presentational class swap). | Existing `tests/smoke/auth.spec.js` covers login/signup happy path; nothing new. | Auth pages diff expected. | — |
|
||||
| 3 | — | Regression-lock in `test/components/Layout.test.js`: render Layout with `isMobileMenuOpen={true}` (mocked) and assert the drawer container's `className` includes `glass-panel-strong`. Render with `<UserProfileDropdown>` open (mocked) and assert its dropdown container's `className` includes `glass-panel-strong`. | Existing smoke covers nav clicks. | Layout dropdown/drawer visual diff expected. | — |
|
||||
| 4 | — | None needed (rare to bulk-select in a smoke test). | — | Expected visual diff on the bulk-select toolbar — but only fires when one of the visual-diff `tests/visual/*` specs actually triggers bulk-select. Probably won't surface in the diff at all; rely on manual QA + dark-mode side-by-side screenshot for review. | The dark-mode invisibility is the regression we're closing; verify manually post-merge. |
|
||||
| 5 | — | None needed (className swap). | — | 4 pages worth of visual diff expected. | — |
|
||||
| 6 | — | None needed. | Existing smoke `tests/smoke/landing.spec.js` covers landing nav rendering. | Expected on landing. | — |
|
||||
| 7 | **Negative test** documented in the brief's acceptance criteria: an implementer dry-run that adds a scratch `style={{ background: 'var(--glass-surface-low)' }}` to a `components/` file should fail the new `forbidden-bespoke-glass-surface` job. Verified once by the architect via the boot-the-brief check below; not committed. | — | — | — | The grep gate runs in CI; no test code needed. |
|
||||
|
||||
Reference test files for the brief authors:
|
||||
- `test/components/ui-primitives.test.js` (Button/Input/SearchBar test
|
||||
patterns, jsdom env, cleanup pattern; Brief 1 extends in-file).
|
||||
- `test/components/Layout.test.js` (5 regression-lock assertions
|
||||
established by `fix-layout-default-user` PR #15; Brief 3 extends).
|
||||
|
||||
### Risk list
|
||||
|
||||
1. **Brief 1 ripple effect** (**MED**). The `<GlassSurface>` primitive
|
||||
feeds `<Modal>`, `<StatCard>`, and 4-5 landing-page feature cards.
|
||||
Adding `border: 1px solid transparent` to every consumer changes
|
||||
the rendered geometry by 2px in both axes — most consumers don't
|
||||
notice (their `padding`/`gap` absorbs it), but any consumer that
|
||||
relied on a 0-border box-model for pixel-exact layout will shift.
|
||||
The `<Modal>` panel sets `maxWidth` in pixels and is centered via
|
||||
flex — it's not sensitive. `<StatCard>` is in a 4-up grid — also
|
||||
not sensitive. The landing feature cards use `grid` with `gap`
|
||||
— not sensitive. But the visual-diff baseline refresh is
|
||||
non-negotiable; queue it before Briefs 3 + 4 ship.
|
||||
2. **Brief 1 + GPU-budget constraint** (**LOW-MED**). AGENTS.md
|
||||
forbids `backdrop-filter` per `CardItem` thumbnail. `<CardItem>`
|
||||
does NOT use `<GlassSurface>` today, so adding gradient-border to
|
||||
`<GlassSurface>` doesn't pull it in. The `'none'` cornerLights
|
||||
value exists as a defensive escape hatch for any future tile
|
||||
surface that DOES want `<GlassSurface>` without the gradient
|
||||
layers. Brief 1's brief explicitly forbids changing CardItem
|
||||
anywhere in this convoy.
|
||||
3. **Brief 3 popovers' z-index + box-shadow chain** (**LOW-MED**).
|
||||
`.glass-panel-strong` ships with
|
||||
`var(--rim-light-inner), var(--elevation-ambient)`. Today's
|
||||
inline-style on the profile dropdown adds
|
||||
`var(--ember-rim-subtle)` as a third shadow for ember emphasis;
|
||||
today's mobile drawer uses `var(--elevation-pronounced)` (heavier
|
||||
elevation than ambient). The brief MUST preserve these via an
|
||||
inline-style override chained onto the class's box-shadow — DON'T
|
||||
just drop them. Verbatim shape documented in Brief 3.
|
||||
4. **Brief 4 dark-mode regressions** (**LOW**). The bulk-select
|
||||
toolbar is currently `bg-white` — invisible in dark mode. The
|
||||
migration FIXES that regression but Brief 4's reviewer should
|
||||
capture before/after dark-mode screenshots so the fix is
|
||||
visible in PR review.
|
||||
5. **Brief 5 `.card` deletion risk** (**LOW-MED, conditional**).
|
||||
Per D2, all 8 consumers migrate to `.glass-panel`. If any one of
|
||||
them breaks visually (the architect doesn't expect this; the
|
||||
8 sites are all "panel with content inside" surfaces), the
|
||||
brief includes an escape hatch to revert that one site to
|
||||
inline `style={{ background: 'var(--bg-secondary)' }}` rather
|
||||
than holding the whole class deletion. If MORE than 1 of 8
|
||||
needs the escape hatch, the brief's reviewer is expected to
|
||||
push back and propose keeping `.card`.
|
||||
6. **Brief 6 `.page-header-glass` already-applied check** (**LOW**).
|
||||
The implementer must verify `pages/index.js`'s nav bar isn't
|
||||
already inside another `.page-header-glass` ancestor (it isn't —
|
||||
landing has no shared Layout); otherwise nested
|
||||
`backdrop-filter`s would compound.
|
||||
7. **Brief 7 false-positive risk** (**LOW**). The grep gate must
|
||||
correctly distinguish JSX inline-style usage of
|
||||
`var(--glass-surface-*)` from CSS class definitions in
|
||||
`styles/globals.css`. The architect-prescribed approach is to
|
||||
restrict the grep to files under `components/**` and `pages/**`
|
||||
(excluding `.css`), which by construction means JSX inline-style
|
||||
only. Brief 7 includes a verbatim grep command shape and the
|
||||
2-entry allowlist.
|
||||
8. **PR #117 + PR #118 not yet merged** (**LOW, dependency**). This
|
||||
convoy's `depends_on:` frontmatter lists both. If either fails
|
||||
review, the briefs may need to refactor against a different
|
||||
baseline. Recommended: hold Brief 1 dispatch until PR #117 + PR
|
||||
#118 are both squash-merged to main.
|
||||
|
||||
### Decomposition
|
||||
|
||||
| Brief # | Title | Files | Depends on | LOC est. |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 1 | Upgrade `<GlassSurface>` primitive with `cornerLights` prop | `components/ui/GlassSurface.js`, `test/components/ui-primitives.test.js` | — | ~80 |
|
||||
| 2 | Migrate auth form cards to `.glass-panel-strong` | `pages/login.js`, `pages/signup.js` | — | ~20 |
|
||||
| 3 | Migrate floating popovers to `.glass-panel-strong` | `components/Layout.js`, `components/ui/TopSearchBar.js`, `test/components/Layout.test.js` | 1 | ~90 |
|
||||
| 4 | Migrate `BulkSelectionToolbar` to `.glass-panel-strong` + token sweep | `components/BulkSelectionToolbar.js` | 1 | ~70 |
|
||||
| 5 | Retire `.card` class; migrate 8 consumers to `.glass-panel` | `pages/profile.js`, `pages/settings.js`, `pages/community/collections.js`, `components/CollectionsPageView.js`, `styles/globals.css` | — | ~30 |
|
||||
| 6 | Migrate landing nav bar to `.page-header-glass` | `pages/index.js` | — | ~10 |
|
||||
| 7 | `forbidden-bespoke-glass-surface` CI gate | `.github/workflows/ci.yml` | 1, 2, 3, 4, 5, 6 | ~40 |
|
||||
|
||||
All briefs target <400 LOC; the largest (Brief 3) is ~90 LOC. Files
|
||||
across briefs are disjoint — verified against the slice_dependencies
|
||||
block above. No two parallel writers on the same file.
|
||||
|
||||
Parallel dispatch recommended for Briefs 1, 2, 5, 6 (no deps, disjoint
|
||||
files). Briefs 3 + 4 wait for Brief 1 to land (per D1 — they may
|
||||
simplify by adopting the upgraded primitive's `cornerLights='subtle'`
|
||||
default rather than handrolling). Brief 7 runs LAST so the grep gate
|
||||
doesn't fail the build on in-flight migrations.
|
||||
|
||||
### Boot-the-brief check
|
||||
|
||||
Read-only verification against the post-PR-#117 + post-PR-#118 tree.
|
||||
|
||||
1. **Dep-set check.** No new npm dependencies added by any brief. The
|
||||
gradient-border recipe is pure CSS; the `cornerLights` prop is
|
||||
pure React composition; the CI gate is a shell `grep` invocation
|
||||
in a YAML job. No `pnpm view` / `package.json` audit needed.
|
||||
2. **Verbatim code-shape check.**
|
||||
- Brief 1's 4-layer recipe matches `.glass-panel-strong`'s current
|
||||
shape verbatim — verified against `styles/globals.css` post-PR
|
||||
#118 (the `background:` block uses
|
||||
`linear-gradient(var(--glass-surface-high), ...) padding-box,
|
||||
radial-gradient(at 0% 100%, var(--corner-light-warm-subtle) ...) border-box,
|
||||
radial-gradient(at 100% 0%, var(--corner-light-cool-subtle) ...) border-box,
|
||||
var(--chip-border-base) border-box`). The primitive's
|
||||
`'chrome'` value uses the non-`-subtle` tokens; both pairs exist
|
||||
in `:root` + `[data-theme="dark"]` (verified).
|
||||
- Brief 3's box-shadow preservation: the sidebar profile dropdown
|
||||
ships `var(--rim-light-inner), var(--ember-rim-subtle),
|
||||
var(--elevation-ambient)`; the mobile drawer ships
|
||||
`var(--rim-light-inner), var(--rim-light-outer),
|
||||
var(--elevation-pronounced)`; the TopSearchBar UserMenu dropdown
|
||||
ships `var(--rim-light-inner), var(--ember-rim-subtle),
|
||||
var(--elevation-pronounced)` (verified during boot-the-brief
|
||||
recheck — uses `-pronounced`, not `-ambient`, because it floats
|
||||
higher in the viewport). All three are preserved as inline
|
||||
`style={{ boxShadow: '...' }}` overrides on the
|
||||
`.glass-panel-strong` element (the class's default
|
||||
`var(--rim-light-inner), var(--elevation-ambient)` would otherwise
|
||||
lose the ember-rim and either heavier elevation). Brief 3
|
||||
documents the verbatim chain.
|
||||
- Brief 5's class deletion: the `.card` block in
|
||||
`styles/globals.css` is a single rule; deleting it is a clean
|
||||
diff (verified by reading L750-L770 area — no other `.card`
|
||||
compound selectors exist).
|
||||
- Brief 7's grep command shape: modeled on the existing
|
||||
`forbidden-modal-shell-without-primitive` job (`ci.yml`
|
||||
L200-L226), which uses
|
||||
`grep -lE 'pattern' pages components -r --include='*.js'`.
|
||||
Same shape works here. Allowlist is implemented via
|
||||
`! -path 'components/ui/GlassSurface.js'` and an explicit
|
||||
line-range exclusion for `components/Layout.js` and
|
||||
`components/ui/TopSearchBar.js` (the chrome blocks).
|
||||
3. **Cross-brief commitments.** None. Each brief lands a complete
|
||||
migration of its scope; no brief introduces a stub or forward
|
||||
declaration that another brief resolves. Brief 7's grep gate
|
||||
passes ONLY AFTER Briefs 1-6 land — that's a dispatch ordering
|
||||
constraint (encoded in `depends_on:`), not a cross-brief
|
||||
commitment in the role-architect sense.
|
||||
|
||||
All checks passed. No briefs need revision.
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
---
|
||||
convoy: unify-glass-panel-surfaces
|
||||
brief_number: 1
|
||||
depends_on: []
|
||||
files:
|
||||
- components/ui/GlassSurface.js
|
||||
- test/components/ui-primitives.test.js
|
||||
---
|
||||
|
||||
# Brief 1: Upgrade `<GlassSurface>` primitive with `cornerLights` prop
|
||||
|
||||
## Goal (1 sentence)
|
||||
|
||||
Add a `cornerLights = 'subtle' | 'chrome' | 'none'` prop to `<GlassSurface>` and compose the 4-layer gradient-border pattern (padding-box fill + two border-box corner radials + `--chip-border-base` base) on top of the existing `tint` / `blur` / `rim` / `elevation` props so every consumer (Modal, StatCard, landing feature cards) picks up corner catch-lights automatically.
|
||||
|
||||
## Files in scope (do not edit anything else)
|
||||
|
||||
- `components/ui/GlassSurface.js` — add the prop, compose the 4-layer `background` and the `border: 1px solid transparent` when the prop is not `'none'`.
|
||||
- `test/components/ui-primitives.test.js` — add 3 corner-light assertions to the existing `describe('GlassSurface', ...)` block (or add the block if it doesn't exist; today the file covers Button / Input / SearchBar only, so a new top-level `describe('GlassSurface', ...)` block at the end of the file is correct).
|
||||
|
||||
**Out of scope:** every consumer of `<GlassSurface>` — `components/ui/Modal.js`, `components/ui/StatCard.js`, `pages/index.js` landing cards. Their behavior changes via the new default; no edits required. Do NOT modify them in this PR.
|
||||
|
||||
## Conventions to follow
|
||||
|
||||
- **Verbatim 4-layer recipe** (mirrors `.glass-panel-strong` post-PR #118; see `styles/globals.css`):
|
||||
```js
|
||||
// When cornerLights === 'subtle' (default):
|
||||
background:
|
||||
`linear-gradient(var(--glass-surface-${tint}), var(--glass-surface-${tint})) padding-box,
|
||||
radial-gradient(at 0% 100%, var(--corner-light-warm-subtle) 0%, transparent 42%) border-box,
|
||||
radial-gradient(at 100% 0%, var(--corner-light-cool-subtle) 0%, transparent 42%) border-box,
|
||||
var(--chip-border-base) border-box`
|
||||
// When cornerLights === 'chrome':
|
||||
// Same recipe; swap --corner-light-warm-subtle → --corner-light-warm
|
||||
// and --corner-light-cool-subtle → --corner-light-cool.
|
||||
// When cornerLights === 'none':
|
||||
// Today's behavior — single layer background: var(--glass-surface-${tint}).
|
||||
// No border declaration.
|
||||
```
|
||||
- **Border declaration**: when `cornerLights !== 'none'`, also set
|
||||
`border: '1px solid transparent'` on the composed style. This is what
|
||||
reveals the border-box gradient layers. When `'none'`, omit the
|
||||
border entirely (preserve today's box-model).
|
||||
- **Shadow stack**: unchanged. The existing `RIM_SHADOWS` /
|
||||
`ELEVATION_SHADOWS` composition logic stays exactly as written. The
|
||||
corner-light layers live in `background`, not `box-shadow`.
|
||||
- **Default value**: `cornerLights = 'subtle'`. This is the new default
|
||||
for every consumer that doesn't pass the prop explicitly.
|
||||
- **Backdrop filter**: unchanged. `backdropFilter` /
|
||||
`WebkitBackdropFilter` continue to compose from `blur` / `tint` exactly
|
||||
as today. Do not touch this line.
|
||||
- **`style` prop merging**: today the composed style spreads
|
||||
`...style` LAST so caller overrides win. Preserve that exactly — a
|
||||
caller passing `style={{ border: '1px solid #f00' }}` overrides the
|
||||
transparent border. This is the explicit escape hatch any consumer
|
||||
who needs a real border can use without setting `cornerLights='none'`.
|
||||
- **AGENTS.md / no-go zones**: `styles/globals.css` is untouched in
|
||||
this brief (Brief 5 owns the `.card` deletion; no other CSS edits in
|
||||
this convoy). The `--corner-light-warm-subtle` /
|
||||
`--corner-light-cool-subtle` / `--corner-light-warm` /
|
||||
`--corner-light-cool` / `--chip-border-base` tokens all exist
|
||||
in `:root` and `[data-theme="dark"]` per PR #118 — verified.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] `<GlassSurface>` accepts a `cornerLights` prop with values `'subtle'` (default), `'chrome'`, `'none'`. Any other value falls back to `'subtle'` (mirror the existing `RIM_SHADOWS[rim] ?? []` defensive fallback shape).
|
||||
- [ ] When `cornerLights='subtle'`, rendered output includes:
|
||||
- `background:` containing both `var(--corner-light-warm-subtle)` and `var(--corner-light-cool-subtle)`.
|
||||
- `border:` value `1px solid transparent`.
|
||||
- [ ] When `cornerLights='chrome'`, rendered output includes:
|
||||
- `background:` containing `var(--corner-light-warm)` and `var(--corner-light-cool)` (NOT the `-subtle` variants).
|
||||
- `border:` value `1px solid transparent`.
|
||||
- [ ] When `cornerLights='none'`, rendered output is identical to today: single-layer `background: var(--glass-surface-${tint})`, no `border` declaration in the composed style.
|
||||
- [ ] `tint` / `blur` / `rim` / `elevation` / `as` / `style` / `className` props all behave exactly as before. Existing consumers continue to compile + render without prop changes.
|
||||
- [ ] `test/components/ui-primitives.test.js` has 3 new assertions (one per `cornerLights` value) verifying the inline style attribute or the rendered element's `style.background` / `style.border` accordingly. Use the existing `render` + `screen.getByRole` / `getByText` / `container` patterns from the file; don't pull in new test deps.
|
||||
- [ ] `npm run lint` passes.
|
||||
- [ ] `npm run test:run` is 22+/22+ (today's count is 21; this brief adds at least 3 — exact count depends on whether you split assertions across multiple `it(...)` blocks).
|
||||
- [ ] No edits to files outside the two listed in `files:` above. **No consumer migrations in this PR.** Brief 3 + 4 will compose against the new default; Briefs 2 + 5 + 6 don't use the primitive.
|
||||
|
||||
## Rationale (≤3 sentences)
|
||||
|
||||
The primitive is the single largest leverage point in the convoy — upgrading it ripples through `<Modal>`, `<StatCard>`, and the landing-page feature cards in one commit. Defaulting to `'subtle'` matches the PR #118 token-tier decision and means downstream consumers don't need to opt in. The `'chrome'` + `'none'` values exist so the prop is future-proof: floating chrome can opt up without handrolling, and any GPU-constrained tile (e.g. a future <CardItem>-like surface) can opt out cleanly.
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
---
|
||||
convoy: unify-glass-panel-surfaces
|
||||
brief_number: 2
|
||||
depends_on: []
|
||||
files:
|
||||
- pages/login.js
|
||||
- pages/signup.js
|
||||
---
|
||||
|
||||
# Brief 2: Migrate auth form cards to `.glass-panel-strong`
|
||||
|
||||
## Goal (1 sentence)
|
||||
|
||||
Replace the handrolled `rgba(var(--bg-secondary-rgb), 0.85)` + `backdrop-blur-sm` glass imitation on the login + signup form cards with the canonical `.glass-panel-strong rounded-2xl p-8` className so the auth flow shares the rest of the app's surface treatment (corner catch-lights, system blur tier, gradient border).
|
||||
|
||||
## Files in scope (do not edit anything else)
|
||||
|
||||
- `pages/login.js` — replace the form-card container `<div>` (currently L82-88, look for the `className="p-8 rounded-2xl shadow-2xl backdrop-blur-sm border border-opacity-20"` + inline `backgroundColor: 'rgba(var(--bg-secondary-rgb), 0.85)'`).
|
||||
- `pages/signup.js` — same migration on the form-card container (currently L227-232 in the symmetric `<div className="p-8 rounded-2xl shadow-2xl backdrop-blur-sm ...">` block).
|
||||
|
||||
**Out of scope:** the surrounding layout/header on either page, the `<Input>` / `<Button>` children, error/success banners, any of the legend/divider/social-button styling below the form. Do not touch them.
|
||||
|
||||
## Conventions to follow
|
||||
|
||||
- **Verbatim replacement** for each card container:
|
||||
```jsx
|
||||
// Before:
|
||||
<div
|
||||
className="p-8 rounded-2xl shadow-2xl backdrop-blur-sm border border-opacity-20"
|
||||
style={{
|
||||
backgroundColor: 'rgba(var(--bg-secondary-rgb), 0.85)',
|
||||
borderColor: 'var(--border)',
|
||||
}}
|
||||
>
|
||||
|
||||
// After:
|
||||
<div className="glass-panel-strong rounded-2xl p-8">
|
||||
```
|
||||
The `style={{}}` block is removed entirely. `.glass-panel-strong`
|
||||
already composes the background, blur, rim, elevation, and the
|
||||
gradient-border treatment.
|
||||
- **No new imports.** This brief does NOT use `<GlassSurface>`
|
||||
directly — the className path is correct because (a) it's an HTML
|
||||
div with no compositional requirements, (b) the existing
|
||||
`.glass-panel-strong` class is the documented canonical shape per
|
||||
the design audit, and (c) using the class keeps the diff minimal.
|
||||
- **Preserve children verbatim.** The `<form>`, every `<Input>`, every
|
||||
`<Button>`, the error banner, the social-sign-in divider, the
|
||||
"Don't have an account?" footer link — all stay byte-identical.
|
||||
- **Do not adjust the surrounding header block** (Deck Hearth logo +
|
||||
greeting text); only the form-card `<div>` itself migrates.
|
||||
- **Tailwind safelist note:** `.glass-panel-strong` is defined in
|
||||
`styles/globals.css` as a plain CSS class (not a Tailwind
|
||||
utility). It's already used in `<Layout>` and elsewhere, so the
|
||||
build picks it up via the `@layer` block. No `tailwind.config.js`
|
||||
edit needed.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] `pages/login.js` form-card `<div>` uses
|
||||
`className="glass-panel-strong rounded-2xl p-8"` and carries
|
||||
no inline `backgroundColor` / `borderColor` style.
|
||||
- [ ] `pages/signup.js` form-card `<div>` uses
|
||||
`className="glass-panel-strong rounded-2xl p-8"` and carries
|
||||
no inline `backgroundColor` / `borderColor` style.
|
||||
- [ ] All form fields, labels, buttons, error banners, and footer
|
||||
links render identically post-migration (manual smoke;
|
||||
`tests/smoke/auth.spec.js` continues to pass without edits).
|
||||
- [ ] No edits to other files (e.g. no token additions in
|
||||
`styles/globals.css`, no new components in `components/ui/`).
|
||||
- [ ] `npm run lint` passes; `npm run test:run` is unchanged
|
||||
(no test additions needed for a pure className swap).
|
||||
- [ ] Dark mode: card remains legible against the body background
|
||||
gradient (verify manually; before-shot vs after-shot screenshot
|
||||
attached to the PR description).
|
||||
|
||||
## Rationale (≤3 sentences)
|
||||
|
||||
These two cards are the first surfaces a new user sees and they predate the `.glass-panel-strong` system; aligning them is both a correctness fix (the `rgba(--bg-secondary-rgb, 0.85)` shape doesn't compose corner lights) and a consistency win. The migration is a pure className swap with zero behavioral change — the lowest-risk brief in the convoy.
|
||||
160
.convoys/unify-glass-panel-surfaces/brief-3-floating-popovers.md
Normal file
160
.convoys/unify-glass-panel-surfaces/brief-3-floating-popovers.md
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
---
|
||||
convoy: unify-glass-panel-surfaces
|
||||
brief_number: 3
|
||||
depends_on: [1]
|
||||
files:
|
||||
- components/Layout.js
|
||||
- components/ui/TopSearchBar.js
|
||||
- test/components/Layout.test.js
|
||||
---
|
||||
|
||||
# Brief 3: Migrate floating popovers to `.glass-panel-strong`
|
||||
|
||||
## Goal (1 sentence)
|
||||
|
||||
Migrate three floating surfaces — the sidebar profile dropdown, the mobile drawer, and the TopSearchBar `<UserMenu>` dropdown — from inline `var(--glass-surface-*)` + `backdropFilter` styles to the canonical `.glass-panel-strong` className, while preserving their existing box-shadow chains (ember rim for the profile dropdown; pronounced elevation for the drawer) via inline override.
|
||||
|
||||
## Files in scope (do not edit anything else)
|
||||
|
||||
- `components/Layout.js`
|
||||
- Sidebar profile dropdown panel (currently around L88-96 inside `<UserProfileDropdown>`).
|
||||
- Mobile drawer slide-in panel (currently around L698-709 — the `<div>` that takes `${isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full'}` and the `style={{ background: 'var(--glass-surface-mid)', backdropFilter: '...', ... }}` block).
|
||||
- **Do NOT touch** the sidebar nav-chip block (around L858-861); that block intentionally uses full-intensity chrome corner lights and stays handrolled — Decision 4 of this convoy's Architecture allowlists it explicitly.
|
||||
- `components/ui/TopSearchBar.js`
|
||||
- `<UserMenu>` dropdown panel (around L214-222 — the absolutely-positioned `<div>` with the `background: 'var(--glass-surface-high)'` + `backdropFilter` block).
|
||||
- **Do NOT touch** the TopSearchBar `<header>` block itself; that block also intentionally uses full-intensity chrome corner lights and is allowlisted.
|
||||
- `test/components/Layout.test.js`
|
||||
- Add regression-lock assertions that the sidebar profile dropdown and mobile drawer render with `.glass-panel-strong` in their className.
|
||||
|
||||
**Out of scope:** the rest of `Layout.js` (`<DesktopSidebar>`, `<MobileNavigation>`, `<UserProfileDropdown>` trigger button, `<DailyEmberWidget>`, etc.), the rest of `TopSearchBar.js` (search input, `<CommandPaletteModal>` link, etc.), and any deeper menu-item styling (e.g. `hover:bg-gray-50 dark:hover:bg-gray-700` cleanup — that's owned by PR #117 / `design-sweep-pass`).
|
||||
|
||||
## Conventions to follow
|
||||
|
||||
- **Verbatim replacement** for each floating-surface `<div>`:
|
||||
|
||||
### A. Sidebar profile dropdown panel (Layout.js)
|
||||
```jsx
|
||||
// Before:
|
||||
<div
|
||||
className="absolute bottom-full left-0 right-0 mb-2 rounded-xl z-20"
|
||||
style={{
|
||||
background: 'var(--glass-surface-high)',
|
||||
backdropFilter: 'blur(var(--glass-blur-mid)) saturate(var(--glass-saturate))',
|
||||
WebkitBackdropFilter: 'blur(var(--glass-blur-mid)) saturate(var(--glass-saturate))',
|
||||
boxShadow: 'var(--rim-light-inner), var(--ember-rim-subtle), var(--elevation-ambient)',
|
||||
}}
|
||||
>
|
||||
|
||||
// After:
|
||||
<div
|
||||
className="glass-panel-strong absolute bottom-full left-0 right-0 mb-2 rounded-xl z-20"
|
||||
style={{
|
||||
boxShadow:
|
||||
'var(--rim-light-inner), var(--ember-rim-subtle), var(--elevation-ambient)',
|
||||
}}
|
||||
>
|
||||
```
|
||||
The `background` + `backdropFilter` + `WebkitBackdropFilter` inline
|
||||
styles are removed (the class provides them). The `boxShadow` is
|
||||
**preserved as an inline override** because `.glass-panel-strong`
|
||||
ships only `var(--rim-light-inner), var(--elevation-ambient)` —
|
||||
we need the `var(--ember-rim-subtle)` middle entry to keep the
|
||||
ember emphasis the dropdown is known for.
|
||||
|
||||
### B. Mobile drawer panel (Layout.js)
|
||||
```jsx
|
||||
// Before:
|
||||
<div
|
||||
className={`
|
||||
md:hidden fixed inset-y-0 left-0 z-50 w-64 transform transition-transform duration-300 ease-in-out
|
||||
${isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full'}
|
||||
`}
|
||||
style={{
|
||||
background: 'var(--glass-surface-mid)',
|
||||
backdropFilter: 'blur(var(--glass-blur-mid)) saturate(var(--glass-saturate))',
|
||||
WebkitBackdropFilter: 'blur(var(--glass-blur-mid)) saturate(var(--glass-saturate))',
|
||||
boxShadow: 'var(--rim-light-inner), var(--rim-light-outer), var(--elevation-pronounced)',
|
||||
}}
|
||||
>
|
||||
|
||||
// After:
|
||||
<div
|
||||
className={`
|
||||
glass-panel-strong md:hidden fixed inset-y-0 left-0 z-50 w-64 transform transition-transform duration-300 ease-in-out
|
||||
${isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full'}
|
||||
`}
|
||||
style={{
|
||||
boxShadow:
|
||||
'var(--rim-light-inner), var(--rim-light-outer), var(--elevation-pronounced)',
|
||||
}}
|
||||
>
|
||||
```
|
||||
Same pattern. `boxShadow` is preserved as an inline override so the
|
||||
drawer keeps its `var(--elevation-pronounced)` (heavier) elevation
|
||||
+ `var(--rim-light-outer)` (which `.glass-panel-strong` doesn't
|
||||
ship by default).
|
||||
|
||||
### C. TopSearchBar `<UserMenu>` dropdown panel
|
||||
```jsx
|
||||
// Before — the absolutely-positioned <div> nested inside UserMenu,
|
||||
// around L211-222 of TopSearchBar.js (verified against current file
|
||||
// 2026-06-04 during architect boot-the-brief recheck):
|
||||
<div
|
||||
role="menu"
|
||||
aria-label="Account menu"
|
||||
className="absolute right-0 top-full mt-2 w-56 rounded-xl z-30 overflow-hidden"
|
||||
style={{
|
||||
background: 'var(--glass-surface-high)',
|
||||
backdropFilter: 'blur(var(--glass-blur-mid)) saturate(var(--glass-saturate))',
|
||||
WebkitBackdropFilter: 'blur(var(--glass-blur-mid)) saturate(var(--glass-saturate))',
|
||||
boxShadow:
|
||||
'var(--rim-light-inner), var(--ember-rim-subtle), var(--elevation-pronounced)',
|
||||
}}
|
||||
>
|
||||
|
||||
// After:
|
||||
<div
|
||||
role="menu"
|
||||
aria-label="Account menu"
|
||||
className="glass-panel-strong absolute right-0 top-full mt-2 w-56 rounded-xl z-30 overflow-hidden"
|
||||
style={{
|
||||
boxShadow:
|
||||
'var(--rim-light-inner), var(--ember-rim-subtle), var(--elevation-pronounced)',
|
||||
}}
|
||||
>
|
||||
```
|
||||
Note: this dropdown uses `--elevation-pronounced` (not `-ambient`)
|
||||
— it's a heavier shadow than the sidebar profile dropdown's because
|
||||
it floats higher in the viewport over more content. Preserve that
|
||||
verbatim; do not normalize to `-ambient`. The `role="menu"` +
|
||||
`aria-label="Account menu"` + `overflow-hidden` attributes also stay
|
||||
byte-identical.
|
||||
|
||||
- **Preserve all children verbatim.** Menu items, dividers, icons,
|
||||
click handlers — all stay byte-identical.
|
||||
- **Do not touch the existing `hover:bg-gray-50 dark:hover:bg-gray-700`
|
||||
classes on individual menu items.** PR #117 (`design-sweep-pass`)
|
||||
is already migrating those to `nav-item-hover`; this brief MUST NOT
|
||||
duplicate that work or it'll merge-conflict. If PR #117 has already
|
||||
merged when this brief dispatches, the relevant lines may already
|
||||
read `nav-item-hover` — that's fine; just leave them alone.
|
||||
- **Test additions in `test/components/Layout.test.js`:**
|
||||
- One assertion for the sidebar profile dropdown: render `<Layout user={mockUser} />`, simulate clicking the user-profile trigger to open the dropdown, assert the dropdown panel `className` contains `'glass-panel-strong'`.
|
||||
- One assertion for the mobile drawer: render `<Layout user={mockUser} />`, simulate opening the drawer (today the test file already has a way to set `isMobileMenuOpen`; if not, render with the relevant prop / state directly), assert the drawer panel `className` contains `'glass-panel-strong'`.
|
||||
- Use the existing test file's patterns (jsdom env, `cleanup()` afterEach, `render` + `screen` from `@testing-library/react`). Do NOT add new test dependencies. Do NOT lower any existing assertion in the file.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] Sidebar profile dropdown panel uses `className="glass-panel-strong ..."` and carries an inline `boxShadow` with `var(--rim-light-inner), var(--ember-rim-subtle), var(--elevation-ambient)`.
|
||||
- [ ] Mobile drawer panel uses `className="glass-panel-strong ..."` and carries an inline `boxShadow` with `var(--rim-light-inner), var(--rim-light-outer), var(--elevation-pronounced)`.
|
||||
- [ ] TopSearchBar `<UserMenu>` dropdown panel uses `className="glass-panel-strong ..."` and carries an inline `boxShadow` with `var(--rim-light-inner), var(--ember-rim-subtle), var(--elevation-pronounced)`.
|
||||
- [ ] No inline `background: 'var(--glass-surface-*)'` or `backdropFilter` style remains on any of the three migrated `<div>`s.
|
||||
- [ ] Sidebar nav-chip block (Layout.js ~L858-861) UNCHANGED. TopSearchBar `<header>` block UNCHANGED. (Architect's allowlist for chrome-tier surfaces.)
|
||||
- [ ] `test/components/Layout.test.js` has at least 2 new assertions (sidebar dropdown + mobile drawer carry `glass-panel-strong`).
|
||||
- [ ] `npm run lint` + `npm run test:run` (23+/23+, post Brief 1) pass.
|
||||
- [ ] Manual: open sidebar profile dropdown, open mobile drawer, open TopSearchBar UserMenu dropdown in light + dark mode — all three render with visible corner catch-lights and the appropriate shadow elevation.
|
||||
- [ ] No edits to files outside the 3 listed in `files:` above.
|
||||
|
||||
## Rationale (≤3 sentences)
|
||||
|
||||
These three popovers are the most visible floating surfaces in the authenticated app and they predate the gradient-border system; migrating them is the largest visual-correctness win of the convoy after Brief 1. Preserving the existing box-shadow chains via inline override (rather than letting them snap to the class default) is non-negotiable — the `var(--ember-rim-subtle)` ember emphasis on the profile menu and the `var(--elevation-pronounced)` weight on the drawer are deliberate design choices that must survive the class swap.
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
---
|
||||
convoy: unify-glass-panel-surfaces
|
||||
brief_number: 4
|
||||
depends_on: [1]
|
||||
files:
|
||||
- components/BulkSelectionToolbar.js
|
||||
---
|
||||
|
||||
# Brief 4: Migrate `BulkSelectionToolbar` to `.glass-panel-strong` + token sweep
|
||||
|
||||
## Goal (1 sentence)
|
||||
|
||||
Replace the bulk-select toolbar's `bg-white border-gray-200` Tailwind shape with `.glass-panel-strong rounded-2xl` so the floating toolbar is legible in dark mode, and sweep its interior hardcoded gray/red Tailwind classes (`text-gray-700`, `hover:bg-gray-100`, `text-red-600 hover:bg-red-50`) to token-driven inline styles + `nav-item-hover` for consistency with the rest of the post-PR-#117 surfaces.
|
||||
|
||||
## Files in scope (do not edit anything else)
|
||||
|
||||
- `components/BulkSelectionToolbar.js` — the entire file. Specifically:
|
||||
- L47 outer toolbar `<div>` — swap `bg-white rounded-xl shadow-2xl border border-gray-200` to `glass-panel-strong rounded-2xl shadow-2xl`. (Keep `shadow-2xl` for now — the toolbar floats over arbitrary content and the extra weight is intentional. `.glass-panel-strong`'s `var(--rim-light-inner), var(--elevation-ambient)` chain doesn't include it; chain it via inline override.)
|
||||
- L60 divider — swap `bg-gray-300` to a CSS-var-driven divider color (`backgroundColor: 'var(--border)'` inline, since `bg-gray-300` doesn't theme).
|
||||
- L53 selection-count label — swap `text-gray-700` to `style={{ color: 'var(--text-primary)' }}`.
|
||||
- L108 more-actions trigger — swap `text-gray-600 hover:text-gray-800 hover:bg-gray-100` to use `var(--text-secondary)` + `nav-item-hover`, with `rounded-lg` upgraded to `rounded-xl`.
|
||||
- L117 more-actions dropdown — swap `bg-white rounded-lg shadow-xl border border-gray-200` to `glass-panel-strong rounded-xl`. (Drop `shadow-xl`; the class provides shadow. Keep `border` semantics out — the class's gradient border is the new look.)
|
||||
- L118-141 menu items — swap `text-gray-700 hover:bg-gray-100` to `var(--text-primary)` + `nav-item-hover` className.
|
||||
- L144 menu divider — swap `border-gray-200` to `borderColor: 'var(--border)'`.
|
||||
- L146-157 destructive menu item — swap `text-red-600 hover:bg-red-50` to `color: 'var(--accent-danger)'` (verify the token exists in `globals.css`; if not, fall back to `rgb(239, 68, 68)` and document) + `nav-item-hover` className.
|
||||
- L160-end "Clear Selection" button — sweep any remaining `text-gray-*` / `bg-gray-*` hardcodes the same way.
|
||||
|
||||
**Out of scope:** the parent components that consume `BulkSelectionToolbar` (e.g. `pages/my-cards.js`, `components/CollectionPageView.js`); the props contract (`selectedCount`, `selectedCards`, all the `onBulk*` callbacks); any of the SVG icon paths.
|
||||
|
||||
## Conventions to follow
|
||||
|
||||
- **Outer toolbar:**
|
||||
```jsx
|
||||
// Before:
|
||||
<div className="bg-white rounded-xl shadow-2xl border border-gray-200 px-3 sm:px-6 py-3 sm:py-4 flex items-center justify-between sm:justify-start sm:space-x-4 sm:min-w-96">
|
||||
|
||||
// After:
|
||||
<div
|
||||
className="glass-panel-strong rounded-2xl px-3 sm:px-6 py-3 sm:py-4 flex items-center justify-between sm:justify-start sm:space-x-4 sm:min-w-96"
|
||||
style={{
|
||||
// Chain shadow-2xl-equivalent depth onto the class's existing
|
||||
// rim+ambient stack so the floating toolbar still reads as
|
||||
// elevated over arbitrary page content.
|
||||
boxShadow:
|
||||
'var(--rim-light-inner), var(--elevation-pronounced)',
|
||||
}}
|
||||
>
|
||||
```
|
||||
Rationale: `shadow-2xl` is a hardcoded RGB; `var(--elevation-pronounced)` is the system equivalent and respects the theme.
|
||||
|
||||
- **More-actions dropdown:**
|
||||
```jsx
|
||||
// Before:
|
||||
<div className="absolute bottom-full right-0 mb-2 bg-white rounded-lg shadow-xl border border-gray-200 py-2 min-w-48">
|
||||
|
||||
// After:
|
||||
<div className="glass-panel-strong absolute bottom-full right-0 mb-2 rounded-xl py-2 min-w-48">
|
||||
```
|
||||
No inline-style override needed — the class's default
|
||||
`var(--rim-light-inner), var(--elevation-ambient)` is the right
|
||||
weight for an inner dropdown.
|
||||
|
||||
- **Menu items** (the `<button>` rows inside the dropdown):
|
||||
```jsx
|
||||
// Before (non-destructive item):
|
||||
<button className="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex items-center space-x-2">
|
||||
|
||||
// After:
|
||||
<button
|
||||
className="nav-item-hover w-full px-4 py-2 text-left text-sm flex items-center space-x-2"
|
||||
style={{ color: 'var(--text-primary)' }}
|
||||
>
|
||||
|
||||
// Before (destructive item):
|
||||
<button className="w-full px-4 py-2 text-left text-sm text-red-600 hover:bg-red-50 flex items-center space-x-2">
|
||||
|
||||
// After:
|
||||
<button
|
||||
className="nav-item-hover w-full px-4 py-2 text-left text-sm flex items-center space-x-2"
|
||||
style={{ color: 'var(--accent-danger)' }}
|
||||
>
|
||||
```
|
||||
|
||||
- **Verify `--accent-danger` exists** before using it. Quick check:
|
||||
search `styles/globals.css` for `--accent-danger`. If absent, the
|
||||
PR #117 sweep likely defines it; if it's still absent post-#117,
|
||||
use `color: 'rgb(239, 68, 68)'` (the literal Tailwind `red-500`
|
||||
RGB) and add a note in the PR description requesting follow-up to
|
||||
introduce the token in a separate PR. Do NOT add the token in this
|
||||
brief — token additions belong in a design-system PR.
|
||||
|
||||
- **Verify `nav-item-hover` exists** — it's defined in
|
||||
`styles/globals.css` and used widely post-PR #117. If for some
|
||||
reason this brief dispatches before PR #117 lands, escalate to the
|
||||
conductor — that's a dependency violation (Brief 4 depends_on: [1]
|
||||
but transitively depends on the post-PR-#117 token tier).
|
||||
|
||||
- **Verbatim children:** SVG icon paths, button text labels, click
|
||||
handler bindings, the `VOCAB.MY_COLLECTION` / `VOCAB.REMOVE_FROM_MY_COLLECTION`
|
||||
imports, the `setShowActions(false)` flow — all stay byte-identical.
|
||||
|
||||
- **`px-2 sm:px-3 py-2` action buttons** (the 3 colored quick-action
|
||||
buttons: Collection / Deck / My Collection): leave them alone. They
|
||||
use `--accent-flame`, `--accent-gold`, `--accent-wood` already and
|
||||
the `onMouseEnter`/`onMouseLeave` swap is a known pattern. Touching
|
||||
them is scope expansion.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] Outer toolbar `<div>` uses `glass-panel-strong rounded-2xl` and an inline `boxShadow` matching the verbatim shape above.
|
||||
- [ ] More-actions dropdown `<div>` uses `glass-panel-strong rounded-xl` and no inline `background` / `border` / `shadow` style.
|
||||
- [ ] All `text-gray-{600,700,800}` classes within the toolbar are replaced with `style={{ color: 'var(--text-{primary,secondary}) ' }}` or `nav-item-hover` className.
|
||||
- [ ] All `hover:bg-gray-{50,100}` classes are replaced with `nav-item-hover` className.
|
||||
- [ ] The destructive "Delete Selected" item uses `var(--accent-danger)` (or the documented `rgb(239,68,68)` fallback) — NOT `text-red-600`.
|
||||
- [ ] The L60 divider uses `style={{ backgroundColor: 'var(--border)' }}` — NOT `bg-gray-300`.
|
||||
- [ ] The 3 quick-action buttons (Collection / Deck / My Collection at L64-101) are UNCHANGED. The "Clear Selection" button at the end may need a small text-color swap; that's in scope. Everything else is left alone.
|
||||
- [ ] **Dark-mode verification**: open the bulk-select toolbar (select 2+ cards on `/my-cards` or `/collection/[id]`), confirm it renders legibly with corner catch-lights and proper text contrast in BOTH light and dark themes. Attach before/after dark-mode screenshots to the PR description.
|
||||
- [ ] `npm run lint` + `npm run test:run` (23+/23+, post Brief 1) pass.
|
||||
- [ ] No edits to files outside `components/BulkSelectionToolbar.js`.
|
||||
|
||||
## Rationale (≤3 sentences)
|
||||
|
||||
The toolbar is currently invisible in dark mode because `bg-white` doesn't theme — this PR is a correctness fix as much as a design unification. Sweeping the interior hardcoded grays + reds in the same PR is cheap (the file is small, ~200 lines) and prevents a follow-up convoy from having to revisit the file. The 3 colored quick-action buttons stay as-is because they use the right tokens already and a token sweep there is scope expansion.
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
---
|
||||
convoy: unify-glass-panel-surfaces
|
||||
brief_number: 5
|
||||
depends_on: []
|
||||
files:
|
||||
- pages/profile.js
|
||||
- pages/settings.js
|
||||
- pages/community/collections.js
|
||||
- components/CollectionsPageView.js
|
||||
- styles/globals.css
|
||||
---
|
||||
|
||||
# Brief 5: Retire `.card` class; migrate 8 consumers to `.glass-panel`
|
||||
|
||||
## Goal (1 sentence)
|
||||
|
||||
Delete the legacy `.card` class from `styles/globals.css` and migrate all 8 consumers (3 in `pages/profile.js`, 2 in `pages/settings.js`, 1 in `pages/community/collections.js`, 1 in `components/CollectionsPageView.js`, plus 1 likely-missed inline `className="card hover:..."` instance) to `glass-panel rounded-3xl p-{4|6}` so the app converges on a single panel vocabulary.
|
||||
|
||||
## Files in scope (do not edit anything else)
|
||||
|
||||
- `styles/globals.css` — delete the entire `.card { ... }` rule block (currently 4 lines at L729-733):
|
||||
```css
|
||||
.card {
|
||||
@apply rounded-3xl shadow-lg p-6 transition-all duration-300;
|
||||
background-color: var(--bg-primary);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
```
|
||||
- `pages/profile.js` — 3 `className="card p-6 [...]"` swaps. Likely sites (verify line numbers against current file): L290 (`card p-6 text-center` — the profile avatar card), L397 (`card p-6 mt-6` — the stats card), L432 (`card p-6` — the activity card).
|
||||
- `pages/settings.js` — 2 `className="card ..."` swaps. Likely sites: L266 (`card p-4`), L290 (`card p-6`).
|
||||
- `pages/community/collections.js` — 1 swap. Likely site: L266 (`card group cursor-pointer hover:shadow-lg transition-all duration-200`).
|
||||
- `components/CollectionsPageView.js` — 1 swap. Likely site: L125 (`card hover:shadow-xl transition-all duration-300 cursor-pointer group`).
|
||||
|
||||
**Out of scope:** everything else in those files (page-level layout, header strips, modal wrappers, button styling, inline content). Only the `.card` className references migrate.
|
||||
|
||||
## Conventions to follow
|
||||
|
||||
- **Swap pattern (default):**
|
||||
```jsx
|
||||
// Before:
|
||||
<div className="card p-6 [...whatever-else]">
|
||||
|
||||
// After:
|
||||
<div className="glass-panel rounded-3xl p-6 [...whatever-else]">
|
||||
```
|
||||
Verbatim: replace the standalone token `card` with `glass-panel rounded-3xl`. **Preserve any sibling tokens** (`p-4` / `p-6` / `text-center` / `mt-6` / `group` / `cursor-pointer` / `hover:shadow-lg` / `transition-all duration-200`, etc.). The legacy `.card` `@apply`'d `rounded-3xl shadow-lg p-6 transition-all duration-300`; we keep `rounded-3xl` explicitly and let `.glass-panel`'s composed shadow stack replace `shadow-lg` (which is the right move — `.glass-panel`'s tokens theme correctly, `shadow-lg` doesn't).
|
||||
|
||||
- **`p-6` handling:** the legacy class baked in `p-6`. If the consumer wrote `card p-4` (`pages/settings.js` L266), the explicit `p-4` already wins via Tailwind's cascade — `p-4` overrides the `@apply rounded-3xl shadow-lg p-6` because they're at the same specificity and `p-4` is the later-defined declaration in compiled output. After migration, the explicit `p-4` continues to win because there's no longer any baked-in `p-6`. **Bottom line:** preserve whatever padding token the consumer wrote; don't normalize to `p-6`.
|
||||
|
||||
- **`transition-all duration-300` handling:** the legacy class baked in `transition-all duration-300`. Most consumers ALSO wrote it explicitly (e.g. `CollectionsPageView.js` L125: `transition-all duration-300`). If a consumer relied on the baked-in version, the swap loses the transition — add `transition-all duration-300` explicitly to that consumer's className post-swap. **Audit each site for whether the transition is referenced; add it back where needed.**
|
||||
|
||||
- **`shadow-lg` handling:** the legacy class baked in `shadow-lg`. `.glass-panel`'s composed `box-shadow` (rim + elevation-ambient) is the right replacement; do NOT carry `shadow-lg` over.
|
||||
|
||||
- **`pages/community/collections.js` site (L266):** the consumer adds `hover:shadow-lg` on top of the baked `shadow-lg`. The base shadow becomes `.glass-panel`'s shadow stack; the `hover:shadow-lg` is a hardcoded Tailwind shadow. Decision: replace `hover:shadow-lg` with no `hover:` override — the corner-light gradient on `.glass-panel` is the new affordance. (If review shows this regresses hover legibility, fall back to inline `style={{}}` boxShadow override on `:hover` — but the simpler clean diff is to drop it.)
|
||||
|
||||
- **`components/CollectionsPageView.js` site (L125):** same logic. Drop `hover:shadow-xl`.
|
||||
|
||||
- **Escape hatch (per Decision D2):** if any one of the 8 consumers breaks visually post-swap (the architect doesn't expect this), revert that one site to inline `style={{ background: 'var(--bg-secondary)' }}` rather than holding the entire class-deletion. Document each escape-hatch use in the PR description with a screenshot. If MORE than 1 of 8 needs the escape hatch, the brief's reviewer is expected to push back and hold the deletion entirely — flag in PR comments.
|
||||
|
||||
- **CSS deletion order:** delete the `.card { ... }` rule LAST, after all 8 className swaps are in place. (Pure ordering hygiene for diff-readability; CSS doesn't actually care.)
|
||||
|
||||
- **Boundaries:** do NOT touch any other class in `globals.css` (no renaming `.glass-panel` → `.opaque-panel`, no adding new classes). Do NOT touch any pages or components not in the 4 listed.
|
||||
|
||||
- **Grep verification before PR:**
|
||||
```bash
|
||||
# Should return 0 matches anywhere in pages/ or components/:
|
||||
rg "className=[\"\\'\\\`]card\\b" pages/ components/ --type js
|
||||
# And the class definition itself:
|
||||
rg "^\\.card \\{" styles/
|
||||
```
|
||||
Both should return zero. If either returns anything, the migration is incomplete.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] `styles/globals.css` no longer contains a `.card { ... }` rule block.
|
||||
- [ ] Zero `className=".*\bcard\b.*"` references remain under `pages/` or `components/` (verified by the grep command above).
|
||||
- [ ] Each of the 8 migrated consumers renders with `.glass-panel rounded-3xl` and the original `p-{4|6}` padding intact.
|
||||
- [ ] Wherever the consumer relied on baked-in `transition-all duration-300` (audit by reviewing each site), the transition is added explicitly post-migration.
|
||||
- [ ] `hover:shadow-{lg,xl}` Tailwind overrides on community/CollectionsPageView are dropped (replaced by `.glass-panel`'s corner-light affordance).
|
||||
- [ ] No escape-hatch sites or — if any — at most 1 escape-hatch site, documented with a screenshot in the PR description.
|
||||
- [ ] `npm run lint` + `npm run test:run` (no test count change expected; 22+/22+ post Brief 1) pass.
|
||||
- [ ] Manual: visit `/profile`, `/settings`, `/community/collections`, `/community/collections` collection-detail in light + dark mode and confirm all panels render with corner catch-lights and theme-correct backgrounds. Attach before/after screenshots to PR.
|
||||
- [ ] No edits to files outside the 5 listed in `files:` above.
|
||||
|
||||
## Rationale (≤3 sentences)
|
||||
|
||||
`.card` predates the gradient-border system and is the only remaining "opaque solid panel" pattern in user-facing pages — a single panel vocabulary across the app is the convoy's success metric. The 8 consumers are all "panel-with-content-inside" surfaces with no GPU-budget constraint, so the migration is a clean className swap with no compositional surprises. Deleting the class outright (rather than keeping it as a documented fallback) prevents future agents from picking the wrong pattern by accident.
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
---
|
||||
convoy: unify-glass-panel-surfaces
|
||||
brief_number: 6
|
||||
depends_on: []
|
||||
files:
|
||||
- pages/index.js
|
||||
---
|
||||
|
||||
# Brief 6: Migrate landing nav bar to `.page-header-glass`
|
||||
|
||||
## Goal (1 sentence)
|
||||
|
||||
Replace the handrolled translucent `<nav>` on the public landing page with the canonical `.page-header-glass border-b` className, and drop the malformed `boxShadow: 'inset 0 1px 0 var(--rim-light-inner)'` (the token already contains its own `inset 0 1px 0`; double-wrapping breaks the cascade — documented in `styles/globals.css` L748-761).
|
||||
|
||||
## Files in scope (do not edit anything else)
|
||||
|
||||
- `pages/index.js` — the landing `<nav>` block, currently L61-72 (around the `border-b` declaration with inline `background: 'var(--glass-surface-mid)'` + `backdropFilter` + `boxShadow: 'inset 0 1px 0 var(--rim-light-inner)'`).
|
||||
|
||||
**Out of scope:** everything else on the landing page (the Deck Hearth heading, the Sign In / Get Started buttons inside the nav, the hero section, feature cards, footer, the `if (user) return null;` redirect logic, etc.). The migration is a single `<nav>` element's `className` + `style` swap.
|
||||
|
||||
## Conventions to follow
|
||||
|
||||
- **Verbatim replacement:**
|
||||
```jsx
|
||||
// Before:
|
||||
<nav
|
||||
className="border-b"
|
||||
style={{
|
||||
background: 'var(--glass-surface-mid)',
|
||||
backdropFilter:
|
||||
'blur(var(--glass-blur-mid)) saturate(var(--glass-saturate))',
|
||||
WebkitBackdropFilter:
|
||||
'blur(var(--glass-blur-mid)) saturate(var(--glass-saturate))',
|
||||
borderColor: 'var(--border)',
|
||||
boxShadow: 'inset 0 1px 0 var(--rim-light-inner)',
|
||||
}}
|
||||
>
|
||||
|
||||
// After:
|
||||
<nav
|
||||
className="page-header-glass border-b"
|
||||
style={{
|
||||
borderColor: 'var(--border)',
|
||||
}}
|
||||
>
|
||||
```
|
||||
The class provides `background`, `backdrop-filter`, and the
|
||||
correct `box-shadow` (which IS `var(--rim-light-inner), 0 1px 0
|
||||
var(--border)` per its definition in `globals.css` L300-306). The
|
||||
`border-b` Tailwind class needs the `borderColor` style to remain
|
||||
themed; everything else collapses into the class.
|
||||
|
||||
- **The malformed boxShadow is the bug:** `var(--rim-light-inner)` is
|
||||
already a complete `inset 0 1px 0 <color>` declaration. Wrapping
|
||||
it in another `inset 0 1px 0 ...` produces invalid CSS that the
|
||||
browser silently drops. The class's own `box-shadow:
|
||||
var(--rim-light-inner), 0 1px 0 var(--border)` does the right
|
||||
thing. Removing the inline `boxShadow` line is the fix.
|
||||
|
||||
- **Children verbatim:** the `<div className="max-w-7xl mx-auto ...">`
|
||||
inside the `<nav>`, the `<AnimatedFireLogo>`, the heading, and the
|
||||
Sign In / Get Started buttons all stay byte-identical. Do not
|
||||
touch them.
|
||||
|
||||
- **`.page-header-glass` placement check:** the class is defined in
|
||||
`styles/globals.css` L300-306 and is already used by authenticated
|
||||
pages' header strips (dashboard, my-cards, cards, etc.). It's safe
|
||||
to reuse on the public landing page — the class has no auth
|
||||
coupling.
|
||||
|
||||
- **Nested backdrop-filter check** (boot-the-brief risk #6): the
|
||||
landing page has no enclosing Layout (the `pages/index.js` redirect
|
||||
logic returns `null` for authenticated users and renders its own
|
||||
full-page `<div>` for anonymous visitors — no `<Layout>` wrapper).
|
||||
So there's no risk of nested `backdrop-filter` stacking. Verified.
|
||||
|
||||
- **AGENTS.md / no-go zones:** `pages/index.js` is an authored page,
|
||||
not a generated artifact. Allowed.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] Landing `<nav>` uses `className="page-header-glass border-b"`.
|
||||
- [ ] Inline `style` on the `<nav>` retains only `borderColor: 'var(--border)'` — no `background`, no `backdropFilter`, no `WebkitBackdropFilter`, no `boxShadow`.
|
||||
- [ ] The Deck Hearth heading + Sign In / Get Started buttons inside the nav render identically.
|
||||
- [ ] Manual: open `/` in an incognito window (anonymous user), verify the nav bar renders with a subtle glassy treatment, an `inset 0 1px 0` highlight (the inner rim), and a 1px bottom border. Verify in both light and dark theme via the theme toggle (which appears on the public landing). Attach light + dark screenshots to PR.
|
||||
- [ ] `npm run lint` + `npm run test:run` pass (no test count change expected).
|
||||
- [ ] No edits to other files.
|
||||
|
||||
## Rationale (≤3 sentences)
|
||||
|
||||
The landing nav is the highest-visibility public surface in the app and currently both (a) duplicates `.page-header-glass`'s composition handrolled, and (b) ships a malformed `boxShadow` that silently breaks. Migrating to the canonical class is a one-line cleanup that fixes a latent bug. No corner-light treatment is appropriate here — the nav is full-bleed, so corner radials would land at viewport edges and never read as light sources; `.page-header-glass` (which uses the rim/border-only composition) is the right shape.
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
---
|
||||
convoy: unify-glass-panel-surfaces
|
||||
brief_number: 7
|
||||
depends_on: [1, 2, 3, 4, 5, 6]
|
||||
files:
|
||||
- .github/workflows/ci.yml
|
||||
---
|
||||
|
||||
# Brief 7: `forbidden-bespoke-glass-surface` CI gate
|
||||
|
||||
## Goal (1 sentence)
|
||||
|
||||
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-style usage under `components/**` or `pages/**`, with a curated allowlist for the documented chrome exceptions (Layout sidebar nav-chip block, TopSearchBar `<header>` block, and the `<GlassSurface>` primitive itself).
|
||||
|
||||
## Files in scope (do not edit anything else)
|
||||
|
||||
- `.github/workflows/ci.yml` — append a new job after the existing `forbidden-modal-shell-without-primitive` job (around L200) or alongside it in the `forbidden-*` cluster. Don't reorder existing jobs.
|
||||
|
||||
**Out of scope:** every other file in the repo. This brief is pure CI surface; no application code changes.
|
||||
|
||||
## Conventions to follow
|
||||
|
||||
- **Model after the existing `forbidden-modal-shell-without-primitive` job** (`.github/workflows/ci.yml` L200-226). Same shape:
|
||||
- Single `runs-on: ubuntu-latest` step that runs a `grep` command.
|
||||
- Collects matches into a bash array.
|
||||
- Iterates and emits `::error file=${f}::<reason>` for each match.
|
||||
- Exits non-zero if any match remains.
|
||||
- The grep pattern is bounded to `pages/ components/ -r --include='*.js'`.
|
||||
|
||||
- **Verbatim job shape** (drop into `ci.yml` at the bottom of the forbidden-* cluster):
|
||||
```yaml
|
||||
forbidden-bespoke-glass-surface:
|
||||
name: No bespoke var(--glass-surface-*) inline styles
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Fail if JSX inline styles handroll glass surfaces
|
||||
run: |
|
||||
# unify-glass-panel-surfaces convoy (PR sequence after #118 +
|
||||
# #117). Once Briefs 1-6 land, every panel-shaped surface in
|
||||
# the app composes via .glass-panel / .glass-panel-strong /
|
||||
# .page-header-glass / <GlassSurface>. Inline-style usage of
|
||||
# var(--glass-surface-low|mid|high) under pages/ or
|
||||
# components/ JSX is the regression vector this gate prevents.
|
||||
#
|
||||
# Allowlist (explicit, documented exceptions):
|
||||
# - components/ui/GlassSurface.js # the primitive itself
|
||||
# - components/Layout.js # sidebar nav-chip chrome block (L858 area)
|
||||
# - components/ui/TopSearchBar.js # <header> chrome block
|
||||
# These three files intentionally compose handrolled chrome
|
||||
# surfaces ratified in PR #116 (gradient borders) + the
|
||||
# unify-glass-panel-surfaces architect decision D4. If you
|
||||
# need to add a fourth allowlist entry, that's a design-system
|
||||
# decision — open a new convoy.
|
||||
ALLOWLIST=(
|
||||
"components/ui/GlassSurface.js"
|
||||
"components/Layout.js"
|
||||
"components/ui/TopSearchBar.js"
|
||||
)
|
||||
FOUND=()
|
||||
while IFS= read -r file; do
|
||||
skip=false
|
||||
for allowed in "${ALLOWLIST[@]}"; do
|
||||
if [ "$file" = "$allowed" ]; then
|
||||
skip=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ "$skip" = true ]; then
|
||||
continue
|
||||
fi
|
||||
FOUND+=("$file")
|
||||
done < <(grep -lE "var\\(--glass-surface-(low|mid|high)\\)" \
|
||||
pages components -r --include='*.js' 2>/dev/null \
|
||||
| sort -u || true)
|
||||
if [ ${#FOUND[@]} -gt 0 ]; then
|
||||
echo "::error::Bespoke var(--glass-surface-*) inline styles detected outside the documented allowlist."
|
||||
echo "Use .glass-panel / .glass-panel-strong / .page-header-glass or compose <GlassSurface> from components/ui/ instead."
|
||||
for f in "${FOUND[@]}"; do
|
||||
echo "::error file=${f}::Replace inline var(--glass-surface-*) with the appropriate class or primitive."
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
echo "OK: no bespoke glass-surface inline styles outside the allowlist."
|
||||
```
|
||||
|
||||
- **Allowlist hygiene:** the 3 allowlist entries are exact paths. If
|
||||
the relevant chrome block in `components/Layout.js` is later
|
||||
refactored into its own sub-component (e.g. `<SidebarNavChip>`),
|
||||
the new file path replaces `components/Layout.js` in the
|
||||
allowlist — that's the kind of edit a future PR would carry.
|
||||
|
||||
- **Pre-merge negative test:** before opening this PR, run the
|
||||
allowlist locally — verify that adding a scratch `style={{
|
||||
background: 'var(--glass-surface-low)' }}` to a non-allowlisted
|
||||
file (e.g. `pages/profile.js`) and re-running the grep produces a
|
||||
match. Then revert the scratch change. Document the negative test
|
||||
in the PR description (don't commit the scratch change).
|
||||
|
||||
- **CI ordering:** Brief 7 depends on Briefs 1-6 ALL landing first.
|
||||
If this job is added before any of the prior briefs ships, the
|
||||
build will fail on the in-flight migrations (every site this
|
||||
convoy is migrating IS currently a bespoke `var(--glass-surface-*)`
|
||||
inline-style usage). Conductor MUST hold dispatch until 1-6 are
|
||||
all on `main`.
|
||||
|
||||
- **Boundaries:** do NOT touch any other CI job. Do NOT edit any
|
||||
application file. Do NOT update AGENTS.md or other docs in this
|
||||
PR (a separate AGENTS.md update can land alongside Brief 1 if
|
||||
the architect wants — Brief 7 is pure CI).
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] `.github/workflows/ci.yml` contains a new `forbidden-bespoke-glass-surface` job matching the verbatim shape above (job name, `runs-on`, checkout step, grep-and-allowlist shell block).
|
||||
- [ ] The allowlist has exactly 3 entries: `components/ui/GlassSurface.js`, `components/Layout.js`, `components/ui/TopSearchBar.js`.
|
||||
- [ ] On a clean post-Briefs-1-through-6 `main`, the new CI job is GREEN — no false positives. (Verify by running the grep locally before opening the PR.)
|
||||
- [ ] Negative test: adding a scratch `style={{ background: 'var(--glass-surface-low)' }}` to e.g. `pages/profile.js` and re-running the same grep command produces a match. (Documented in PR description; not committed.)
|
||||
- [ ] No other CI jobs are reordered, renamed, or modified.
|
||||
- [ ] PR description links back to this brief and to the convoy file (`.convoys/unify-glass-panel-surfaces.md`).
|
||||
|
||||
## Rationale (≤3 sentences)
|
||||
|
||||
The convoy's success metric is unification, but unification without a regression gate is half a fix — a future PR can re-introduce a bespoke `var(--glass-surface-*)` inline style and undo the work. Modeling the gate on the existing `forbidden-modal-shell-without-primitive` job keeps it consistent with the repo's CI vocabulary and makes the failure message actionable. The 3-entry allowlist is small and intentional; growing it requires an explicit design-system decision, which is the right friction for a convention-enforcing gate.
|
||||
Loading…
Reference in a new issue