@ -20,6 +20,7 @@ export function Layout() {
const [ paletteOpen , setPaletteOpen ] = useState ( false )
const [ clientHits , setClientHits ] = useState < PaletteItem [ ] > ( [ ] )
const queryTimer = useRef < number | undefined > ( undefined )
const querySeq = useRef ( 0 )
useEffect ( ( ) = > {
if ( ! hasSession ( ) ) return
@ -42,10 +43,14 @@ export function Layout() {
// Close the mobile drawer when the route changes.
useEffect ( ( ) = > { setMenuOpen ( false ) } , [ location . pathname ] )
// Cancel any pending palette query on unmount.
useEffect ( ( ) = > ( ) = > window . clearTimeout ( queryTimer . current ) , [ ] )
// Ctrl+K / Cmd+K opens the palette.
useEffect ( ( ) = > {
const onKey = ( e : KeyboardEvent ) = > {
if ( ( e . ctrlKey || e . metaKey ) && e . key . toLowerCase ( ) === 'k' ) {
if ( e . repeat ) return
e . preventDefault ( )
setPaletteOpen ( ( v ) = > ! v )
}
@ -68,17 +73,25 @@ export function Layout() {
const onPaletteQuery = ( q : string ) = > {
window . clearTimeout ( queryTimer . current )
if ( q . trim ( ) . length < 2 ) { setClientHits ( [ ] ) ; return }
if ( q . trim ( ) . length < 2 ) { querySeq . current ++ ; setClientHits ( [ ] ) ; return }
const mySeq = ++ querySeq . current
queryTimer . current = window . setTimeout ( ( ) = > {
getClients ( q )
. then ( ( cs ) = > setClientHits ( cs . slice ( 0 , 8 ) . map ( ( c ) = > ( {
id : ` client: ${ c . id } ` , label : ` ${ c . code } · ${ c . name } ` , group : 'Clients' , hint : c.status ,
} ) ) ) )
. catch ( ( ) = > setClientHits ( [ ] ) )
. then ( ( cs ) = > {
if ( querySeq . current !== mySeq ) return
setClientHits ( cs . slice ( 0 , 8 ) . map ( ( c ) = > ( {
id : ` client: ${ c . id } ` , label : ` ${ c . code } · ${ c . name } ` , group : 'Clients' , hint : c.status ,
} ) ) )
} )
. catch ( ( ) = > {
if ( querySeq . current !== mySeq ) return
setClientHits ( [ { id : 'err:clients' , label : 'Client search failed — is the server up?' , group : 'Clients' } ] )
} )
} , 200 )
}
const onPaletteSelect = ( item : PaletteItem ) = > {
if ( item . id . startsWith ( 'err:' ) ) return
if ( item . id . startsWith ( 'nav:' ) ) nav ( item . id . slice ( 4 ) )
else if ( item . id . startsWith ( 'act:' ) ) nav ( item . id . slice ( 4 ) )
else if ( item . id . startsWith ( 'client:' ) ) nav ( ` /clients/ ${ item . id . slice ( 7 ) } ` )
@ -93,6 +106,7 @@ export function Layout() {
const name = displayName ( )
const av = avatarColors ( name )
const badgeCount = queueCounts . queued + queueCounts . failed
const badgeTitle = ` ${ queueCounts . queued } queued ${ queueCounts . failed > 0 ? ` , ${ queueCounts . failed } failed ` : '' } `
return (
< div className = "hq-shell" >
@ -110,7 +124,11 @@ export function Layout() {
< n.icon size = { 15 } strokeWidth = { 1.8 } aria - hidden / >
< span className = "lbl" > { n . label } < / span >
{ n . to === '/' && badgeCount > 0 && (
< span className = { ` hq-count ${ queueCounts . failed > 0 ? ' err' : '' } ` } > { badgeCount } < / span >
< span
className = { ` hq-count ${ queueCounts . failed > 0 ? ' err' : '' } ` }
title = { badgeTitle }
aria - label = { badgeTitle }
> { badgeCount } < / span >
) }
< / NavLink >
) ) }
@ -130,7 +148,7 @@ export function Layout() {
< / div >
) }
< header className = "hq-top" >
< button type = "button" className = "wf hq-burger" aria - label = "Menu" onClick= { ( ) = > setMenuOpen ( ( v ) = > ! v ) } >
< button type = "button" className = "wf hq-burger" aria - label = "Menu" aria- expanded = { menuOpen } onClick= { ( ) = > setMenuOpen ( ( v ) = > ! v ) } >
< Menu size = { 16 } / >
< / button >
< button type = "button" className = "hq-search" onClick = { ( ) = > setPaletteOpen ( true ) } >