~bigbes/lethe

ref: 321125b29b8dcde4046cf019b588f2bfe4e96d2a lethe/web/src/routes/__root.tsx -rw-r--r-- 2.9 KiB
321125b2 — Eugene Blikh stats: add /api/v1/stats aggregate endpoint a month ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
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 '../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 (
    <KeyboardCursorContext.Provider value={cursorRef}>
      <div className="app">
        <TopBar onPaletteOpen={() => setPaletteOpen(true)} />
        <Outlet />
        <Palette open={paletteOpen} onClose={() => setPaletteOpen(false)} />
      </div>
    </KeyboardCursorContext.Provider>
  )
}