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>