From b8a4a87cec65e6c762d4b795cae6fc6251e2e8d9 Mon Sep 17 00:00:00 2001 From: Randall Stillwell Date: Thu, 4 Jun 2026 12:13:52 -0500 Subject: [PATCH] refactor(layout): split sidebar into nav+badge chips; move help/theme to topbar dropdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 {/* 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. */} + );