/** * @file Node editor toolbar component */ import * as React from "react"; import { useNodeDefinitionList } from "../../contexts/node-definitions/hooks/useNodeDefinitionList"; import { useEditorActionState } from "../../contexts/composed/EditorActionStateContext"; import { useNodeCanvas } from "../../contexts/composed/canvas/viewport/context"; import { useNodeEditor } from "../../contexts/composed/node-editor/context"; import { Button } from "../elements/Button"; import styles from "./NodeEditorToolbar.module.css"; import { countNodesByType, canAddNodeType } from "../../contexts/node-definitions/utils/nodeTypeLimits"; export type NodeEditorToolbarProps = { children?: React.ReactNode; }; export const NodeEditorToolbar: React.FC = React.memo(({ children }) => { const nodeDefinitions = useNodeDefinitionList(); const { actions: actionActions } = useEditorActionState(); const { state: canvasState } = useNodeCanvas(); const { state: editorState, actions } = useNodeEditor(); const commonNodeTypes = React.useMemo(() => { return nodeDefinitions .filter((definition) => ["standard", "input", "output", "process", "group"].includes(definition.type)) .slice(0, 5); }, [nodeDefinitions]); const nodeTypeCounts = React.useMemo(() => countNodesByType(editorState), [editorState]); const handleToolbarNodeCreate = React.useCallback( (nodeType: string) => { const nodeDefinition = nodeDefinitions.find((definition) => definition.type === nodeType); if (!nodeDefinition) { console.warn(`Node definition not found for type: ${nodeType}`); return; } if (!canAddNodeType(nodeType, nodeDefinitions, nodeTypeCounts)) { return; } const viewport = canvasState.viewport; const centerX = (-viewport.offset.x + window.innerWidth / 2) / viewport.scale; const centerY = (-viewport.offset.y + window.innerHeight / 2) / viewport.scale; const nodeId = `${nodeType}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; const newNode = { id: nodeId, type: nodeType, position: { x: centerX, y: centerY }, size: nodeDefinition.defaultSize || { width: 150, height: 50 }, // Use definition defaults but force empty title per requirements data: (() => { const base: Record = nodeDefinition.defaultData ? { ...(nodeDefinition.defaultData as Record) } : {}; return { ...base, title: "" }; })(), }; actions.addNode(newNode); actionActions.setInteractionSelection([nodeId]); actionActions.setEditingSelection([nodeId]); }, [nodeDefinitions, nodeTypeCounts, canvasState.viewport, actions, actionActions], ); const showNodeSearchMenu = React.useCallback(() => { const viewport = canvasState.viewport; const centerScreenX = window.innerWidth / 2; const centerScreenY = window.innerHeight / 2; const canvasX = (-viewport.offset.x + centerScreenX) / viewport.scale; const canvasY = (-viewport.offset.y + centerScreenY) / viewport.scale; actionActions.showContextMenu({ position: { x: centerScreenX, y: centerScreenY }, canvasPosition: { x: canvasX, y: canvasY } }); }, [canvasState.viewport, actionActions]); const toolbarContent = ( <> {commonNodeTypes.map((nodeType) => ( ))} {commonNodeTypes.length > 0 &&
} ); return (
{toolbarContent} {children}
); }); NodeEditorToolbar.displayName = "NodeEditorToolbar"; export type NodeEditorToolbarGroupProps = { children?: React.ReactNode; }; export const NodeEditorToolbarGroup: React.FC = React.memo(({ children }) => { return (
{children}
); }); NodeEditorToolbarGroup.displayName = "NodeEditorToolbarGroup"; export const NodeEditorToolbarSeparator: React.FC = React.memo(() => { return
; }); NodeEditorToolbarSeparator.displayName = "NodeEditorToolbarSeparator";