feat: add node help system and registry for extensible node types

- Implemented a help system for different node types (config, render, variable, function) with detailed usage instructions.
- Created a node registry to manage node types, including registration, retrieval, and validation of connections between nodes.
- Defined central node and edge types for the application to streamline state management.
- Added Nunjucks autocomplete functionality to enhance user experience in template editing.
- Developed a syntax highlighting parser for PlantUML and Nunjucks within the CodeMirror editor.
- Registered built-in node types at application startup, including their default configurations and help entries.
- Introduced a theme context provider to manage light/dark mode preferences across the application.
- Created utility functions for class name management using clsx and tailwind-merge.
- Set up Tailwind CSS for styling with custom themes and responsive design.
- Configured Vite for development with proxy settings for backend API calls and Kroki diagram service.
This commit is contained in:
2026-03-09 20:04:31 +01:00
parent 11fd9cd54d
commit b3c2c6711f
67 changed files with 1286 additions and 2124 deletions

View File

@@ -0,0 +1,117 @@
import { useCallback, useRef, useState } from 'react'
import type { AppNode, AppEdge } from '@/lib/nodeTypes'
export type GraphState = { nodes: AppNode[]; edges: AppEdge[] }
function cloneState(state: GraphState): GraphState {
return {
nodes: state.nodes.map((n) => ({ ...n, data: n.data && typeof n.data === 'object' ? { ...n.data } : n.data })),
edges: state.edges.map((e) => ({ ...e })),
}
}
const MAX_HISTORY = 100
export function useGraphStateWithHistory(initialNodes: AppNode[], initialEdges: AppEdge[]) {
const [nodes, setNodesState] = useState<AppNode[]>(initialNodes)
const [edges, setEdgesState] = useState<AppEdge[]>(initialEdges)
const [historySizes, setHistorySizes] = useState({ past: 0, future: 0 })
const pastRef = useRef<GraphState[]>([])
const futureRef = useRef<GraphState[]>([])
const preDragRef = useRef<GraphState | null>(null)
const nodesRef = useRef(nodes)
const edgesRef = useRef(edges)
nodesRef.current = nodes
edgesRef.current = edges
const pushToPast = useCallback((state: GraphState) => {
pastRef.current = pastRef.current.slice(-(MAX_HISTORY - 1))
pastRef.current.push(cloneState(state))
futureRef.current = []
setHistorySizes({ past: pastRef.current.length, future: 0 })
}, [])
const setNodes = useCallback((updater: AppNode[] | ((prev: AppNode[]) => AppNode[])) => {
pushToPast({ nodes: nodesRef.current, edges: edgesRef.current })
setNodesState(typeof updater === 'function' ? updater : () => updater)
}, [pushToPast])
const setEdges = useCallback((updater: AppEdge[] | ((prev: AppEdge[]) => AppEdge[])) => {
pushToPast({ nodes: nodesRef.current, edges: edgesRef.current })
setEdgesState(typeof updater === 'function' ? updater : () => updater)
}, [pushToPast])
const setNodesSilent = useCallback((updater: AppNode[] | ((prev: AppNode[]) => AppNode[])) => {
setNodesState(typeof updater === 'function' ? updater : () => updater)
}, [])
const setEdgesSilent = useCallback((updater: AppEdge[] | ((prev: AppEdge[]) => AppEdge[])) => {
setEdgesState(typeof updater === 'function' ? updater : () => updater)
}, [])
const applyGraph = useCallback((updater: (state: GraphState) => GraphState) => {
pushToPast({ nodes: nodesRef.current, edges: edgesRef.current })
const next = updater({ nodes: nodesRef.current, edges: edgesRef.current })
setNodesState(next.nodes)
setEdgesState(next.edges)
}, [pushToPast])
const saveForDragEnd = useCallback(() => {
preDragRef.current = cloneState({ nodes: nodesRef.current, edges: edgesRef.current })
}, [])
const commitDragEnd = useCallback(() => {
if (preDragRef.current) {
pastRef.current = pastRef.current.slice(-(MAX_HISTORY - 1))
pastRef.current.push(preDragRef.current)
futureRef.current = []
preDragRef.current = null
setHistorySizes({ past: pastRef.current.length, future: 0 })
}
}, [])
const undo = useCallback(() => {
if (pastRef.current.length === 0) return
const prev = pastRef.current.pop()!
futureRef.current.push(cloneState({ nodes: nodesRef.current, edges: edgesRef.current }))
setNodesState(prev.nodes)
setEdgesState(prev.edges)
setHistorySizes({ past: pastRef.current.length, future: futureRef.current.length })
}, [])
const redo = useCallback(() => {
if (futureRef.current.length === 0) return
const next = futureRef.current.pop()!
pastRef.current.push(cloneState({ nodes: nodesRef.current, edges: edgesRef.current }))
setNodesState(next.nodes)
setEdgesState(next.edges)
setHistorySizes({ past: pastRef.current.length, future: futureRef.current.length })
}, [])
const setStateImmediate = useCallback((state: GraphState) => {
setNodesState(state.nodes)
setEdgesState(state.edges)
pastRef.current = []
futureRef.current = []
preDragRef.current = null
setHistorySizes({ past: 0, future: 0 })
}, [])
return {
nodes,
edges,
setNodes,
setEdges,
setNodesSilent,
setEdgesSilent,
applyGraph,
saveForDragEnd,
commitDragEnd,
undo,
redo,
canUndo: historySizes.past > 0,
canRedo: historySizes.future > 0,
setStateImmediate,
}
}

View File

@@ -0,0 +1,27 @@
import { useEffect, useRef, useState } from 'react'
/**
* Returns the height of the observed element, updating when it resizes (e.g. node resize).
* Used to give CodeMirror and other components an explicit height that tracks their container.
*/
export function useResizeHeight(defaultHeight: number): [number, React.RefObject<HTMLDivElement | null>] {
const ref = useRef<HTMLDivElement | null>(null)
const [height, setHeight] = useState(defaultHeight)
useEffect(() => {
const el = ref.current
if (!el) return
const ro = new ResizeObserver((entries) => {
const entry = entries[0]
if (entry?.contentRect.height != null && entry.contentRect.height > 0) {
setHeight(entry.contentRect.height)
}
})
ro.observe(el)
setHeight(el.getBoundingClientRect().height)
return () => ro.disconnect()
}, [])
return [height, ref]
}