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,116 @@
/**
* Extensible node type registry. Register node types with registerNodeType();
* built-in types are registered in registerBuiltinNodes.ts.
* Use getRegisteredNodeTypes() / getNodeType(id) for defaults, validation, and UI.
*/
import type React from 'react'
import type { Node } from '@xyflow/react'
export type NodeHelpEntry = {
title: string
content: React.ReactNode
}
export type NodeTypeDescriptor = {
id: string
component: React.ComponentType<any>
defaultStyle: { width: number; height: number }
defaultData: Record<string, unknown>
idPrefix: string
hasInput: boolean
hasOutput: boolean
/** When this type is the connection target, which source types are allowed. Omit = all. */
allowedSourceTypes?: string[]
/** When this type is the connection source, which target types are allowed. Omit = all. */
allowedTargetTypes?: string[]
help: NodeHelpEntry
menuLabel: string
menuIcon: React.ReactNode
/** Optional: override default data for new nodes (e.g. set title from newId). */
getDefaultData?: (newId?: string) => Record<string, unknown>
/** Optional: data for Reset action; if omitted, getDefaultData(nodeId) or defaultData is used. */
getResetData?: (nodeId?: string) => Record<string, unknown>
/** Optional: label shown on edge when this type is the target (e.g. "render", "add input"). */
connectionLabel?: string
}
const registry = new Map<string, NodeTypeDescriptor>()
export function registerNodeType(descriptor: NodeTypeDescriptor): void {
if (registry.has(descriptor.id)) {
console.warn(`[nodeRegistry] Overwriting existing node type: ${descriptor.id}`)
}
registry.set(descriptor.id, descriptor)
}
export function getNodeType(id: string): NodeTypeDescriptor | undefined {
return registry.get(id)
}
export function getRegisteredNodeTypes(): NodeTypeDescriptor[] {
return Array.from(registry.values())
}
export function getRegisteredNodeTypeIds(): string[] {
return Array.from(registry.keys())
}
/** Default data for a new node of this type. Uses getDefaultData from descriptor if provided. */
export function getDefaultDataForType(type: string, newId?: string): Record<string, unknown> {
const desc = registry.get(type)
if (!desc) return {}
if (desc.getDefaultData) return { ...desc.getDefaultData(newId) }
const base = { ...desc.defaultData }
if (type === 'config' && newId) (base as any).title = `${newId}`
return base
}
/** Data for Reset action. Uses getResetData from descriptor if provided. */
export function getResetDataForType(type: string, nodeId?: string): Record<string, unknown> {
const desc = registry.get(type)
if (!desc) return getDefaultDataForType(type, nodeId)
if (desc.getResetData) return { ...desc.getResetData(nodeId) }
return getDefaultDataForType(type, nodeId)
}
/** Id prefix for this type (e.g. cfg_, rnd_). Used by getNextNodeId. */
export function getIdPrefix(type: string): string {
return getNodeType(type)?.idPrefix ?? 'node_'
}
/** Default style for this type. */
export function getDefaultStyle(type: string): { width: number; height: number } {
const desc = getNodeType(type)
if (desc) return desc.defaultStyle
return { width: 320, height: 320 }
}
/** Whether a connection from source to target is allowed based on registered types. */
export function isConnectionAllowed(
sourceType: string,
targetType: string,
sourceNodeId: string,
targetNodeId: string
): boolean {
if (sourceNodeId === targetNodeId) return false
const sourceDesc = getNodeType(sourceType)
const targetDesc = getNodeType(targetType)
if (!sourceDesc || !targetDesc) return false
if (!targetDesc.hasInput) return false
if (!sourceDesc.hasOutput) return false
if (targetDesc.allowedSourceTypes != null && !targetDesc.allowedSourceTypes.includes(sourceType)) return false
if (sourceDesc.allowedTargetTypes != null && !sourceDesc.allowedTargetTypes.includes(targetType)) return false
return true
}
/** Edge label when target is of this type (for AnimatedEdge). */
export function getConnectionLabelForTarget(targetType: string): string | undefined {
return getNodeType(targetType)?.connectionLabel
}
/** Help entry for a node type. Use in NodeHelpPopover. */
export function getNodeHelp(nodeType: string): NodeHelpEntry {
const desc = getNodeType(nodeType)
return desc?.help ?? { title: nodeType, content: null }
}