diff --git a/components/Layout.js b/components/Layout.js index 5562e22..3120a3a 100644 --- a/components/Layout.js +++ b/components/Layout.js @@ -760,196 +760,205 @@ export default function Layout({ children, user = null, showSearch = false }) { /> - {/* Mobile Bottom Section. - redesign-v2 refinements 2026-06-04: UserProfileDropdown - removed for authenticated users (the TopSearchBar's - user-menu chip is the canonical entry point now). For - logged-out visitors, the Sign-in CTA still renders here - so the drawer surfaces the auth path. */} -
- {!user && ( + {/* Mobile Bottom Section. 2026-06-04 dropdown-migration pass: + Support + Theme moved into the TopSearchBar + dropdown for authenticated users (the avatar chip in the + top bar is reachable from mobile too — the chevron/name + are hidden via `md:` but the chip itself is always + visible). Logged-out visitors keep the Sign-in CTA + + Support + Theme here since they have no top-bar menu. */} + {!user && ( +
setIsMobileMenuOpen(false)} /> - )} - {/* Icon Buttons Row - Support and Dark Mode */} -
- {/* Support Icon Button */} - - + + + - - - {/* Theme Toggle Icon Button */} - +
-
+ )} - {/* Desktop Sidebar - Hidden on mobile. Liquid Glass mid-tint, ambient - elevation + rim-light edges; the page background visibly cools - through the rail. 2026-06-04 floating-chrome pass: rounded-3xl - + overflow-hidden so the rail reads as its own floating chip; - `var(--rim-light-outer)` dropped from the box-shadow stack - because that 1px outer ring was the "vertical divider between - the sidebar and the right side" the operator wanted gone. The - chip's edge is now defined solely by `var(--elevation-ambient)` - (drop shadow) + the glass-surface-mid tint against the body - gradient that shows through `md:gap-4` on the outer flex. */} - + )} + {/* Main Content. `md:gap-4` adds vertical breathing room between the floating TopSearchBar header and the page content so they diff --git a/components/ui/TopSearchBar.js b/components/ui/TopSearchBar.js index b19190c..d57f72f 100644 --- a/components/ui/TopSearchBar.js +++ b/components/ui/TopSearchBar.js @@ -1,6 +1,6 @@ -import { useState } from 'react'; -import { useRouter } from 'next/router'; +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 @@ -31,30 +31,266 @@ import Link from 'next/link'; * 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) is a Link that routes to /settings; - * a follow-up convoy can replace this with a real dropdown if - * the operator wants the full Notion-style menu. + * - 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. */} + );