deckhearth/components/ui/TopSearchBar.js

449 lines
18 KiB
JavaScript
Raw Normal View History

refactor(layout): split sidebar into nav+badge chips; move help/theme to topbar dropdown (#114) 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>
2026-06-04 13:16:09 -04:00
import { useEffect, useState } from 'react';
feat(design-system): redesign v2 #3 — TopSearchBar + Cmd+K + sweep page-header-glass (#105) 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>
2026-06-04 12:14:50 -04:00
import Link from 'next/link';
refactor(layout): split sidebar into nav+badge chips; move help/theme to topbar dropdown (#114) 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>
2026-06-04 13:16:09 -04:00
import { useTheme } from '../../lib/theme-context';
feat(design-system): redesign v2 #3 — TopSearchBar + Cmd+K + sweep page-header-glass (#105) 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>
2026-06-04 12:14:50 -04:00
/**
* TopSearchBar the top horizontal chrome strip from the operator's
* redesign-v2 mockup.
*
* Layout: prominent search input on the left (with magnifier + Cmd+K
* hint pill), then 3 right-side widgets notification bell (with red
* badge when there are unread items), mail/inbox icon, and a compact
* user-menu chip (avatar + name + chevron).
*
* Mounted globally inside <Layout> for authenticated users; renders
* as a no-op `null` for unauthenticated visitors (the public landing
* uses its own marketing header).
*
* Props:
* user { username, email, avatar_url, ... }
* notificationCount number drives the bell badge; 0 hides it
* onOpenCommandPalette () => void fires when user clicks the
* search input or presses K (the global
* keydown listener lives in <Layout>).
*
* Implementation notes:
* - The search input is intentionally read-only-ish: clicking or
* focusing it opens the CommandPaletteModal instead of typing
* inline. This matches Linear / Notion / Vercel's pattern and
* avoids two competing search affordances on the page.
* - The "Cmd+K" hint adapts to the user's platform: K on Mac,
* Ctrl+K elsewhere. Detection is best-effort via navigator.platform
* and gracefully defaults to on the server (matches the Mac
* audience's default expectation).
refactor(layout): split sidebar into nav+badge chips; move help/theme to topbar dropdown (#114) 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>
2026-06-04 13:16:09 -04:00
* - User menu (avatar + name + chevron) opens a real dropdown
* (see <UserMenu> below) with Profile, Settings, Admin (when
* applicable), Help & Support, Theme toggle, and Logout. The
* Help and Theme entries moved here from the desktop sidebar's
* bottom icon row 2026-06-04 per operator feedback.
feat(design-system): redesign v2 #3 — TopSearchBar + Cmd+K + sweep page-header-glass (#105) 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>
2026-06-04 12:14:50 -04:00
*/
function isMacPlatform() {
if (typeof window === 'undefined' || !window.navigator) return true;
return /Mac|iPhone|iPad|iPod/.test(window.navigator.platform);
}
refactor(layout): split sidebar into nav+badge chips; move help/theme to topbar dropdown (#114) 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>
2026-06-04 13:16:09 -04:00
/**
* UserMenu the dropdown that opens from the top-right avatar chip.
*
* Hosts the canonical authenticated-user menu items (Profile, Settings,
* Admin Panel for admins, Help & Support, Theme toggle, Logout). The
* Help and Theme entries moved here 2026-06-04 per operator feedback
* ("move the help and theme toggle into the profile drop-down") the
* sidebar's bottom icon row is gone for authenticated users now, so
* this menu is the canonical entry point for both.
*
* The Theme entry is a button (not a Link) because toggling theme is
* a side-effect with no destination route. Clicking it flips the
* theme AND closes the menu, matching the "act and dismiss" feel of
* the other items. Profile / Settings / Admin / Support / Logout are
* Links and close the menu on click via onClick.
*
* a11y:
* - The trigger button has aria-haspopup="menu" + aria-expanded.
* - The dropdown is role="menu" with menuitem children.
* - Escape closes; click-outside closes.
* - The focus model is intentionally simple (no roving tabindex);
* each menuitem is in the natural tab order so Tab/Shift+Tab
* navigates between them matches the existing sidebar
* UserProfileDropdown's pattern (Layout.js).
*/
function UserMenu({ user }) {
const { theme, toggleTheme } = useTheme();
const [open, setOpen] = useState(false);
useEffect(() => {
if (!open) return undefined;
const onKeyDown = (event) => {
if (event.key === 'Escape') {
setOpen(false);
}
};
document.addEventListener('keydown', onKeyDown);
return () => document.removeEventListener('keydown', onKeyDown);
}, [open]);
const initial = (user.username || user.email || 'U').charAt(0).toUpperCase();
const displayName =
user.username || user.email?.split('@')[0] || 'Account';
const isAdmin = user?.role === 'admin';
const close = () => setOpen(false);
const menuItems = [
{ id: 'profile', label: 'Profile', href: '/profile', icon: 'user' },
{ id: 'settings', label: 'Settings', href: '/settings', icon: 'settings' },
...(isAdmin
? [{ id: 'admin', label: 'Admin Panel', href: '/admin', icon: 'admin' }]
: []),
{ id: 'support', label: 'Help & Support', href: '/support', icon: 'support' },
];
const renderIcon = (name) => {
const iconProps = {
className: 'h-4 w-4 flex-shrink-0',
fill: 'none',
stroke: 'currentColor',
viewBox: '0 0 24 24',
'aria-hidden': true,
};
switch (name) {
case 'user':
return (
<svg {...iconProps}>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
);
case 'settings':
return (
<svg {...iconProps}>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
);
case 'admin':
return (
<svg {...iconProps}>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
);
case 'support':
return (
<svg {...iconProps}>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
);
case 'logout':
return (
<svg {...iconProps}>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
);
case 'sun':
return (
<svg {...iconProps}>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
);
case 'moon':
return (
<svg {...iconProps}>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
);
default:
return null;
}
};
return (
<div className="relative">
<button
type="button"
onClick={() => setOpen((prev) => !prev)}
className="flex items-center gap-2 pl-2 pr-3 py-1.5 rounded-xl transition-colors focus:outline-none focus:ring-2 nav-item-hover"
style={{
color: 'var(--text-primary)',
'--tw-ring-color': 'var(--accent-ember)',
}}
aria-haspopup="menu"
aria-expanded={open}
aria-label="Account menu"
>
<div
className="w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold flex-shrink-0"
style={{
background:
'linear-gradient(135deg, rgb(255, 140, 30) 0%, rgb(216, 67, 21) 100%)',
color: 'rgb(255, 255, 255)',
boxShadow:
'0 1px 4px rgba(255, 110, 0, 0.40), inset 0 1px 0 rgba(255,255,255,0.20)',
}}
aria-hidden="true"
>
{initial}
</div>
<span className="hidden md:inline text-sm font-medium truncate max-w-[10rem]">
{displayName}
</span>
<svg
className={`hidden md:inline h-4 w-4 flex-shrink-0 transition-transform duration-200 ${
open ? 'rotate-180' : ''
}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{open && (
<>
{/* Click-outside scrim. fixed inset-0 catches taps anywhere
outside the menu. z-index sits between the menu (z-30)
and normal page content. */}
<div
className="fixed inset-0 z-20"
onClick={close}
aria-hidden="true"
/>
<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)',
}}
>
<div className="py-1">
{menuItems.map((item) => (
<Link key={item.id} href={item.href} passHref legacyBehavior>
<a
role="menuitem"
onClick={close}
className="flex items-center gap-3 px-3 py-2 text-sm transition-colors nav-item-hover"
style={{ color: 'var(--text-primary)' }}
>
{renderIcon(item.icon)}
<span className="font-medium">{item.label}</span>
</a>
</Link>
))}
{/* Theme toggle button, not Link, because there's no
destination route. Closes the menu after toggling to
match the act-and-dismiss feel of the other items. */}
<button
type="button"
role="menuitem"
onClick={() => {
toggleTheme();
close();
}}
className="w-full flex items-center gap-3 px-3 py-2 text-sm transition-colors nav-item-hover"
style={{ color: 'var(--text-primary)' }}
>
{renderIcon(theme === 'light' ? 'moon' : 'sun')}
<span className="font-medium">
{theme === 'light' ? 'Dark mode' : 'Light mode'}
</span>
</button>
{/* Visual divider before the destructive Logout entry. */}
<div
role="separator"
className="my-1 mx-3 h-px"
style={{ backgroundColor: 'var(--border)' }}
/>
<Link href="/logout" passHref legacyBehavior>
<a
role="menuitem"
onClick={close}
className="flex items-center gap-3 px-3 py-2 text-sm transition-colors nav-item-hover"
style={{ color: 'var(--accent-ember)' }}
>
{renderIcon('logout')}
<span className="font-medium">Logout</span>
</a>
</Link>
</div>
</div>
</>
)}
</div>
);
}
feat(design-system): redesign v2 #3 — TopSearchBar + Cmd+K + sweep page-header-glass (#105) 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>
2026-06-04 12:14:50 -04:00
export default function TopSearchBar({
user,
notificationCount = 0,
onOpenCommandPalette,
}) {
const [hovered, setHovered] = useState(false);
if (!user) return null;
const isMac = isMacPlatform();
const shortcutLabel = isMac ? '⌘K' : 'Ctrl+K';
const openPalette = () => {
onOpenCommandPalette?.();
};
return (
<header
refactor(layout): floating sidebar + floating top header (#113) 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>
2026-06-04 13:07:17 -04:00
// Floating-chrome pass 2026-06-04: `md:rounded-2xl` so the bar
// reads as its own floating chip on desktop (Layout's
// `md:p-4 md:gap-4` puts visible body gradient on all four sides
// of it). Mobile keeps square corners since the header runs
// edge-to-edge there. The shadow stack pairs `rim-light-inner`
// (subtle top inset highlight kept from the prior pass) with
// `elevation-ambient` (drop shadow) so it floats over the body
// gradient. We deliberately did NOT add `rim-light-outer` here:
// the operator's "remove the divider" feedback from the prior
// pass applies to any visible 1px edge, and a hairline outer
// ring would re-introduce one on mobile where the bar abuts
// content directly.
className="px-4 sm:px-6 py-3 flex items-center gap-3 sm:gap-4 md:rounded-2xl"
feat(design-system): redesign v2 #3 — TopSearchBar + Cmd+K + sweep page-header-glass (#105) 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>
2026-06-04 12:14:50 -04:00
style={{
background: 'var(--glass-surface-mid)',
backdropFilter: 'blur(12px) saturate(180%)',
WebkitBackdropFilter: 'blur(12px) saturate(180%)',
feat(design): warm↔cool gradient + edge-light response on chips (#115) Operator feedback 2026-06-04: "increase the gradient in the background that goes from our nice warm red to a nice purpley blue. And then let's have the corners or the edges of our sections respond as if those two are light sources." Two coordinated changes that together create the "lit by two light sources" effect. 1) Body gradient pushed harder + diagonal warm↔cool story (styles/globals.css body { ... } + [data-theme="dark"] body) - Dark theme: * Warm ember pool bottom-left: alpha 0.55 → 0.72, ellipse enlarged from 75%/55% to 95%/70%. * Top-right corner: switched from secondary ember + dim purple-magenta (0.26) to a unified bright purpley-blue pool (alpha 0.58, indigo→violet ramp, 90%/70% ellipse). * Base linear-gradient: 180deg vertical wash → 45deg diagonal (warm aubergine bottom-left → indigo top-right) so the whole page reads as one continuous warm↔cool dialogue, not just two corner pools. - Light theme: * Top-right gold accent replaced with purpley-blue (indigo 0.42 → violet 0.20 → light violet 0.08) so both themes share the same diagonal story. * Bottom-left ember boosted (0.42 → 0.55, ellipse enlarged). * Base linear-gradient: 180deg → 45deg diagonal (warm-cream → cool-violet). 2) Edge-light tokens on every floating chip's shadow stack (styles/globals.css + components/Layout.js + components/ui/TopSearchBar.js) - New design tokens in :root and [data-theme="dark"]: --edge-light-warm (offset -X +Y → glow on the chip's bottom-left edge, matching the warm ember light source) --edge-light-cool (offset +X -Y → glow on the chip's top-right edge, matching the cool purpley-blue light source) Light: 0.16 warm / 0.12 cool. Dark: 0.34 warm / 0.28 cool. - .glass-panel and .glass-panel-strong shadow stacks updated to prepend the two edge-light tokens. Every glass surface in the app now picks up the directional rim glows from the body gradient without per-component changes (DailyEmberWidget, CommandPaletteModal popover, the logged-out sidebar Support/ Theme tray, UserMenu dropdown, etc). - components/Layout.js nav-chip and components/ui/TopSearchBar.js inline boxShadow strings prepended with the same two tokens (they use inline styles rather than the .glass-panel class so they need the explicit shadow stack). Result: the body now visibly transitions from warm ember (bottom- left) through a neutral midpoint to purpley-blue (top-right). Every floating chip's bottom-left and top-right edges pick up subtle directional glow from the matching corner, reading as light wrapping around the chip's rim from the two off-screen-ish light sources. Tests: - npm run test:run: 113/113 - npm run lint: 0 errors (1 pre-existing warning) - npm run build: green Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 13:23:22 -04:00
// 2026-06-04 two-light-source pass: prepended edge-light
// tokens so the header's bottom-left and top-right edges
// pick up warm + cool directional glows. The header sits
// near the top-right of the viewport so the cool source
// reads stronger on it (the purple light is right above);
// the warm rim still appears on the bottom-left edge of the
// chip, just dimmer at this distance from the ember pool.
boxShadow:
'var(--edge-light-warm), var(--edge-light-cool), var(--rim-light-inner), var(--elevation-ambient)',
feat(design-system): redesign v2 #3 — TopSearchBar + Cmd+K + sweep page-header-glass (#105) 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>
2026-06-04 12:14:50 -04:00
}}
>
{/* Search affordance read-only-ish input that opens the
command palette on click. */}
<button
type="button"
onClick={openPalette}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
className="flex-1 max-w-2xl flex items-center gap-2 px-4 py-2 rounded-xl text-left transition-colors focus:outline-none focus:ring-2"
style={{
backgroundColor: hovered
? 'var(--bg-tertiary)'
: 'var(--input-bg)',
border: '1px solid var(--input-border)',
color: 'var(--text-secondary)',
'--tw-ring-color': 'var(--accent-ember)',
}}
aria-label="Open command palette to search"
>
<svg
className="h-5 w-5 flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
<span className="flex-1 text-sm truncate">
Search cards, sets, decks, users
</span>
<span
className="hidden sm:inline-flex items-center gap-0.5 px-2 py-0.5 rounded-md text-xs font-mono"
style={{
backgroundColor: 'var(--bg-tertiary)',
color: 'var(--text-secondary)',
border: '1px solid var(--border)',
}}
aria-hidden="true"
>
{shortcutLabel}
</span>
</button>
<div className="flex items-center gap-2 sm:gap-3 flex-shrink-0">
<button
type="button"
className="relative p-2 rounded-xl transition-colors focus:outline-none focus:ring-2 nav-item-hover"
style={{
color: 'var(--text-secondary)',
'--tw-ring-color': 'var(--accent-ember)',
}}
aria-label={
notificationCount > 0
? `${notificationCount} unread notifications`
: 'Notifications'
}
>
<svg
className="h-5 w-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
/>
</svg>
{notificationCount > 0 && (
<span
className="absolute -top-0.5 -right-0.5 min-w-[1.25rem] h-5 px-1 rounded-full text-xs font-bold flex items-center justify-center"
style={{
backgroundColor: 'rgb(239, 68, 68)',
color: 'rgb(255, 255, 255)',
boxShadow: '0 1px 3px rgba(239, 68, 68, 0.5)',
}}
aria-hidden="true"
>
{notificationCount > 99 ? '99+' : notificationCount}
</span>
)}
</button>
<Link href="/settings#messages" passHref legacyBehavior>
<a
className="p-2 rounded-xl transition-colors focus:outline-none focus:ring-2 nav-item-hover"
style={{
color: 'var(--text-secondary)',
'--tw-ring-color': 'var(--accent-ember)',
}}
aria-label="Messages"
>
<svg
className="h-5 w-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
/>
</svg>
</a>
</Link>
refactor(layout): split sidebar into nav+badge chips; move help/theme to topbar dropdown (#114) 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>
2026-06-04 13:16:09 -04:00
<UserMenu user={user} />
feat(design-system): redesign v2 #3 — TopSearchBar + Cmd+K + sweep page-header-glass (#105) 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>
2026-06-04 12:14:50 -04:00
</div>
</header>
);
}