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"
refactor(layout): migrate 3 floating popovers to .glass-panel-strong Brief 3 of unify-glass-panel-surfaces convoy. Migrates three floating surfaces from inline var(--glass-surface-*) + backdropFilter to the canonical .glass-panel-strong className, preserving their existing box-shadow chains (ember rim for the dropdown panels; pronounced elevation for the drawer + TopSearchBar UserMenu) via inline override. Three popovers migrated: 1. components/Layout.js UserProfileDropdown panel (sidebar) - boxShadow chain preserved: var(--rim-light-inner), var(--ember-rim-subtle), var(--elevation-ambient). 2. components/Layout.js mobile drawer - boxShadow chain preserved: var(--rim-light-inner), var(--rim-light-outer), var(--elevation-pronounced). 3. components/ui/TopSearchBar.js UserMenu dropdown - boxShadow chain preserved: var(--rim-light-inner), var(--ember-rim-subtle), var(--elevation-pronounced) (note: -pronounced, not -ambient — caught by architect boot-the-brief recheck and documented in convoy's risk note). The sidebar nav-chip / main content chrome block (Layout.js ~L853-863) intentionally remains handrolled with full-intensity corner lights — allowlisted by Brief 7's CI gate (D4 of the architect plan). Tests (test/components/Layout.test.js, +2 new assertions): - mobile drawer container queryable via .glass-panel-strong selector and is wired with width/positioning classes (.w-64, .fixed, etc). - mobile drawer inline style contains no var(--glass-surface-*) and no backdrop-filter (both now provided by the class); does contain var(--elevation-pronounced) (preserved override). Verification: - npm run lint passes (1 pre-existing unrelated warning). - npm run test:run: 118/118 tests pass (was 116; +2 new). Acceptance criteria from .convoys/unify-glass-panel-surfaces/brief-3-floating-popovers.md all met. No edits outside the 3 files in scope. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 16:34:19 -04:00
className="glass-panel-strong absolute right-0 top-full mt-2 w-56 rounded-xl z-30 overflow-hidden"
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
style={{
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={{
refactor(design): move light response onto chip borders; cut directional drop shadows (#116) Operator feedback 2026-06-04 (after PR #115's two-light-source pass): "A lot of gradients happening, though I want the gradient in the background to be the focus, and then the border gets a highlight, perhaps, in a corner that's closest to the background gradients... the borders are reflecting the color, not necessarily the drop shadows behind the cards. In fact, you could probably reduce those box shadows quite a bit. At least they shouldn't span the entire edge of the card. Just a subtle bit in the corner." PR #115's outer directional drop-shadows (`--edge-light-warm` / `--edge-light-cool`) DID make the chips respond to the two light sources but spanned the entire bottom-left + top-right edges, which competed visually with the body gradient. This PR moves the warm/cool response onto the chip's 1px BORDER, concentrated in the two corners closest to the matching light sources, and cuts the directional drop-shadows entirely (just rim-light-inner + a single neutral elevation-ambient remain). Technique: the modern gradient-border pattern. background: /* surface fill, clipped to padding-box */ linear-gradient(surface, surface) padding-box, /* warm catch-light at bottom-left, border-box-clipped */ radial-gradient(at 0% 100%, warm 0%, transparent 42%) border-box, /* cool catch-light at top-right, border-box-clipped */ radial-gradient(at 100% 0%, cool 0%, transparent 42%) border-box, /* faint diagonal white tint for the rest of the rim */ chip-border-base border-box; border: 1px solid transparent; The transparent border is what exposes the border-box-clipped gradient layers as the visible rim. Token changes (styles/globals.css): - Retired: --edge-light-warm, --edge-light-cool (both themes). These were complete box-shadow declarations with offsets spanning the full bottom-left + top-right edges of every chip. - Added: --corner-light-warm, --corner-light-cool — plain RGBA colors for radial-gradient consumption. Light: 0.50 warm / 0.38 cool. Dark: 0.68 warm / 0.58 cool (bumped so catch-lights read against the deep substrate). - Added: --chip-border-base — faint 135deg white tint so the rest of the rim isn't invisible (sensible chip edge far from either light source). Lower alpha on dark. Application sites: - styles/globals.css `.glass-panel` and `.glass-panel-strong` — updated to multi-layer background + transparent border. Picks up DailyEmberWidget, CommandPaletteModal popover, UserMenu dropdown, the logged-out sidebar Support/Theme tray, etc. - components/Layout.js nav-chip inline style — same treatment. - components/ui/TopSearchBar.js inline style — same treatment. Shadow stacks across all four sites are now just: var(--rim-light-inner), var(--elevation-ambient) (or elevation-pronounced for .glass-panel-strong). The body gradient stays unchanged from PR #115 — it remains the dominant gradient story per operator's "I want the gradient in the background to be the focus" framing. 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:31:53 -04:00
// 2026-06-04 corner-border pass: same gradient-border
// technique as .glass-panel — the chip's fill clipped to
// padding-box, with border-box-clipped warm bottom-left +
// cool top-right radial highlights exposed through a 1px
// transparent border. The directional outer drop shadows
// from PR #115 are gone — corner catch-lights only now.
background:
'linear-gradient(var(--glass-surface-mid), var(--glass-surface-mid)) padding-box, ' +
'radial-gradient(at 0% 100%, var(--corner-light-warm) 0%, transparent 42%) border-box, ' +
'radial-gradient(at 100% 0%, var(--corner-light-cool) 0%, transparent 42%) border-box, ' +
'var(--chip-border-base) border-box',
border: '1px solid transparent',
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
backdropFilter: 'blur(12px) saturate(180%)',
WebkitBackdropFilter: 'blur(12px) saturate(180%)',
refactor(design): move light response onto chip borders; cut directional drop shadows (#116) Operator feedback 2026-06-04 (after PR #115's two-light-source pass): "A lot of gradients happening, though I want the gradient in the background to be the focus, and then the border gets a highlight, perhaps, in a corner that's closest to the background gradients... the borders are reflecting the color, not necessarily the drop shadows behind the cards. In fact, you could probably reduce those box shadows quite a bit. At least they shouldn't span the entire edge of the card. Just a subtle bit in the corner." PR #115's outer directional drop-shadows (`--edge-light-warm` / `--edge-light-cool`) DID make the chips respond to the two light sources but spanned the entire bottom-left + top-right edges, which competed visually with the body gradient. This PR moves the warm/cool response onto the chip's 1px BORDER, concentrated in the two corners closest to the matching light sources, and cuts the directional drop-shadows entirely (just rim-light-inner + a single neutral elevation-ambient remain). Technique: the modern gradient-border pattern. background: /* surface fill, clipped to padding-box */ linear-gradient(surface, surface) padding-box, /* warm catch-light at bottom-left, border-box-clipped */ radial-gradient(at 0% 100%, warm 0%, transparent 42%) border-box, /* cool catch-light at top-right, border-box-clipped */ radial-gradient(at 100% 0%, cool 0%, transparent 42%) border-box, /* faint diagonal white tint for the rest of the rim */ chip-border-base border-box; border: 1px solid transparent; The transparent border is what exposes the border-box-clipped gradient layers as the visible rim. Token changes (styles/globals.css): - Retired: --edge-light-warm, --edge-light-cool (both themes). These were complete box-shadow declarations with offsets spanning the full bottom-left + top-right edges of every chip. - Added: --corner-light-warm, --corner-light-cool — plain RGBA colors for radial-gradient consumption. Light: 0.50 warm / 0.38 cool. Dark: 0.68 warm / 0.58 cool (bumped so catch-lights read against the deep substrate). - Added: --chip-border-base — faint 135deg white tint so the rest of the rim isn't invisible (sensible chip edge far from either light source). Lower alpha on dark. Application sites: - styles/globals.css `.glass-panel` and `.glass-panel-strong` — updated to multi-layer background + transparent border. Picks up DailyEmberWidget, CommandPaletteModal popover, UserMenu dropdown, the logged-out sidebar Support/Theme tray, etc. - components/Layout.js nav-chip inline style — same treatment. - components/ui/TopSearchBar.js inline style — same treatment. Shadow stacks across all four sites are now just: var(--rim-light-inner), var(--elevation-ambient) (or elevation-pronounced for .glass-panel-strong). The body gradient stays unchanged from PR #115 — it remains the dominant gradient story per operator's "I want the gradient in the background to be the focus" framing. 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:31:53 -04:00
boxShadow: '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>
);
}