* 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>
7.7 KiB
| convoy | brief_number | depends_on | files | ||
|---|---|---|---|---|---|
| unify-glass-panel-surfaces | 4 |
|
|
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>— swapbg-white rounded-xl shadow-2xl border border-gray-200toglass-panel-strong rounded-2xl shadow-2xl. (Keepshadow-2xlfor now — the toolbar floats over arbitrary content and the extra weight is intentional..glass-panel-strong'svar(--rim-light-inner), var(--elevation-ambient)chain doesn't include it; chain it via inline override.) - L60 divider — swap
bg-gray-300to a CSS-var-driven divider color (backgroundColor: 'var(--border)'inline, sincebg-gray-300doesn't theme). - L53 selection-count label — swap
text-gray-700tostyle={{ color: 'var(--text-primary)' }}. - L108 more-actions trigger — swap
text-gray-600 hover:text-gray-800 hover:bg-gray-100to usevar(--text-secondary)+nav-item-hover, withrounded-lgupgraded torounded-xl. - L117 more-actions dropdown — swap
bg-white rounded-lg shadow-xl border border-gray-200toglass-panel-strong rounded-xl. (Dropshadow-xl; the class provides shadow. Keepbordersemantics out — the class's gradient border is the new look.) - L118-141 menu items — swap
text-gray-700 hover:bg-gray-100tovar(--text-primary)+nav-item-hoverclassName. - L144 menu divider — swap
border-gray-200toborderColor: 'var(--border)'. - L146-157 destructive menu item — swap
text-red-600 hover:bg-red-50tocolor: 'var(--accent-danger)'(verify the token exists inglobals.css; if not, fall back torgb(239, 68, 68)and document) +nav-item-hoverclassName. - L160-end "Clear Selection" button — sweep any remaining
text-gray-*/bg-gray-*hardcodes the same way.
- L47 outer toolbar
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:
// 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-2xlis a hardcoded RGB;var(--elevation-pronounced)is the system equivalent and respects the theme. -
More-actions dropdown:
// 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):// 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-dangerexists before using it. Quick check: searchstyles/globals.cssfor--accent-danger. If absent, the PR #117 sweep likely defines it; if it's still absent post-#117, usecolor: 'rgb(239, 68, 68)'(the literal Tailwindred-500RGB) 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-hoverexists — it's defined instyles/globals.cssand 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_COLLECTIONimports, thesetShowActions(false)flow — all stay byte-identical. -
px-2 sm:px-3 py-2action buttons (the 3 colored quick-action buttons: Collection / Deck / My Collection): leave them alone. They use--accent-flame,--accent-gold,--accent-woodalready and theonMouseEnter/onMouseLeaveswap is a known pattern. Touching them is scope expansion.
Acceptance criteria
- Outer toolbar
<div>usesglass-panel-strong rounded-2xland an inlineboxShadowmatching the verbatim shape above. - More-actions dropdown
<div>usesglass-panel-strong rounded-xland no inlinebackground/border/shadowstyle. - All
text-gray-{600,700,800}classes within the toolbar are replaced withstyle={{ color: 'var(--text-{primary,secondary}) ' }}ornav-item-hoverclassName. - All
hover:bg-gray-{50,100}classes are replaced withnav-item-hoverclassName. - The destructive "Delete Selected" item uses
var(--accent-danger)(or the documentedrgb(239,68,68)fallback) — NOTtext-red-600. - The L60 divider uses
style={{ backgroundColor: 'var(--border)' }}— NOTbg-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-cardsor/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.