import { useEffect, useState } from 'react'; import Link from 'next/link'; import { useTheme } from '../../lib/theme-context'; /** * 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 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 ). * * 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). * - User menu (avatar + name + chevron) opens a real dropdown * (see 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. */ function isMacPlatform() { if (typeof window === 'undefined' || !window.navigator) return true; return /Mac|iPhone|iPad|iPod/.test(window.navigator.platform); } /** * 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 ( ); case 'settings': return ( ); case 'admin': return ( ); case 'support': return ( ); case 'logout': return ( ); case 'sun': return ( ); case 'moon': return ( ); default: return null; } }; return (
{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. */}