deckhearth/.convoys/unify-glass-panel-surfaces/brief-3-floating-popovers.md
Randall Stillwell 6812c5c101 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>
2026-06-04 14:06:53 -05:00

9.1 KiB

convoy brief_number depends_on files
unify-glass-panel-surfaces 3
1
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)

    // 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)

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

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