diff --git a/apps/hq-web/src/Layout.tsx b/apps/hq-web/src/Layout.tsx index 2a9edf9..1bda7d6 100644 --- a/apps/hq-web/src/Layout.tsx +++ b/apps/hq-web/src/Layout.tsx @@ -1,6 +1,6 @@ import { useEffect, useMemo, useRef, useState } from 'react' import { Navigate, NavLink, Outlet, useLocation, useNavigate } from 'react-router-dom' -import { LogOut, Menu, Search } from 'lucide-react' +import { LogOut, Menu, Search, UserRound } from 'lucide-react' import { avatarColors, Badge, CommandPalette, initials, ThemeSwitcher, type PaletteItem, } from '@sims/ui' @@ -17,6 +17,8 @@ export function Layout() { const [queueCounts, setQueueCounts] = useState({ queued: 0, failed: 0 }) const [healthy, setHealthy] = useState() const [menuOpen, setMenuOpen] = useState(false) + const [userOpen, setUserOpen] = useState(false) + const userRef = useRef(null) const [paletteOpen, setPaletteOpen] = useState(false) const [clientHits, setClientHits] = useState([]) const queryTimer = useRef(undefined) @@ -40,8 +42,20 @@ export function Layout() { .catch(() => { /* badge is best-effort */ }) }, [location.pathname]) - // Close the mobile drawer when the route changes. - useEffect(() => { setMenuOpen(false) }, [location.pathname]) + // Close the mobile drawer and the user menu when the route changes. + useEffect(() => { setMenuOpen(false); setUserOpen(false) }, [location.pathname]) + + // User menu dismissal: outside click or Escape. + useEffect(() => { + if (!userOpen) return + const onDown = (e: MouseEvent) => { + if (userRef.current !== null && !userRef.current.contains(e.target as Node)) setUserOpen(false) + } + const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') setUserOpen(false) } + document.addEventListener('mousedown', onDown) + document.addEventListener('keydown', onKey) + return () => { document.removeEventListener('mousedown', onDown); document.removeEventListener('keydown', onKey) } + }, [userOpen]) // Cancel any pending palette query on unmount. useEffect(() => () => window.clearTimeout(queryTimer.current), []) @@ -164,18 +178,28 @@ export function Layout() { )} - {/* D18 WS-E: the user chip opens the self-service profile. */} - - {initials(name)} - {name} - {role()} - - + {/* D18 WS-E: the user chip opens a small menu — Profile / Logout. */} + + + {userOpen && ( +
+ + +
+ )} +
diff --git a/apps/hq-web/src/app.css b/apps/hq-web/src/app.css index adcc68d..4ab0136 100644 --- a/apps/hq-web/src/app.css +++ b/apps/hq-web/src/app.css @@ -66,7 +66,27 @@ } .hq-pill.ok { color: var(--ok); background: var(--ok-soft); border-color: color-mix(in srgb, var(--ok) 25%, transparent); } .hq-pill.warn { color: var(--warn); background: var(--warn-soft); border-color: color-mix(in srgb, var(--warn) 25%, transparent); } -.hq-user { display: inline-flex; align-items: center; gap: 8px; } +.hq-user-wrap { position: relative; display: inline-flex; } +.hq-user { + display: inline-flex; align-items: center; gap: 8px; + background: none; border: 1px solid transparent; border-radius: 999px; + padding: 3px 8px 3px 4px; cursor: pointer; font: inherit; color: inherit; +} +.hq-user:hover, .hq-user[aria-expanded='true'] { background: var(--bg-hover); border-color: var(--border); } +.hq-user-menu { + position: absolute; top: calc(100% + 6px); right: 0; z-index: 60; + min-width: 150px; padding: 4px; + background: var(--bg-raised); border: 1px solid var(--border); + border-radius: var(--radius); box-shadow: var(--shadow-lg); + display: flex; flex-direction: column; +} +.hq-user-item { + display: flex; align-items: center; gap: 8px; + background: none; border: none; border-radius: var(--radius-sm); + padding: 7px 10px; font: 500 13px var(--font); color: var(--text); + cursor: pointer; text-align: left; +} +.hq-user-item:hover { background: var(--bg-hover); } .hq-user-av { width: 26px; height: 26px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;