deckhearth/.convoys/unify-glass-panel-surfaces/brief-3-floating-popovers.md
varutasu 66a4d7b721
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>
2026-06-04 14:09:40 -05:00

160 lines
9.1 KiB
Markdown

---
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.