import React, { useState, useEffect, useRef, createContext, useContext } from 'react'
import { createRootRoute, Outlet, useNavigate } from '@tanstack/react-router'
import { bootstrapTheme } from '../lib/theme'
import { createKeyboardController } from '../lib/keyboard'
import type { RouteName } from '../lib/keyboard'
import { TopBar } from '../shell/TopBar'
import { Palette } from '../shell/Palette'
import { AuthProvider } from '../lib/authContext'
import '../styles/tokens.css'
import '../styles/primitives.css'
import '../styles/shell.css'
import '../styles/palette.css'
interface CursorHandle {
move(d: 1 | -1): void
activate(): void
}
const noopCursor: CursorHandle = {
move: (_d: 1 | -1) => { /* no-op */ },
activate: () => { /* no-op */ },
}
// Context that routes use to register/unregister their cursor handle
export const KeyboardCursorContext = createContext<React.MutableRefObject<CursorHandle> | null>(null)
export function useKeyboardCursor(): React.MutableRefObject<CursorHandle> {
const ctx = useContext(KeyboardCursorContext)
if (ctx === null) throw new Error('useKeyboardCursor must be used within RootComponent')
return ctx
}
export const Route = createRootRoute({
component: RootComponent,
})
function RootComponent(): React.JSX.Element {
const [paletteOpen, setPaletteOpen] = useState(false)
const navigate = useNavigate()
// Ref so keyboard controller callbacks always see current state
const paletteOpenRef = useRef(paletteOpen)
useEffect(() => {
paletteOpenRef.current = paletteOpen
}, [paletteOpen])
// Ref that routes swap in to wire their cursor into the global keyboard handler
const cursorRef = useRef<CursorHandle>(noopCursor)
useEffect(() => {
// Bootstrap theme once on mount
bootstrapTheme()
const controller = createKeyboardController({
go: (route: RouteName) => {
const paths: Record<RouteName, string> = {
home: '/',
projects: '/projects',
stats: '/stats',
health: '/health',
}
void navigate({ to: paths[route] })
},
openPalette: () => setPaletteOpen(true),
closePalette: () => setPaletteOpen(false),
isPaletteOpen: () => paletteOpenRef.current,
cursor: {
move: (d) => cursorRef.current.move(d),
activate: () => cursorRef.current.activate(),
},
})
document.addEventListener('keydown', controller.onKeyDown)
return () => {
document.removeEventListener('keydown', controller.onKeyDown)
controller.teardown()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []) // navigate is stable; intentional empty-dep mount effect
return (
<AuthProvider>
<KeyboardCursorContext.Provider value={cursorRef}>
<div className="app">
<TopBar onPaletteOpen={() => setPaletteOpen(true)} />
<Outlet />
<Palette open={paletteOpen} onClose={() => setPaletteOpen(false)} />
</div>
</KeyboardCursorContext.Provider>
</AuthProvider>
)
}