@ -1,6 +1,6 @@
import { useEffect , useMemo , useRef , useState } from 'react'
import { useEffect , useMemo , useRef , useState } from 'react'
import { Navigate , NavLink , Outlet , useLocation , useNavigate } from 'react-router-dom'
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 {
import {
avatarColors , Badge , CommandPalette , initials , ThemeSwitcher , type PaletteItem ,
avatarColors , Badge , CommandPalette , initials , ThemeSwitcher , type PaletteItem ,
} from '@sims/ui'
} from '@sims/ui'
@ -17,6 +17,8 @@ export function Layout() {
const [ queueCounts , setQueueCounts ] = useState ( { queued : 0 , failed : 0 } )
const [ queueCounts , setQueueCounts ] = useState ( { queued : 0 , failed : 0 } )
const [ healthy , setHealthy ] = useState < boolean | undefined > ( )
const [ healthy , setHealthy ] = useState < boolean | undefined > ( )
const [ menuOpen , setMenuOpen ] = useState ( false )
const [ menuOpen , setMenuOpen ] = useState ( false )
const [ userOpen , setUserOpen ] = useState ( false )
const userRef = useRef < HTMLSpanElement > ( null )
const [ paletteOpen , setPaletteOpen ] = useState ( false )
const [ paletteOpen , setPaletteOpen ] = useState ( false )
const [ clientHits , setClientHits ] = useState < PaletteItem [ ] > ( [ ] )
const [ clientHits , setClientHits ] = useState < PaletteItem [ ] > ( [ ] )
const queryTimer = useRef < number | undefined > ( undefined )
const queryTimer = useRef < number | undefined > ( undefined )
@ -40,8 +42,20 @@ export function Layout() {
. catch ( ( ) = > { /* badge is best-effort */ } )
. catch ( ( ) = > { /* badge is best-effort */ } )
} , [ location . pathname ] )
} , [ location . pathname ] )
// Close the mobile drawer when the route changes.
// Close the mobile drawer and the user menu when the route changes.
useEffect ( ( ) = > { setMenuOpen ( false ) } , [ location . pathname ] )
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.
// Cancel any pending palette query on unmount.
useEffect ( ( ) = > ( ) = > window . clearTimeout ( queryTimer . current ) , [ ] )
useEffect ( ( ) = > ( ) = > window . clearTimeout ( queryTimer . current ) , [ ] )
@ -164,18 +178,28 @@ export function Layout() {
< / span >
< / span >
) }
) }
< ThemeSwitcher / >
< ThemeSwitcher / >
{ /* D18 WS-E: the user chip opens the self-service profile. */ }
{ /* D18 WS-E: the user chip opens a small menu — Profile / Logout. */ }
< NavLink to = "/profile" className = "hq-user" title = "My profile" style = { { textDecoration : 'none' , color : 'inherit' } } >
< span className = "hq-user-wrap" ref = { userRef } >
< span className = "hq-user-av" style = { { background : av.bg , color : av.fg } } > { initials ( name ) } < / span >
< button
< span className = "hq-user-name" > { name } < / span >
type = "button" className = "hq-user" title = "Account"
< Badge > { role ( ) } < / Badge >
aria - haspopup = "menu" aria - expanded = { userOpen }
< / NavLink >
onClick = { ( ) = > setUserOpen ( ( v ) = > ! v ) }
< button
>
type = "button" className = "wf" title = "Logout" aria - label = "Logout"
< span className = "hq-user-av" style = { { background : av.bg , color : av.fg } } > { initials ( name ) } < / span >
onClick = { ( ) = > { clearSession ( ) ; nav ( '/login' ) } }
< span className = "hq-user-name" > { name } < / span >
>
< Badge > { role ( ) } < / Badge >
< LogOut size = { 14 } / >
< / button >
< / button >
{ userOpen && (
< div className = "hq-user-menu" role = "menu" >
< button type = "button" role = "menuitem" className = "hq-user-item" onClick = { ( ) = > { setUserOpen ( false ) ; nav ( '/profile' ) } } >
< UserRound size = { 14 } aria - hidden / > Profile
< / button >
< button type = "button" role = "menuitem" className = "hq-user-item" onClick = { ( ) = > { setUserOpen ( false ) ; clearSession ( ) ; nav ( '/login' ) } } >
< LogOut size = { 14 } aria - hidden / > Logout
< / button >
< / div >
) }
< / span >
< / header >
< / header >
< div className = "hq-content" >
< div className = "hq-content" >
< Outlet / >
< Outlet / >