Initial skeleton: React/TS frontend + Go backend structure

This commit is contained in:
Hermes Agent
2026-06-16 08:51:55 -04:00
parent 78f19cde7d
commit 33c6648b84
40 changed files with 1799 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
export { useCanvas } from './useCanvas'
export { useDebounce, useKeyPress, useResizeObserver } from './useUtils'
export { useKeyboardShortcuts } from './useKeyboardShortcuts'
export { fuzzySearch, fuzzyFilter } from '../utils/fuzzy'
export { initCRDT, getCRDTDoc, destroyCRDT } from '../utils/crdt'

View File

@@ -0,0 +1,49 @@
import { useCanvasStore } from '../state/canvasStore'
export const useCanvas = () => {
const { camX, camY, zoom, flying, dragging, collapsed, spacePanning } = useCanvasStore()
const screenToWorld = (screenX: number, screenY: number) => {
return {
x: (screenX - window.innerWidth / 2) / zoom + camX,
y: (screenY - window.innerHeight / 2) / zoom + camY,
}
}
const worldToScreen = (worldX: number, worldY: number) => {
return {
x: (worldX - camX) * zoom + window.innerWidth / 2,
y: (worldY - camY) * zoom + window.innerHeight / 2,
}
}
const pan = (dx: number, dy: number) => {
useCanvasStore.getState().setCam(camX - dx / zoom, camY - dy / zoom)
}
const zoomAt = (factor: number, centerX: number, centerY: number) => {
const state = useCanvasStore.getState()
const newZoom = Math.max(0.1, Math.min(5, state.zoom * factor))
const worldCenter = screenToWorld(centerX, centerY)
const newCamX = worldCenter.x - (centerX - window.innerWidth / 2) / newZoom
const newCamY = worldCenter.y - (centerY - window.innerHeight / 2) / newZoom
useCanvasStore.getState().setZoom(newZoom)
useCanvasStore.getState().setCam(newCamX, newCamY)
}
return {
camX,
camY,
zoom,
flying,
dragging,
collapsed,
spacePanning,
screenToWorld,
worldToScreen,
pan,
zoomAt,
}
}

View File

@@ -0,0 +1,41 @@
import { useEffect } from 'react'
export const useKeyboardShortcuts = (handlers: {
onOpenSpotlight: () => void
onCloseSpotlight: () => void
onNavigateUp: () => void
onNavigateDown: () => void
onNavigateLeft: () => void
onNavigateRight: () => void
onSelect: () => void
}) => {
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.ctrlKey || e.metaKey) return
switch (e.key) {
case 'Escape':
handlers.onCloseSpotlight()
break
case 'k':
handlers.onNavigateUp()
break
case 'j':
handlers.onNavigateDown()
break
case 'h':
handlers.onNavigateLeft()
break
case 'l':
handlers.onNavigateRight()
break
case 'Enter':
handlers.onSelect()
break
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [handlers])
}

View File

@@ -0,0 +1 @@
export { useKubernetes } from './useKubernetes'

View File

@@ -0,0 +1,53 @@
import { useMemo } from 'react'
export const useSpotlight = () => {
const fuzzy = (query: string, text: string): number => {
if (!query) return 0
const queryLower = query.toLowerCase()
const textLower = text.toLowerCase()
let score = 0
let queryIndex = 0
for (let i = 0; i < textLower.length && queryIndex < queryLower.length; i++) {
if (textLower[i] === queryLower[queryIndex]) {
score++
queryIndex++
}
}
return score / Math.max(query.length, text.length)
}
const filterItems = <T extends { name: string; type: string }>(
items: T[],
query: string,
filterType: string | null
): T[] => {
return items.filter((item) => {
const matchesQuery = fuzzy(query, item.name) > 0.3
const matchesType = !filterType || filterType === 'all' || item.type === filterType
return matchesQuery && matchesType
})
}
const debounce = <T extends (...args: any[]) => any>(fn: T, delay: number) => {
let timeoutId: NodeJS.Timeout | null = null
return (...args: Parameters<T>): Promise<ReturnType<T>> => {
return new Promise((resolve) => {
if (timeoutId) clearTimeout(timeoutId)
timeoutId = setTimeout(() => resolve(fn(...args)), delay)
})
}
}
return useMemo(
() => ({
fuzzy,
filterItems,
debounce,
}),
[]
)
}

View File

@@ -0,0 +1,50 @@
import { useState, useEffect, useCallback } from 'react'
export const useDebounce = <T>(value: T, delay: number): T => {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => {
clearTimeout(timer)
}
}, [value, delay])
return debouncedValue
}
export const useKeyPress = (targetKey: string, callback: () => void) => {
useEffect(() => {
const downHandler = ({ key }: KeyboardEvent) => {
if (key.toLowerCase() === targetKey.toLowerCase()) {
callback()
}
}
window.addEventListener('keydown', downHandler)
return () => window.removeEventListener('keydown', downHandler)
}, [targetKey, callback])
}
export const useResizeObserver = (
ref: React.RefObject<HTMLElement>,
onResize: (rect: DOMRectReadOnly) => void
) => {
useEffect(() => {
const element = ref.current
if (!element) return
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
onResize(entry.contentRect)
}
})
observer.observe(element)
return () => observer.disconnect()
}, [ref, onResize])
}

View File

@@ -0,0 +1 @@
export { useWebSocket } from './useWebSocket'