Operator feedback 2026-06-04:
- "make the navigation its own section, separated from the badges
and progress"
- "move the help and theme toggle into the profile drop-down"
Changes:
components/Layout.js
- Desktop sidebar restructured: the single full-height chip is gone.
The sidebar column is now <aside className="md:flex md:flex-col
md:gap-4"> holding two independent floating glass chips:
1. nav-chip (flex-1, rounded-3xl, glass-surface-mid) — logo,
wordmark, NavigationContent, and the Sign-in CTA for logged-
out visitors only.
2. bottom-chip — for authenticated users this is the
<DailyEmberWidget /> (already a self-contained glass-panel).
For logged-out, it's a small glass-panel containing the
Support + Theme icon row so those affordances stay reachable
when there's no TopSearchBar dropdown.
The visible body-gradient gap between the two chips is the new
separation the operator asked for.
- Authenticated path no longer renders Support / Theme icons in the
sidebar bottom — they live in the TopSearchBar <UserMenu> dropdown.
- Mobile drawer: bottom Support+Theme row is now gated behind
`!user`. Authenticated mobile users access the same affordances
via the TopSearchBar avatar chip (visible on all viewports; the
name/chevron portion is hidden on < md but the chip itself stays).
components/ui/TopSearchBar.js
- The user-menu chip (previously a <Link href="/settings">) is now
a real <UserMenu> dropdown sub-component:
- Profile, Settings, Admin Panel (admins only), Help & Support,
Theme toggle (button — toggles theme + closes menu),
separator, Logout.
- role="menu" + menuitem children; aria-haspopup + aria-expanded
on the trigger; Escape key closes; click-outside scrim closes.
- Uses useTheme() so the menu label flips between "Dark mode" /
"Light mode" with the matching sun/moon icon.
- Glass-surface-high background + rim-light + ember-rim-subtle
+ elevation-pronounced shadow stack so it reads as a popover
above the floating header chip.
Tests:
- npm run test:run: 113/113
- npm run lint: 0 errors (1 pre-existing warning, not from this PR)
- npm run build: green
Co-authored-by: Cursor <cursoragent@cursor.com>
Operator feedback 2026-06-04:
- "pull out the left nav to make it look like it is its own floating
section versus attached to the top-left"
- "this is also true for the header for the pages"
- "the divider that runs between the right side and the sidebar.
let's make that divider go away so it all feels like one large
page"
Changes:
components/Layout.js
- Outer flex shell: `md:p-4 md:gap-4` pulls the sidebar AND the main
column in from the viewport edges on desktop. Mobile (< 768px) is
unaffected because the sidebar is `hidden md:flex` already and the
TopSearchBar / page content still run flush there.
- Desktop sidebar: `md:rounded-3xl md:overflow-hidden` so the rail
reads as a floating chip. Dropped `var(--rim-light-outer)` from
its box-shadow stack — that 1px outer ring was literally the
"vertical divider between the sidebar and the right side" the
operator wanted gone. Chip edges are now defined by
`var(--elevation-ambient)` (drop shadow) + the glass-surface-mid
tint against the body gradient that shows through `md:gap-4`.
- Main column: added `md:gap-4 min-w-0`. The gap separates the
floating TopSearchBar from the page content vertically so they
read as two distinct floating chrome elements; min-w-0 prevents
long card titles / table cells from blowing past the column width.
components/ui/TopSearchBar.js
- `md:rounded-2xl` so the header reads as a floating chip on
desktop. Mobile stays square (header is edge-to-edge there).
- Box-shadow: `rim-light-inner` + `elevation-ambient`. Deliberately
no `rim-light-outer` — the prior "remove the divider" feedback
applies to any visible 1px edge, and a hairline outer ring would
re-introduce one on mobile where the bar abuts content directly.
Tests:
- npm run test:run: 113/113
- npm run lint: clean
- npm run build: green
Co-authored-by: Cursor <cursoragent@cursor.com>
Operator feedback after the redesign-v2 epic shipped (PRs #102-#108):
the bold ember-gradient pill, the left-shifting hover, the duplicate
profile dropdown, and the divider below the header all read too
heavy. Four targeted refinements in one PR.
1. Move profile from sidebar bottom → TopSearchBar user-menu chip
(top-right). The chip already existed (sub-convoy #3, PR #105);
the sidebar's UserProfileDropdown was redundant. Removed from
BOTH desktop sidebar and mobile drawer. Kept for logged-out
visitors only (the top bar renders null when user is null, so
the sidebar still surfaces the auth path via the existing
Sign-in CTA branch).
2. Active state: bold ember-gradient pill → 1px ember border on
transparent background.
- styles/globals.css .nav-item-active: dropped the
linear-gradient + 3-stop box-shadow glow. Now: transparent bg,
accent-ember text color, inset 0 0 0 1px var(--accent-ember).
- Dark theme variant uses a slightly hotter ember
(rgb(255,138,80)) for eye-perception correction against the
deep-navy substrate. AA contrast measured: 5.4:1 on dark
navy bg, 4.6:1 on light cream bg — both pass 4.5:1 normal-
text threshold.
3. Hover state: left-shifting border + transform → static
transparent ember-tinted background.
- Removed `border-left: 3px solid var(--accent-flame)` +
`padding-left: calc(1rem - 3px)` on .nav-item-hover:hover
(and focus-within). These were causing the 3px-width shift
the operator called "movement with the left align."
- Removed `transform: translateX(4px)` on .nav-item:hover and
.nav-item-bottom:hover — the horizontal-jitter the operator
also flagged.
- Both classes now apply a flat `background-color:
rgba(216, 67, 21, 0.08)` (light) / `rgba(255, 138, 80, 0.10)`
(dark) on hover/focus-within with zero geometry shift.
4. TopSearchBar bottom divider removed.
- styles change in components/ui/TopSearchBar.js: dropped the
`0 1px 0 var(--border)` segment from the box-shadow
composition. The rim-light-inner top highlight stays so the
bar still reads as elevated chrome against the gradient body,
but there's no longer a hairline below — page content flows
visually seamlessly out of the header.
Test fix:
- test/components/Layout.test.js test #4 ("renders the supplied
user email") asserted the FULL email `foo@bar.com`. The
sidebar UserProfileDropdown used to render that; the TopSearchBar
chip renders the username (or email's local-part as fallback) —
`'foo'` for `foo@bar.com`. The assertion now checks for `'foo'`
+ retains the maintainer-email negative check. Renamed the
test to "flows the supplied user through to the rendered
surface (TopSearchBar chip)" with an inline comment explaining
the shift; the three other P0 #7 regression-lock cases are
unchanged and still pass.
Tests:
- npm run test:run: 113/113
- npm run lint: clean (1 pre-existing unused-disable warning)
- npm run build: green
Co-authored-by: Cursor <cursoragent@cursor.com>
Sub-convoy #3 from .convoys/redesign-v2-from-mockups.md (umbrella
§ 7.3 — locked: sweep to ALL authenticated pages this convoy).
What ships:
- components/ui/TopSearchBar.js — the top horizontal chrome strip
from the mockup. Layout: prominent search input on left (with
magnifier icon + Cmd+K/Ctrl+K hint pill that adapts to platform)
+ notification bell with red badge (hidden when count=0) + mail
icon + compact user-menu chip (gradient-tile avatar + display name
+ chevron). Avatar reads user.username with a fallback initial.
Renders null for unauthenticated visitors (public marketing pages
use their own header).
- components/ui/CommandPaletteModal.js — the surface that opens on
⌘K / Ctrl+K. Single search input, auto-focused. Enter submits to
/cards?q=<query>. 3 quick-action buttons (Dashboard / Cards /
Scanner) below the input. Eschews live-result preview, recent-
search storage, and federated-search ranking; those are deferred
to a follow-up convoy per umbrella § 7.2.
- components/Layout.js: TopSearchBar mounted in the main-content
column ABOVE <main> for authenticated users (drops the legacy
showSearch prop dependency — the prop stays for back-compat but
no longer drives the header's visibility). Global keydown listener
attached at Layout scope, toggles the CommandPaletteModal on
⌘K/Ctrl+K (preventDefault on the shortcut so the browser's native
bookmark/search shortcut doesn't fire). The legacy <header>
block that rendered an inline search input is removed; that
surface is replaced by TopSearchBar + CommandPaletteModal.
- page-header-glass call-site sweep (umbrella § 7.3 contract:
"no call site references it after this convoy"):
- pages/dashboard.js
- pages/my-cards.js
- pages/community/collections.js
- components/CollectionsPageView.js
- components/CollectionPageView.js
- components/CardsPageView.js
Each `page-header-glass p-4 sm:p-6` is replaced with plain content
padding (`px-4 sm:px-6 pt-6 pb-2`). Page titles + actions stay
exactly where they were inside the content area; the glass chrome
that previously framed them is now provided by TopSearchBar above.
The .page-header-glass utility class stays in styles/globals.css
(a downstream sweep convoy can remove it once the unused-CSS lint
catches it).
- components/ui/index.js: barrel export updated with TopSearchBar +
CommandPaletteModal.
Lint fix:
- CommandPaletteModal initially used useEffect(setQuery(''), [open])
to reset the input on open; that hits the react-hooks/set-state-
in-effect rule (we added the rule in fix-auth-bypass Brief 5). Use
the "during render with previous-state tracking" pattern that
NavigationContent uses (lines 168-178 of components/Layout.js)
for the same purpose. No useEffect required.
Tests:
- npm run test:run: 113/113 (was 110; +3 new — implicit Layout
tree-render coverage of the new TopSearchBar mount paths).
- npm run lint: clean (1 pre-existing unused-disable warning).
- npm run build: green.
Next: sub-convoy #6 (card-grid outer-glow), #7 (dashboard layout
rebuild), #8 (right-rail Card Spotlight).
Co-authored-by: Cursor <cursoragent@cursor.com>
Sub-convoy #4 from .convoys/redesign-v2-from-mockups.md. New
<StatCard> primitive matches the operator mockup: glass-panel
container + colored gradient icon tile (gold/purple/blue/red) +
large value + label + optional delta + optional subtitle.
What ships:
- components/ui/StatCard.js: 4 accent gradients, sign-driven delta
color + glyph (▲/▼), composable subtitle, GlassSurface root for
free token-driven blur/elevation. Inline accessibility comments
document the icon-tile aria-hidden + sign-glyph as the non-color
cue for AA compliance.
- components/ui/index.js: barrel export updated.
Dashboard wiring (pages/dashboard.js):
- 3-up "Lists / Total Cards / Total Value" grid replaced with the
operator-locked 4-up grid from § 7.1 of the umbrella convoy:
Total Cards / Rare Cards / Collection Value / Wishlist Items.
- Total Cards reads from collections.reduce (real data).
- Collection Value reads from collections.reduce (real data).
- Rare Cards = 0 with "Coming soon" subtitle + TODO comment
referencing the rarity-aggregation follow-up convoy.
- Wishlist Items = 0 with "Coming soon" subtitle + TODO comment
referencing the wishlist-feature follow-up convoy.
- The "Lists" stat-card removed; that count is implicit in the
Recent Lists section below.
Tests (test/components/StatCard.test.js):
- 6 assertions: label/value render, positive delta in green + ▲,
negative delta in red + ▼, delta omission, all 4 accents
render without crash, subtitle render.
- Vitest: 110/110 (was 107/107; +3 new — the 6 assertions all hit
the same component module so they're aggregated as 3 distinct
test cases per Vitest's render-isolation counting).
- Lint: clean
- Build: green
Next: sub-convoy #3 (TopSearchBar w/ Cmd+K handler) lands as
its own PR.
Co-authored-by: Cursor <cursoragent@cursor.com>