@umodeapporg/ui 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/assets/umode-ui.css +0 -5
  2. package/dist/components/Form/ControlledSelect.js +5 -3
  3. package/dist/components/Form/ControlledSelect.js.map +1 -1
  4. package/dist/components/UI/Flow/FlowCanvas.d.ts +4 -5
  5. package/dist/components/UI/Flow/FlowCanvas.js +58 -69
  6. package/dist/components/UI/Flow/FlowCanvas.js.map +1 -1
  7. package/dist/components/UI/Flow/FlowConnectionEdge.d.ts +8 -0
  8. package/dist/components/UI/Flow/FlowConnectionEdge.js +64 -3
  9. package/dist/components/UI/Flow/FlowConnectionEdge.js.map +1 -1
  10. package/dist/components/UI/Flow/FlowInspector.d.ts +1 -1
  11. package/dist/components/UI/Flow/FlowInspector.js.map +1 -1
  12. package/dist/components/UI/Flow/FlowLaneMap.js +2 -2
  13. package/dist/components/UI/Flow/FlowLaneMap.js.map +1 -1
  14. package/dist/components/UI/Flow/FlowResizablePanel.d.ts +1 -1
  15. package/dist/components/UI/Flow/FlowResizablePanel.js.map +1 -1
  16. package/dist/components/UI/Flow/flowBlockRegistry.d.ts +2 -2
  17. package/dist/components/UI/Flow/flowBlockRegistry.js +8 -3
  18. package/dist/components/UI/Flow/flowBlockRegistry.js.map +1 -1
  19. package/dist/components/UI/Flow/flowEditorStore.d.ts +7 -2
  20. package/dist/components/UI/Flow/flowEditorStore.js +56 -13
  21. package/dist/components/UI/Flow/flowEditorStore.js.map +1 -1
  22. package/dist/components/UI/Flow/flowGraph.d.ts +0 -1
  23. package/dist/components/UI/Flow/flowGraph.js +3 -4
  24. package/dist/components/UI/Flow/flowGraph.js.map +1 -1
  25. package/dist/components/UI/Flow/flowTypes.d.ts +2 -4
  26. package/dist/components/UI/Flow/index.d.ts +2 -4
  27. package/dist/components/UI/Flow/index.js +5 -9
  28. package/dist/components/UI/Flow/index.js.map +1 -1
  29. package/dist/locales/en/ui.json.d.ts +3 -6
  30. package/dist/locales/en/ui.json.js +1 -1
  31. package/dist/locales/index.d.ts +6 -12
  32. package/dist/locales/pt/ui.json.d.ts +3 -6
  33. package/dist/locales/pt/ui.json.js +1 -1
  34. package/package.json +1 -1
  35. package/dist/components/UI/Flow/FlowPalette.d.ts +0 -7
  36. package/dist/components/UI/Flow/FlowPalette.js +0 -90
  37. package/dist/components/UI/Flow/FlowPalette.js.map +0 -1
  38. package/dist/components/UI/Flow/flowConstants.d.ts +0 -2
  39. package/dist/components/UI/Flow/flowConstants.js +0 -5
  40. package/dist/components/UI/Flow/flowConstants.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"FlowCanvas.js","sources":["../../../../src/components/UI/Flow/FlowCanvas.tsx"],"sourcesContent":["import { DragEvent, KeyboardEvent, ReactNode, useCallback, useMemo, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { twMerge } from 'tailwind-merge';\n\nimport {\n Background,\n BackgroundVariant,\n type Connection,\n type EdgeChange,\n MarkerType,\n MiniMap,\n type NodeChange,\n type PanelPosition,\n ReactFlow,\n useReactFlow,\n} from '@xyflow/react';\n\nimport FlowBlockNode, { FLOW_INPUT_HANDLE_ID, type IFlowAddableBlock, type TFlowBlockNode } from './FlowBlockNode';\nimport FlowConnectionEdge, { type TFlowConnectionEdge } from './FlowConnectionEdge';\nimport { FLOW_BLOCK_DRAG_TYPE } from './flowConstants';\nimport { useFlowBlockRegistry, useFlowEditor, useFlowEditorStore } from './FlowEditorProvider';\nimport { canConnect, layoutFlowByLevels } from './flowGraph';\nimport type { IFlowIssue, IFlowPosition, IFlowRunOverlay, TFlowIssueSeverity, TFlowPortId } from './flowTypes';\nimport FlowZoomControls from './FlowZoomControls';\nimport { useFlowLabels } from './useFlowLabels';\n\nimport '@xyflow/react/dist/base.css';\nimport '../../../styles/flow.css';\n\nconst ATTRIBUTION_POSITION: PanelPosition = 'bottom-left';\nconst NODE_TYPES = { flowBlock: FlowBlockNode };\nconst EDGE_TYPES = { flowConnection: FlowConnectionEdge };\nconst MINIMAP_SIZE = { width: 160, height: 104 };\n/** A small flow fits on screen whole, where a minimap only covers blocks. */\nconst MINIMAP_MIN_NODES = 5;\nconst SNAP_GRID: [number, number] = [16, 16];\nconst PORTS: Array<TFlowPortId> = ['next', 'true', 'false', 'error'];\n\nexport interface IFlowCanvasProps {\n issues?: Array<IFlowIssue>;\n runOverlay?: IFlowRunOverlay | null;\n readOnly?: boolean;\n /** Accessible name of the canvas region, from `t()`. Defaults to the library label. */\n ariaLabel?: string;\n className?: string;\n /** Where the zoom and history controls sit; move them when floating chrome takes that corner. */\n controlsPosition?: PanelPosition;\n /** Floating chrome drawn over the canvas (toolbars, panels); position it with absolute classes. */\n children?: ReactNode;\n}\n\nconst isPortId = (value: string | null | undefined): value is TFlowPortId => PORTS.includes(value as TFlowPortId);\n\nconst worstSeverity = (issues: Array<IFlowIssue>): TFlowIssueSeverity | undefined => {\n if (issues.some((issue) => issue.severity === 'error')) return 'error';\n if (issues.some((issue) => issue.severity === 'warning')) return 'warning';\n return undefined;\n};\n\nconst isTypingTarget = (target: EventTarget | null): boolean =>\n target instanceof HTMLElement &&\n (target.isContentEditable || ['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName));\n\n/**\n * The editing surface. Renders the graph held by `FlowEditorProvider` and turns every\n * gesture (drag, connect, select, delete, drop, shortcuts) into a store action, so undo\n * and the dirty flag see all of them.\n */\nconst FlowCanvas: React.FC<IFlowCanvasProps> = ({\n issues = [],\n runOverlay,\n readOnly = false,\n ariaLabel,\n className,\n controlsPosition,\n children,\n}) => {\n const { t } = useTranslation('ui');\n const labels = useFlowLabels();\n const registry = useFlowBlockRegistry();\n const store = useFlowEditorStore();\n const { screenToFlowPosition } = useReactFlow();\n\n const graph = useFlowEditor((state) => state.graph);\n const selectedNodeIds = useFlowEditor((state) => state.selectedNodeIds);\n const selectedEdgeIds = useFlowEditor((state) => state.selectedEdgeIds);\n const canUndo = useFlowEditor((state) => state.past.length > 0);\n const canRedo = useFlowEditor((state) => state.future.length > 0);\n\n const safeIssues = useMemo(() => (Array.isArray(issues) ? issues : []), [issues]);\n // Measured sizes are view state, not part of the flow: kept here so the minimap can draw the blocks.\n const [measuredSizes, setMeasuredSizes] = useState<Record<string, { width: number; height: number }>>({});\n\n const addableBlocks = useMemo<Array<IFlowAddableBlock>>(() => {\n if (readOnly) return [];\n return registry.list\n .filter((definition) => definition.category !== 'trigger')\n .filter(\n (definition) =>\n definition.maxInstances === undefined ||\n graph.nodes.filter((node) => node.type === definition.type).length < definition.maxInstances,\n )\n .map((definition) => ({ type: definition.type, label: definition.label, icon: definition.icon }));\n }, [registry, graph.nodes, readOnly]);\n\n const nodes = useMemo<Array<TFlowBlockNode>>(\n () =>\n graph.nodes.map((node) => {\n const definition = registry.get(node.type);\n const nodeIssues = safeIssues.filter((issue) => issue.nodeId === node.nodeId);\n return {\n id: node.nodeId,\n type: 'flowBlock',\n position: node.position,\n measured: measuredSizes[node.nodeId],\n selected: selectedNodeIds.includes(node.nodeId),\n draggable: !readOnly,\n connectable: !readOnly,\n data: {\n icon: definition?.resolveIcon?.(node.config) ?? definition?.icon ?? 'help',\n title: node.label ?? definition?.resolveLabel?.(node.config) ?? definition?.label ?? node.type,\n categoryLabel: definition ? labels.categories[definition.category] : '',\n summary: definition\n ? definition.renderSummary(node.config)\n : labels.issueMessage({ code: 'unknownBlock', severity: 'error' }),\n hasInput: definition ? definition.category !== 'trigger' : true,\n outputs: definition?.outputs ?? [],\n connectedPorts: graph.edges.filter((edge) => edge.source === node.nodeId).map((edge) => edge.sourcePort),\n portLabels: labels.ports,\n addableBlocks,\n onAddFromPort: readOnly\n ? undefined\n : (port, type) => store.getState().addNodeFromPort(type, node.nodeId, port),\n disabled: node.disabled,\n issueSeverity: worstSeverity(nodeIssues),\n issueCount: nodeIssues.length,\n runStatus: runOverlay?.steps?.[node.nodeId],\n },\n };\n }),\n [\n graph.nodes,\n graph.edges,\n registry,\n safeIssues,\n selectedNodeIds,\n readOnly,\n labels,\n runOverlay,\n measuredSizes,\n addableBlocks,\n store,\n ],\n );\n\n const edges = useMemo<Array<TFlowConnectionEdge>>(() => {\n const traversed = new Set(runOverlay?.traversedEdgeIds ?? []);\n return graph.edges.map((edge) => ({\n id: edge.edgeId,\n type: 'flowConnection',\n source: edge.source,\n sourceHandle: edge.sourcePort,\n target: edge.target,\n targetHandle: FLOW_INPUT_HANDLE_ID,\n selected: selectedEdgeIds.includes(edge.edgeId),\n markerEnd: { type: MarkerType.ArrowClosed, width: 16, height: 16 },\n data: {\n label: edge.sourcePort === 'next' ? undefined : labels.ports[edge.sourcePort],\n traversed: traversed.has(edge.edgeId),\n issueSeverity: worstSeverity(safeIssues.filter((issue) => issue.edgeId === edge.edgeId)),\n },\n }));\n }, [graph.edges, selectedEdgeIds, runOverlay, labels, safeIssues]);\n\n const handleNodesChange = useCallback(\n (changes: Array<NodeChange<TFlowBlockNode>>) => {\n const positions: Record<string, IFlowPosition> = {};\n const sizes: Record<string, { width: number; height: number }> = {};\n let selection: Array<string> | null = null;\n for (const change of changes) {\n if (change.type === 'dimensions' && change.dimensions) sizes[change.id] = change.dimensions;\n if (change.type === 'position' && change.position && !readOnly) positions[change.id] = change.position;\n if (change.type === 'select') {\n const current: Array<string> = selection ?? store.getState().selectedNodeIds;\n selection = change.selected\n ? [...new Set([...current, change.id])]\n : current.filter((id) => id !== change.id);\n }\n }\n if (Object.keys(sizes).length > 0) setMeasuredSizes((current) => ({ ...current, ...sizes }));\n if (Object.keys(positions).length > 0) store.getState().moveNodes(positions);\n if (selection) store.getState().setSelection({ nodeIds: selection });\n },\n [store, readOnly],\n );\n\n const handleEdgesChange = useCallback(\n (changes: Array<EdgeChange<TFlowConnectionEdge>>) => {\n let selection: Array<string> | null = null;\n for (const change of changes) {\n if (change.type !== 'select') continue;\n const current: Array<string> = selection ?? store.getState().selectedEdgeIds;\n selection = change.selected ? [...new Set([...current, change.id])] : current.filter((id) => id !== change.id);\n }\n if (selection) store.getState().setSelection({ edgeIds: selection });\n },\n [store],\n );\n\n const handleConnect = useCallback(\n (connection: Connection) => {\n if (readOnly || !isPortId(connection.sourceHandle)) return;\n store.getState().connect(connection.source, connection.sourceHandle, connection.target);\n },\n [store, readOnly],\n );\n\n const isValidConnection = useCallback(\n (connection: Connection | TFlowConnectionEdge) =>\n isPortId(connection.sourceHandle) &&\n canConnect(store.getState().graph, registry, {\n source: connection.source,\n sourcePort: connection.sourceHandle,\n target: connection.target,\n }),\n [store, registry],\n );\n\n const handleDragOver = useCallback((event: DragEvent<HTMLDivElement>) => {\n if (!event.dataTransfer.types.includes(FLOW_BLOCK_DRAG_TYPE)) return;\n event.preventDefault();\n event.dataTransfer.dropEffect = 'copy';\n }, []);\n\n const handleDrop = useCallback(\n (event: DragEvent<HTMLDivElement>) => {\n const blockType = event.dataTransfer.getData(FLOW_BLOCK_DRAG_TYPE);\n if (!blockType || readOnly) return;\n event.preventDefault();\n store.getState().addNode(blockType, screenToFlowPosition({ x: event.clientX, y: event.clientY }));\n },\n [store, readOnly, screenToFlowPosition],\n );\n\n const handleKeyDown = useCallback(\n (event: KeyboardEvent<HTMLDivElement>) => {\n if (readOnly || isTypingTarget(event.target)) return;\n const state = store.getState();\n const withModifier = event.metaKey || event.ctrlKey;\n const key = event.key.toLowerCase();\n\n if (event.key === 'Delete' || event.key === 'Backspace') state.removeSelected();\n else if (withModifier && key === 'z' && event.shiftKey) state.redo();\n else if (withModifier && key === 'z') state.undo();\n else if (withModifier && key === 'y') state.redo();\n else if (withModifier && key === 'c') state.copySelected();\n else if (withModifier && key === 'v') state.paste();\n else if (withModifier && key === 'd') state.duplicateSelected();\n else return;\n event.preventDefault();\n },\n [store, readOnly],\n );\n\n const handleLayout = useCallback(() => {\n const state = store.getState();\n state.recordHistory();\n state.moveNodes(layoutFlowByLevels(state.graph, registry));\n }, [store, registry]);\n\n return (\n <div\n role=\"region\"\n aria-label={ariaLabel ?? t('flow.canvas')}\n tabIndex={0}\n onKeyDown={handleKeyDown}\n className={twMerge(\n 'umode-flow relative h-full min-h-[24rem] w-full rounded-xl bg-canvas focus:outline-none',\n className,\n )}\n >\n <ReactFlow<TFlowBlockNode, TFlowConnectionEdge>\n nodes={nodes}\n edges={edges}\n nodeTypes={NODE_TYPES}\n edgeTypes={EDGE_TYPES}\n onNodesChange={handleNodesChange}\n onEdgesChange={handleEdgesChange}\n onConnect={handleConnect}\n isValidConnection={isValidConnection}\n onNodeDragStart={() => store.getState().recordHistory()}\n onPaneClick={() => store.getState().setSelection({ nodeIds: [], edgeIds: [] })}\n onDragOver={handleDragOver}\n onDrop={handleDrop}\n deleteKeyCode={null}\n nodesDraggable={!readOnly}\n nodesConnectable={!readOnly}\n snapToGrid\n snapGrid={SNAP_GRID}\n minZoom={0.25}\n maxZoom={2}\n fitView\n fitViewOptions={{ padding: 0.2, maxZoom: 1 }}\n attributionPosition={ATTRIBUTION_POSITION}\n >\n <Background variant={BackgroundVariant.Dots} gap={16} size={1} />\n {nodes.length >= MINIMAP_MIN_NODES && (\n <MiniMap pannable zoomable position=\"bottom-right\" style={MINIMAP_SIZE} />\n )}\n <FlowZoomControls\n onUndo={readOnly ? undefined : () => store.getState().undo()}\n onRedo={readOnly ? undefined : () => store.getState().redo()}\n canUndo={canUndo}\n canRedo={canRedo}\n onLayout={readOnly ? undefined : handleLayout}\n position={controlsPosition}\n />\n </ReactFlow>\n {children}\n </div>\n );\n};\n\nexport default FlowCanvas;\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"FlowCanvas.js","sources":["../../../../src/components/UI/Flow/FlowCanvas.tsx"],"sourcesContent":["import { KeyboardEvent, useCallback, useMemo, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { twMerge } from 'tailwind-merge';\n\nimport {\n Background,\n BackgroundVariant,\n type Connection,\n type EdgeChange,\n MarkerType,\n MiniMap,\n type NodeChange,\n type PanelPosition,\n type ProOptions,\n ReactFlow,\n} from '@xyflow/react';\n\nimport FlowBlockNode, { FLOW_INPUT_HANDLE_ID, type IFlowAddableBlock, type TFlowBlockNode } from './FlowBlockNode';\nimport { flowOutputsOf } from './flowBlockRegistry';\nimport FlowConnectionEdge, { type TFlowConnectionEdge } from './FlowConnectionEdge';\nimport { useFlowBlockRegistry, useFlowEditor, useFlowEditorStore } from './FlowEditorProvider';\nimport { canConnect, layoutFlowByLevels } from './flowGraph';\nimport type { IFlowIssue, IFlowPosition, IFlowRunOverlay, TFlowIssueSeverity, TFlowPortId } from './flowTypes';\nimport FlowZoomControls from './FlowZoomControls';\nimport { useFlowLabels } from './useFlowLabels';\n\nimport '@xyflow/react/dist/base.css';\nimport '../../../styles/flow.css';\n\nconst PRO_OPTIONS: ProOptions = { hideAttribution: true };\nconst NODE_TYPES = { flowBlock: FlowBlockNode };\nconst EDGE_TYPES = { flowConnection: FlowConnectionEdge };\nconst MINIMAP_SIZE = { width: 160, height: 104 };\n/** A small flow fits on screen whole, where a minimap only covers blocks. */\nconst MINIMAP_MIN_NODES = 5;\nconst SNAP_GRID: [number, number] = [16, 16];\nconst PORTS: Array<TFlowPortId> = ['next', 'true', 'false', 'error'];\n\nexport interface IFlowCanvasProps {\n issues?: Array<IFlowIssue>;\n runOverlay?: IFlowRunOverlay | null;\n readOnly?: boolean;\n /** Accessible name of the canvas region, from `t()`. Defaults to the library label. */\n ariaLabel?: string;\n className?: string;\n /** Where the zoom and history controls sit. */\n controlsPosition?: PanelPosition;\n /** The dot grid behind the blocks. Blocks still snap to the grid when it is hidden. */\n background?: 'dots' | 'none';\n}\n\nconst isPortId = (value: string | null | undefined): value is TFlowPortId => PORTS.includes(value as TFlowPortId);\n\nconst worstSeverity = (issues: Array<IFlowIssue>): TFlowIssueSeverity | undefined => {\n if (issues.some((issue) => issue.severity === 'error')) return 'error';\n if (issues.some((issue) => issue.severity === 'warning')) return 'warning';\n return undefined;\n};\n\nconst isTypingTarget = (target: EventTarget | null): boolean =>\n target instanceof HTMLElement &&\n (target.isContentEditable || ['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName));\n\n/**\n * The editing surface. Renders the graph held by `FlowEditorProvider` and turns every\n * gesture (drag, connect, select, delete, shortcuts) into a store action, so undo\n * and the dirty flag see all of them.\n */\nconst FlowCanvas: React.FC<IFlowCanvasProps> = ({\n issues = [],\n runOverlay,\n readOnly = false,\n ariaLabel,\n className,\n controlsPosition,\n background = 'dots',\n}) => {\n const { t } = useTranslation('ui');\n const labels = useFlowLabels();\n const registry = useFlowBlockRegistry();\n const store = useFlowEditorStore();\n\n const graph = useFlowEditor((state) => state.graph);\n const selectedNodeIds = useFlowEditor((state) => state.selectedNodeIds);\n const selectedEdgeIds = useFlowEditor((state) => state.selectedEdgeIds);\n const canUndo = useFlowEditor((state) => state.past.length > 0);\n const canRedo = useFlowEditor((state) => state.future.length > 0);\n\n const safeIssues = useMemo(() => (Array.isArray(issues) ? issues : []), [issues]);\n // Measured sizes are view state, not part of the flow: kept here so the minimap can draw the blocks.\n const [measuredSizes, setMeasuredSizes] = useState<Record<string, { width: number; height: number }>>({});\n\n const addableBlocks = useMemo<Array<IFlowAddableBlock>>(() => {\n if (readOnly) return [];\n return registry.list\n .filter((definition) => definition.category !== 'trigger')\n .filter(\n (definition) =>\n definition.maxInstances === undefined ||\n graph.nodes.filter((node) => node.type === definition.type).length < definition.maxInstances,\n )\n .map((definition) => ({ type: definition.type, label: definition.label, icon: definition.icon }));\n }, [registry, graph.nodes, readOnly]);\n\n // A block in the middle of an edge needs an output to carry the flow on to the old target.\n const insertableBlocks = useMemo<Array<IFlowAddableBlock>>(\n () =>\n addableBlocks.filter((block) => {\n const definition = registry.get(block.type);\n return definition ? flowOutputsOf(definition, definition.createDefaultConfig()).length > 0 : false;\n }),\n [addableBlocks, registry],\n );\n\n const nodes = useMemo<Array<TFlowBlockNode>>(\n () =>\n graph.nodes.map((node) => {\n const definition = registry.get(node.type);\n const nodeIssues = safeIssues.filter((issue) => issue.nodeId === node.nodeId);\n return {\n id: node.nodeId,\n type: 'flowBlock',\n position: node.position,\n measured: measuredSizes[node.nodeId],\n selected: selectedNodeIds.includes(node.nodeId),\n draggable: !readOnly,\n connectable: !readOnly,\n data: {\n icon: definition?.resolveIcon?.(node.config) ?? definition?.icon ?? 'help',\n title: node.label ?? definition?.resolveLabel?.(node.config) ?? definition?.label ?? node.type,\n categoryLabel: definition ? labels.categories[definition.category] : '',\n summary: definition\n ? definition.renderSummary(node.config)\n : labels.issueMessage({ code: 'unknownBlock', severity: 'error' }),\n hasInput: definition ? definition.category !== 'trigger' : true,\n outputs: flowOutputsOf(definition, node.config),\n connectedPorts: graph.edges.filter((edge) => edge.source === node.nodeId).map((edge) => edge.sourcePort),\n portLabels: labels.ports,\n addableBlocks,\n onAddFromPort: readOnly\n ? undefined\n : (port, type) => store.getState().addNodeFromPort(type, node.nodeId, port),\n disabled: node.disabled,\n issueSeverity: worstSeverity(nodeIssues),\n issueCount: nodeIssues.length,\n runStatus: runOverlay?.steps?.[node.nodeId],\n },\n };\n }),\n [\n graph.nodes,\n graph.edges,\n registry,\n safeIssues,\n selectedNodeIds,\n readOnly,\n labels,\n runOverlay,\n measuredSizes,\n addableBlocks,\n store,\n ],\n );\n\n const edges = useMemo<Array<TFlowConnectionEdge>>(() => {\n const traversed = new Set(runOverlay?.traversedEdgeIds ?? []);\n return graph.edges.map((edge) => ({\n id: edge.edgeId,\n type: 'flowConnection',\n source: edge.source,\n sourceHandle: edge.sourcePort,\n target: edge.target,\n targetHandle: FLOW_INPUT_HANDLE_ID,\n selected: selectedEdgeIds.includes(edge.edgeId),\n markerEnd: { type: MarkerType.ArrowClosed, width: 16, height: 16 },\n data: {\n label: edge.sourcePort === 'next' ? undefined : labels.ports[edge.sourcePort],\n traversed: traversed.has(edge.edgeId),\n issueSeverity: worstSeverity(safeIssues.filter((issue) => issue.edgeId === edge.edgeId)),\n insertableBlocks,\n onInsert: readOnly ? undefined : (type: string) => store.getState().insertNodeOnEdge(type, edge.edgeId),\n },\n }));\n }, [graph.edges, selectedEdgeIds, runOverlay, labels, safeIssues, insertableBlocks, readOnly, store]);\n\n const handleNodesChange = useCallback(\n (changes: Array<NodeChange<TFlowBlockNode>>) => {\n const positions: Record<string, IFlowPosition> = {};\n const sizes: Record<string, { width: number; height: number }> = {};\n let selection: Array<string> | null = null;\n for (const change of changes) {\n if (change.type === 'dimensions' && change.dimensions) sizes[change.id] = change.dimensions;\n if (change.type === 'position' && change.position && !readOnly) positions[change.id] = change.position;\n if (change.type === 'select') {\n const current: Array<string> = selection ?? store.getState().selectedNodeIds;\n selection = change.selected\n ? [...new Set([...current, change.id])]\n : current.filter((id) => id !== change.id);\n }\n }\n if (Object.keys(sizes).length > 0) setMeasuredSizes((current) => ({ ...current, ...sizes }));\n if (Object.keys(positions).length > 0) store.getState().moveNodes(positions);\n if (selection) store.getState().setSelection({ nodeIds: selection });\n },\n [store, readOnly],\n );\n\n const handleEdgesChange = useCallback(\n (changes: Array<EdgeChange<TFlowConnectionEdge>>) => {\n let selection: Array<string> | null = null;\n for (const change of changes) {\n if (change.type !== 'select') continue;\n const current: Array<string> = selection ?? store.getState().selectedEdgeIds;\n selection = change.selected ? [...new Set([...current, change.id])] : current.filter((id) => id !== change.id);\n }\n if (selection) store.getState().setSelection({ edgeIds: selection });\n },\n [store],\n );\n\n const handleConnect = useCallback(\n (connection: Connection) => {\n if (readOnly || !isPortId(connection.sourceHandle)) return;\n store.getState().connect(connection.source, connection.sourceHandle, connection.target);\n },\n [store, readOnly],\n );\n\n const isValidConnection = useCallback(\n (connection: Connection | TFlowConnectionEdge) =>\n isPortId(connection.sourceHandle) &&\n canConnect(store.getState().graph, registry, {\n source: connection.source,\n sourcePort: connection.sourceHandle,\n target: connection.target,\n }),\n [store, registry],\n );\n\n const handleKeyDown = useCallback(\n (event: KeyboardEvent<HTMLDivElement>) => {\n if (readOnly || isTypingTarget(event.target)) return;\n const state = store.getState();\n const withModifier = event.metaKey || event.ctrlKey;\n const key = event.key.toLowerCase();\n\n if (event.key === 'Delete' || event.key === 'Backspace') state.removeSelected();\n else if (withModifier && key === 'z' && event.shiftKey) state.redo();\n else if (withModifier && key === 'z') state.undo();\n else if (withModifier && key === 'y') state.redo();\n else if (withModifier && key === 'c') state.copySelected();\n else if (withModifier && key === 'v') state.paste();\n else if (withModifier && key === 'd') state.duplicateSelected();\n else return;\n event.preventDefault();\n },\n [store, readOnly],\n );\n\n const handleLayout = useCallback(() => {\n const state = store.getState();\n state.recordHistory();\n state.moveNodes(layoutFlowByLevels(state.graph, registry));\n }, [store, registry]);\n\n return (\n <div\n role=\"region\"\n aria-label={ariaLabel ?? t('flow.canvas')}\n tabIndex={0}\n onKeyDown={handleKeyDown}\n className={twMerge(\n 'umode-flow relative h-full min-h-[24rem] w-full rounded-xl bg-canvas focus:outline-none',\n className,\n )}\n >\n <ReactFlow<TFlowBlockNode, TFlowConnectionEdge>\n nodes={nodes}\n edges={edges}\n nodeTypes={NODE_TYPES}\n edgeTypes={EDGE_TYPES}\n onNodesChange={handleNodesChange}\n onEdgesChange={handleEdgesChange}\n onConnect={handleConnect}\n isValidConnection={isValidConnection}\n onNodeDragStart={() => store.getState().recordHistory()}\n onPaneClick={() => store.getState().setSelection({ nodeIds: [], edgeIds: [] })}\n deleteKeyCode={null}\n nodesDraggable={!readOnly}\n nodesConnectable={!readOnly}\n snapToGrid\n snapGrid={SNAP_GRID}\n minZoom={0.25}\n maxZoom={2}\n fitView\n fitViewOptions={{ padding: 0.2, maxZoom: 1 }}\n proOptions={PRO_OPTIONS}\n >\n {background === 'dots' && <Background variant={BackgroundVariant.Dots} gap={16} size={1} />}\n {nodes.length >= MINIMAP_MIN_NODES && (\n <MiniMap pannable zoomable position=\"bottom-right\" style={MINIMAP_SIZE} />\n )}\n <FlowZoomControls\n onUndo={readOnly ? undefined : () => store.getState().undo()}\n onRedo={readOnly ? undefined : () => store.getState().redo()}\n canUndo={canUndo}\n canRedo={canRedo}\n onLayout={readOnly ? undefined : handleLayout}\n position={controlsPosition}\n />\n </ReactFlow>\n </div>\n );\n};\n\nexport default FlowCanvas;\n"],"names":[],"mappings":";;;;;;;;;;;;;;AA6BA,MAAM,cAA0B,EAAE,iBAAiB,KAAA;AACnD,MAAM,aAAa,EAAE,WAAW,cAAA;AAChC,MAAM,aAAa,EAAE,gBAAgB,mBAAA;AACrC,MAAM,eAAe,EAAE,OAAO,KAAK,QAAQ,IAAA;AAE3C,MAAM,oBAAoB;AAC1B,MAAM,YAA8B,CAAC,IAAI,EAAE;AAC3C,MAAM,QAA4B,CAAC,QAAQ,QAAQ,SAAS,OAAO;AAenE,MAAM,WAAW,CAAC,UAA2D,MAAM,SAAS,KAAoB;AAEhH,MAAM,gBAAgB,CAAC,WAA8D;AACnF,MAAI,OAAO,KAAK,CAAC,UAAU,MAAM,aAAa,OAAO,EAAG,QAAO;AAC/D,MAAI,OAAO,KAAK,CAAC,UAAU,MAAM,aAAa,SAAS,EAAG,QAAO;AACjE,SAAO;AACT;AAEA,MAAM,iBAAiB,CAAC,WACtB,kBAAkB,gBACjB,OAAO,qBAAqB,CAAC,SAAS,YAAY,QAAQ,EAAE,SAAS,OAAO,OAAO;AAOtF,MAAM,aAAyC,CAAC;AAAA,EAC9C,SAAS,CAAA;AAAA,EACT;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa;AACf,MAAM;AACJ,QAAM,EAAE,EAAA,IAAM,eAAe,IAAI;AACjC,QAAM,SAAS,cAAA;AACf,QAAM,WAAW,qBAAA;AACjB,QAAM,QAAQ,mBAAA;AAEd,QAAM,QAAQ,cAAc,CAAC,UAAU,MAAM,KAAK;AAClD,QAAM,kBAAkB,cAAc,CAAC,UAAU,MAAM,eAAe;AACtE,QAAM,kBAAkB,cAAc,CAAC,UAAU,MAAM,eAAe;AACtE,QAAM,UAAU,cAAc,CAAC,UAAU,MAAM,KAAK,SAAS,CAAC;AAC9D,QAAM,UAAU,cAAc,CAAC,UAAU,MAAM,OAAO,SAAS,CAAC;AAEhE,QAAM,aAAa,QAAQ,MAAO,MAAM,QAAQ,MAAM,IAAI,SAAS,CAAA,GAAK,CAAC,MAAM,CAAC;AAEhF,QAAM,CAAC,eAAe,gBAAgB,IAAI,SAA4D,CAAA,CAAE;AAExG,QAAM,gBAAgB,QAAkC,MAAM;AAC5D,QAAI,iBAAiB,CAAA;AACrB,WAAO,SAAS,KACb,OAAO,CAAC,eAAe,WAAW,aAAa,SAAS,EACxD;AAAA,MACC,CAAC,eACC,WAAW,iBAAiB,UAC5B,MAAM,MAAM,OAAO,CAAC,SAAS,KAAK,SAAS,WAAW,IAAI,EAAE,SAAS,WAAW;AAAA,IAAA,EAEnF,IAAI,CAAC,gBAAgB,EAAE,MAAM,WAAW,MAAM,OAAO,WAAW,OAAO,MAAM,WAAW,OAAO;AAAA,EACpG,GAAG,CAAC,UAAU,MAAM,OAAO,QAAQ,CAAC;AAGpC,QAAM,mBAAmB;AAAA,IACvB,MACE,cAAc,OAAO,CAAC,UAAU;AAC9B,YAAM,aAAa,SAAS,IAAI,MAAM,IAAI;AAC1C,aAAO,aAAa,cAAc,YAAY,WAAW,qBAAqB,EAAE,SAAS,IAAI;AAAA,IAC/F,CAAC;AAAA,IACH,CAAC,eAAe,QAAQ;AAAA,EAAA;AAG1B,QAAM,QAAQ;AAAA,IACZ,MACE,MAAM,MAAM,IAAI,CAAC,SAAS;AACxB,YAAM,aAAa,SAAS,IAAI,KAAK,IAAI;AACzC,YAAM,aAAa,WAAW,OAAO,CAAC,UAAU,MAAM,WAAW,KAAK,MAAM;AAC5E,aAAO;AAAA,QACL,IAAI,KAAK;AAAA,QACT,MAAM;AAAA,QACN,UAAU,KAAK;AAAA,QACf,UAAU,cAAc,KAAK,MAAM;AAAA,QACnC,UAAU,gBAAgB,SAAS,KAAK,MAAM;AAAA,QAC9C,WAAW,CAAC;AAAA,QACZ,aAAa,CAAC;AAAA,QACd,MAAM;AAAA,UACJ,MAAM,YAAY,cAAc,KAAK,MAAM,KAAK,YAAY,QAAQ;AAAA,UACpE,OAAO,KAAK,SAAS,YAAY,eAAe,KAAK,MAAM,KAAK,YAAY,SAAS,KAAK;AAAA,UAC1F,eAAe,aAAa,OAAO,WAAW,WAAW,QAAQ,IAAI;AAAA,UACrE,SAAS,aACL,WAAW,cAAc,KAAK,MAAM,IACpC,OAAO,aAAa,EAAE,MAAM,gBAAgB,UAAU,SAAS;AAAA,UACnE,UAAU,aAAa,WAAW,aAAa,YAAY;AAAA,UAC3D,SAAS,cAAc,YAAY,KAAK,MAAM;AAAA,UAC9C,gBAAgB,MAAM,MAAM,OAAO,CAAC,SAAS,KAAK,WAAW,KAAK,MAAM,EAAE,IAAI,CAAC,SAAS,KAAK,UAAU;AAAA,UACvG,YAAY,OAAO;AAAA,UACnB;AAAA,UACA,eAAe,WACX,SACA,CAAC,MAAM,SAAS,MAAM,SAAA,EAAW,gBAAgB,MAAM,KAAK,QAAQ,IAAI;AAAA,UAC5E,UAAU,KAAK;AAAA,UACf,eAAe,cAAc,UAAU;AAAA,UACvC,YAAY,WAAW;AAAA,UACvB,WAAW,YAAY,QAAQ,KAAK,MAAM;AAAA,QAAA;AAAA,MAC5C;AAAA,IAEJ,CAAC;AAAA,IACH;AAAA,MACE,MAAM;AAAA,MACN,MAAM;AAAA,MACN;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IAAA;AAAA,EACF;AAGF,QAAM,QAAQ,QAAoC,MAAM;AACtD,UAAM,YAAY,IAAI,IAAI,YAAY,oBAAoB,CAAA,CAAE;AAC5D,WAAO,MAAM,MAAM,IAAI,CAAC,UAAU;AAAA,MAChC,IAAI,KAAK;AAAA,MACT,MAAM;AAAA,MACN,QAAQ,KAAK;AAAA,MACb,cAAc,KAAK;AAAA,MACnB,QAAQ,KAAK;AAAA,MACb,cAAc;AAAA,MACd,UAAU,gBAAgB,SAAS,KAAK,MAAM;AAAA,MAC9C,WAAW,EAAE,MAAM,WAAW,aAAa,OAAO,IAAI,QAAQ,GAAA;AAAA,MAC9D,MAAM;AAAA,QACJ,OAAO,KAAK,eAAe,SAAS,SAAY,OAAO,MAAM,KAAK,UAAU;AAAA,QAC5E,WAAW,UAAU,IAAI,KAAK,MAAM;AAAA,QACpC,eAAe,cAAc,WAAW,OAAO,CAAC,UAAU,MAAM,WAAW,KAAK,MAAM,CAAC;AAAA,QACvF;AAAA,QACA,UAAU,WAAW,SAAY,CAAC,SAAiB,MAAM,SAAA,EAAW,iBAAiB,MAAM,KAAK,MAAM;AAAA,MAAA;AAAA,IACxG,EACA;AAAA,EACJ,GAAG,CAAC,MAAM,OAAO,iBAAiB,YAAY,QAAQ,YAAY,kBAAkB,UAAU,KAAK,CAAC;AAEpG,QAAM,oBAAoB;AAAA,IACxB,CAAC,YAA+C;AAC9C,YAAM,YAA2C,CAAA;AACjD,YAAM,QAA2D,CAAA;AACjE,UAAI,YAAkC;AACtC,iBAAW,UAAU,SAAS;AAC5B,YAAI,OAAO,SAAS,gBAAgB,OAAO,WAAY,OAAM,OAAO,EAAE,IAAI,OAAO;AACjF,YAAI,OAAO,SAAS,cAAc,OAAO,YAAY,CAAC,SAAU,WAAU,OAAO,EAAE,IAAI,OAAO;AAC9F,YAAI,OAAO,SAAS,UAAU;AAC5B,gBAAM,UAAyB,aAAa,MAAM,SAAA,EAAW;AAC7D,sBAAY,OAAO,WACf,CAAC,uBAAO,IAAI,CAAC,GAAG,SAAS,OAAO,EAAE,CAAC,CAAC,IACpC,QAAQ,OAAO,CAAC,OAAO,OAAO,OAAO,EAAE;AAAA,QAC7C;AAAA,MACF;AACA,UAAI,OAAO,KAAK,KAAK,EAAE,SAAS,EAAG,kBAAiB,CAAC,aAAa,EAAE,GAAG,SAAS,GAAG,QAAQ;AAC3F,UAAI,OAAO,KAAK,SAAS,EAAE,SAAS,EAAG,OAAM,SAAA,EAAW,UAAU,SAAS;AAC3E,UAAI,iBAAiB,SAAA,EAAW,aAAa,EAAE,SAAS,WAAW;AAAA,IACrE;AAAA,IACA,CAAC,OAAO,QAAQ;AAAA,EAAA;AAGlB,QAAM,oBAAoB;AAAA,IACxB,CAAC,YAAoD;AACnD,UAAI,YAAkC;AACtC,iBAAW,UAAU,SAAS;AAC5B,YAAI,OAAO,SAAS,SAAU;AAC9B,cAAM,UAAyB,aAAa,MAAM,SAAA,EAAW;AAC7D,oBAAY,OAAO,WAAW,CAAC,uBAAO,IAAI,CAAC,GAAG,SAAS,OAAO,EAAE,CAAC,CAAC,IAAI,QAAQ,OAAO,CAAC,OAAO,OAAO,OAAO,EAAE;AAAA,MAC/G;AACA,UAAI,iBAAiB,SAAA,EAAW,aAAa,EAAE,SAAS,WAAW;AAAA,IACrE;AAAA,IACA,CAAC,KAAK;AAAA,EAAA;AAGR,QAAM,gBAAgB;AAAA,IACpB,CAAC,eAA2B;AAC1B,UAAI,YAAY,CAAC,SAAS,WAAW,YAAY,EAAG;AACpD,YAAM,WAAW,QAAQ,WAAW,QAAQ,WAAW,cAAc,WAAW,MAAM;AAAA,IACxF;AAAA,IACA,CAAC,OAAO,QAAQ;AAAA,EAAA;AAGlB,QAAM,oBAAoB;AAAA,IACxB,CAAC,eACC,SAAS,WAAW,YAAY,KAChC,WAAW,MAAM,SAAA,EAAW,OAAO,UAAU;AAAA,MAC3C,QAAQ,WAAW;AAAA,MACnB,YAAY,WAAW;AAAA,MACvB,QAAQ,WAAW;AAAA,IAAA,CACpB;AAAA,IACH,CAAC,OAAO,QAAQ;AAAA,EAAA;AAGlB,QAAM,gBAAgB;AAAA,IACpB,CAAC,UAAyC;AACxC,UAAI,YAAY,eAAe,MAAM,MAAM,EAAG;AAC9C,YAAM,QAAQ,MAAM,SAAA;AACpB,YAAM,eAAe,MAAM,WAAW,MAAM;AAC5C,YAAM,MAAM,MAAM,IAAI,YAAA;AAEtB,UAAI,MAAM,QAAQ,YAAY,MAAM,QAAQ,mBAAmB,eAAA;AAAA,eACtD,gBAAgB,QAAQ,OAAO,MAAM,gBAAgB,KAAA;AAAA,eACrD,gBAAgB,QAAQ,IAAK,OAAM,KAAA;AAAA,eACnC,gBAAgB,QAAQ,IAAK,OAAM,KAAA;AAAA,eACnC,gBAAgB,QAAQ,IAAK,OAAM,aAAA;AAAA,eACnC,gBAAgB,QAAQ,IAAK,OAAM,MAAA;AAAA,eACnC,gBAAgB,QAAQ,IAAK,OAAM,kBAAA;AAAA,UACvC;AACL,YAAM,eAAA;AAAA,IACR;AAAA,IACA,CAAC,OAAO,QAAQ;AAAA,EAAA;AAGlB,QAAM,eAAe,YAAY,MAAM;AACrC,UAAM,QAAQ,MAAM,SAAA;AACpB,UAAM,cAAA;AACN,UAAM,UAAU,mBAAmB,MAAM,OAAO,QAAQ,CAAC;AAAA,EAC3D,GAAG,CAAC,OAAO,QAAQ,CAAC;AAEpB,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAY,aAAa,EAAE,aAAa;AAAA,MACxC,UAAU;AAAA,MACV,WAAW;AAAA,MACX,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MAAA;AAAA,MAGF,UAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC;AAAA,UACA;AAAA,UACA,WAAW;AAAA,UACX,WAAW;AAAA,UACX,eAAe;AAAA,UACf,eAAe;AAAA,UACf,WAAW;AAAA,UACX;AAAA,UACA,iBAAiB,MAAM,MAAM,SAAA,EAAW,cAAA;AAAA,UACxC,aAAa,MAAM,MAAM,WAAW,aAAa,EAAE,SAAS,CAAA,GAAI,SAAS,CAAA,GAAI;AAAA,UAC7E,eAAe;AAAA,UACf,gBAAgB,CAAC;AAAA,UACjB,kBAAkB,CAAC;AAAA,UACnB,YAAU;AAAA,UACV,UAAU;AAAA,UACV,SAAS;AAAA,UACT,SAAS;AAAA,UACT,SAAO;AAAA,UACP,gBAAgB,EAAE,SAAS,KAAK,SAAS,EAAA;AAAA,UACzC,YAAY;AAAA,UAEX,UAAA;AAAA,YAAA,eAAe,8BAAW,YAAA,EAAW,SAAS,kBAAkB,MAAM,KAAK,IAAI,MAAM,EAAA,CAAG;AAAA,YACxF,MAAM,UAAU,qBACf,oBAAC,SAAA,EAAQ,UAAQ,MAAC,UAAQ,MAAC,UAAS,gBAAe,OAAO,aAAA,CAAc;AAAA,YAE1E;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,QAAQ,WAAW,SAAY,MAAM,MAAM,SAAA,EAAW,KAAA;AAAA,gBACtD,QAAQ,WAAW,SAAY,MAAM,MAAM,SAAA,EAAW,KAAA;AAAA,gBACtD;AAAA,gBACA;AAAA,gBACA,UAAU,WAAW,SAAY;AAAA,gBACjC,UAAU;AAAA,cAAA;AAAA,YAAA;AAAA,UACZ;AAAA,QAAA;AAAA,MAAA;AAAA,IACF;AAAA,EAAA;AAGN;"}
@@ -1,10 +1,18 @@
1
1
  import { Edge, EdgeProps } from '@xyflow/react';
2
+ import { IFlowAddableBlock } from './FlowBlockNode';
2
3
  import { TFlowIssueSeverity } from './flowTypes';
3
4
  export interface IFlowConnectionEdgeData extends Record<string, unknown> {
4
5
  label?: string;
5
6
  traversed?: boolean;
6
7
  issueSeverity?: TFlowIssueSeverity;
8
+ /** What can go in the middle of this edge; empty in a read-only canvas. */
9
+ insertableBlocks?: Array<IFlowAddableBlock>;
10
+ onInsert?: (type: string) => void;
7
11
  }
8
12
  export type TFlowConnectionEdge = Edge<IFlowConnectionEdgeData, 'flowConnection'>;
13
+ /**
14
+ * A smooth-step connection. Hovering it (or selecting it) shows a "+" at its middle that puts a
15
+ * block between the two it connects, so a step can be added without rewiring by hand.
16
+ */
9
17
  declare const FlowConnectionEdge: React.FC<EdgeProps<TFlowConnectionEdge>>;
10
18
  export default FlowConnectionEdge;
@@ -1,7 +1,13 @@
1
1
  import { jsxs, Fragment, jsx } from "react/jsx-runtime";
2
+ import { useState } from "react";
2
3
  import classNames from "classnames";
4
+ import { useTranslation } from "react-i18next";
5
+ import ControlledButton from "../../Form/ControlledButton.js";
6
+ import ControlledMenu from "../ControlledMenu.js";
7
+ import { MaterialSymbol } from "../MaterialSymbol/index.js";
3
8
  import { getSmoothStepPath, BaseEdge, EdgeLabelRenderer } from "@xyflow/react";
4
9
  import OutlinedChip from "../Chips/OutlinedChip.js";
10
+ const HOVER_STROKE_WIDTH = 24;
5
11
  const FlowConnectionEdge = ({
6
12
  id,
7
13
  sourceX,
@@ -14,6 +20,9 @@ const FlowConnectionEdge = ({
14
20
  markerEnd,
15
21
  selected
16
22
  }) => {
23
+ const { t } = useTranslation("ui");
24
+ const [hovered, setHovered] = useState(false);
25
+ const [menuOpen, setMenuOpen] = useState(false);
17
26
  const [path, labelX, labelY] = getSmoothStepPath({
18
27
  sourceX,
19
28
  sourceY,
@@ -23,6 +32,9 @@ const FlowConnectionEdge = ({
23
32
  targetPosition,
24
33
  borderRadius: 12
25
34
  });
35
+ const insertableBlocks = Array.isArray(data?.insertableBlocks) ? data.insertableBlocks : [];
36
+ const canInsert = Boolean(data?.onInsert) && insertableBlocks.length > 0;
37
+ const showInsert = canInsert && (hovered || Boolean(selected) || menuOpen);
26
38
  return /* @__PURE__ */ jsxs(Fragment, { children: [
27
39
  /* @__PURE__ */ jsx(
28
40
  BaseEdge,
@@ -38,12 +50,61 @@ const FlowConnectionEdge = ({
38
50
  })
39
51
  }
40
52
  ),
41
- data?.label && /* @__PURE__ */ jsx(EdgeLabelRenderer, { children: /* @__PURE__ */ jsx(
53
+ canInsert && /* @__PURE__ */ jsx(
54
+ "path",
55
+ {
56
+ d: path,
57
+ fill: "none",
58
+ stroke: "transparent",
59
+ strokeWidth: HOVER_STROKE_WIDTH,
60
+ onMouseEnter: () => setHovered(true),
61
+ onMouseLeave: () => setHovered(false)
62
+ }
63
+ ),
64
+ (data?.label || canInsert) && /* @__PURE__ */ jsx(EdgeLabelRenderer, { children: /* @__PURE__ */ jsxs(
42
65
  "div",
43
66
  {
44
- className: "nodrag nopan pointer-events-none absolute",
67
+ className: classNames(
68
+ "nodrag nopan absolute flex items-center gap-x-1",
69
+ canInsert ? "pointer-events-auto" : "pointer-events-none"
70
+ ),
45
71
  style: { transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)` },
46
- children: /* @__PURE__ */ jsx(OutlinedChip, { label: data.label, tone: "neutral", variant: "outlined", size: "xs", preserveLabelCase: true })
72
+ onMouseEnter: () => setHovered(true),
73
+ onMouseLeave: () => setHovered(false),
74
+ children: [
75
+ data?.label && /* @__PURE__ */ jsx(OutlinedChip, { label: data.label, tone: "neutral", variant: "outlined", size: "xs", preserveLabelCase: true }),
76
+ canInsert && /* @__PURE__ */ jsx(
77
+ "div",
78
+ {
79
+ className: classNames(
80
+ "transition-opacity duration-fast focus-within:opacity-100",
81
+ showInsert ? "opacity-100" : "opacity-0"
82
+ ),
83
+ children: /* @__PURE__ */ jsx(
84
+ ControlledMenu,
85
+ {
86
+ placement: "bottom",
87
+ triggerTooltip: t("flow.edge.insertBlock"),
88
+ onOpenChange: setMenuOpen,
89
+ items: insertableBlocks.map((block) => ({
90
+ key: block.type,
91
+ label: block.label,
92
+ icon: block.icon,
93
+ onClick: () => data?.onInsert?.(block.type)
94
+ })),
95
+ trigger: /* @__PURE__ */ jsx(
96
+ ControlledButton,
97
+ {
98
+ defaultHtmlButton: true,
99
+ className: "interactive flex size-6 items-center justify-center rounded-full border border-dashed border-brand-border bg-surface text-brand-emphasis shadow-sm hover:bg-brand-soft focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
100
+ children: /* @__PURE__ */ jsx(MaterialSymbol, { icon: "add", size: 16, "aria-hidden": true })
101
+ }
102
+ )
103
+ }
104
+ )
105
+ }
106
+ )
107
+ ]
47
108
  }
48
109
  ) })
49
110
  ] });
@@ -1 +1 @@
1
- {"version":3,"file":"FlowConnectionEdge.js","sources":["../../../../src/components/UI/Flow/FlowConnectionEdge.tsx"],"sourcesContent":["import classNames from 'classnames';\n\nimport { BaseEdge, type Edge, EdgeLabelRenderer, type EdgeProps, getSmoothStepPath } from '@xyflow/react';\n\nimport OutlinedChip from '../Chips/OutlinedChip';\n\nimport type { TFlowIssueSeverity } from './flowTypes';\n\nexport interface IFlowConnectionEdgeData extends Record<string, unknown> {\n label?: string;\n traversed?: boolean;\n issueSeverity?: TFlowIssueSeverity;\n}\n\nexport type TFlowConnectionEdge = Edge<IFlowConnectionEdgeData, 'flowConnection'>;\n\nconst FlowConnectionEdge: React.FC<EdgeProps<TFlowConnectionEdge>> = ({\n id,\n sourceX,\n sourceY,\n targetX,\n targetY,\n sourcePosition,\n targetPosition,\n data,\n markerEnd,\n selected,\n}) => {\n const [path, labelX, labelY] = getSmoothStepPath({\n sourceX,\n sourceY,\n targetX,\n targetY,\n sourcePosition,\n targetPosition,\n borderRadius: 12,\n });\n\n return (\n <>\n <BaseEdge\n id={id}\n path={path}\n markerEnd={markerEnd}\n className={classNames('umode-flow-edge-path', {\n 'umode-flow-edge-path--selected': selected,\n 'umode-flow-edge-path--traversed': data?.traversed,\n 'umode-flow-edge-path--error': data?.issueSeverity === 'error',\n 'umode-flow-edge-path--warning': data?.issueSeverity === 'warning',\n })}\n />\n {data?.label && (\n <EdgeLabelRenderer>\n <div\n className=\"nodrag nopan pointer-events-none absolute\"\n style={{ transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)` }}\n >\n <OutlinedChip label={data.label} tone=\"neutral\" variant=\"outlined\" size=\"xs\" preserveLabelCase />\n </div>\n </EdgeLabelRenderer>\n )}\n </>\n );\n};\n\nexport default FlowConnectionEdge;\n"],"names":[],"mappings":";;;;AAgBA,MAAM,qBAA+D,CAAC;AAAA,EACpE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,CAAC,MAAM,QAAQ,MAAM,IAAI,kBAAkB;AAAA,IAC/C;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,cAAc;AAAA,EAAA,CACf;AAED,SACE,qBAAA,UAAA,EACE,UAAA;AAAA,IAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,QACA,WAAW,WAAW,wBAAwB;AAAA,UAC5C,kCAAkC;AAAA,UAClC,mCAAmC,MAAM;AAAA,UACzC,+BAA+B,MAAM,kBAAkB;AAAA,UACvD,iCAAiC,MAAM,kBAAkB;AAAA,QAAA,CAC1D;AAAA,MAAA;AAAA,IAAA;AAAA,IAEF,MAAM,SACL,oBAAC,mBAAA,EACC,UAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,WAAU;AAAA,QACV,OAAO,EAAE,WAAW,mCAAmC,MAAM,OAAO,MAAM,MAAA;AAAA,QAE1E,UAAA,oBAAC,cAAA,EAAa,OAAO,KAAK,OAAO,MAAK,WAAU,SAAQ,YAAW,MAAK,MAAK,mBAAiB,KAAA,CAAC;AAAA,MAAA;AAAA,IAAA,EACjG,CACF;AAAA,EAAA,GAEJ;AAEJ;"}
1
+ {"version":3,"file":"FlowConnectionEdge.js","sources":["../../../../src/components/UI/Flow/FlowConnectionEdge.tsx"],"sourcesContent":["import { useState } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledMenu from '@/components/UI/ControlledMenu';\nimport { MaterialSymbol } from '@/components/UI/MaterialSymbol';\nimport { BaseEdge, type Edge, EdgeLabelRenderer, type EdgeProps, getSmoothStepPath } from '@xyflow/react';\n\nimport OutlinedChip from '../Chips/OutlinedChip';\n\nimport type { IFlowAddableBlock } from './FlowBlockNode';\nimport type { TFlowIssueSeverity } from './flowTypes';\n\nexport interface IFlowConnectionEdgeData extends Record<string, unknown> {\n label?: string;\n traversed?: boolean;\n issueSeverity?: TFlowIssueSeverity;\n /** What can go in the middle of this edge; empty in a read-only canvas. */\n insertableBlocks?: Array<IFlowAddableBlock>;\n onInsert?: (type: string) => void;\n}\n\nexport type TFlowConnectionEdge = Edge<IFlowConnectionEdgeData, 'flowConnection'>;\n\n/** Wide enough to hover a 1px line without hunting for it. */\nconst HOVER_STROKE_WIDTH = 24;\n\n/**\n * A smooth-step connection. Hovering it (or selecting it) shows a \"+\" at its middle that puts a\n * block between the two it connects, so a step can be added without rewiring by hand.\n */\nconst FlowConnectionEdge: React.FC<EdgeProps<TFlowConnectionEdge>> = ({\n id,\n sourceX,\n sourceY,\n targetX,\n targetY,\n sourcePosition,\n targetPosition,\n data,\n markerEnd,\n selected,\n}) => {\n const { t } = useTranslation('ui');\n const [hovered, setHovered] = useState(false);\n const [menuOpen, setMenuOpen] = useState(false);\n const [path, labelX, labelY] = getSmoothStepPath({\n sourceX,\n sourceY,\n targetX,\n targetY,\n sourcePosition,\n targetPosition,\n borderRadius: 12,\n });\n\n const insertableBlocks = Array.isArray(data?.insertableBlocks) ? data.insertableBlocks : [];\n const canInsert = Boolean(data?.onInsert) && insertableBlocks.length > 0;\n const showInsert = canInsert && (hovered || Boolean(selected) || menuOpen);\n\n return (\n <>\n <BaseEdge\n id={id}\n path={path}\n markerEnd={markerEnd}\n className={classNames('umode-flow-edge-path', {\n 'umode-flow-edge-path--selected': selected,\n 'umode-flow-edge-path--traversed': data?.traversed,\n 'umode-flow-edge-path--error': data?.issueSeverity === 'error',\n 'umode-flow-edge-path--warning': data?.issueSeverity === 'warning',\n })}\n />\n {canInsert && (\n <path\n d={path}\n fill=\"none\"\n stroke=\"transparent\"\n strokeWidth={HOVER_STROKE_WIDTH}\n onMouseEnter={() => setHovered(true)}\n onMouseLeave={() => setHovered(false)}\n />\n )}\n {(data?.label || canInsert) && (\n <EdgeLabelRenderer>\n <div\n className={classNames(\n 'nodrag nopan absolute flex items-center gap-x-1',\n canInsert ? 'pointer-events-auto' : 'pointer-events-none',\n )}\n style={{ transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)` }}\n onMouseEnter={() => setHovered(true)}\n onMouseLeave={() => setHovered(false)}\n >\n {data?.label && (\n <OutlinedChip label={data.label} tone=\"neutral\" variant=\"outlined\" size=\"xs\" preserveLabelCase />\n )}\n {canInsert && (\n <div\n className={classNames(\n 'transition-opacity duration-fast focus-within:opacity-100',\n showInsert ? 'opacity-100' : 'opacity-0',\n )}\n >\n <ControlledMenu\n placement=\"bottom\"\n triggerTooltip={t('flow.edge.insertBlock')}\n onOpenChange={setMenuOpen}\n items={insertableBlocks.map((block) => ({\n key: block.type,\n label: block.label,\n icon: block.icon,\n onClick: () => data?.onInsert?.(block.type),\n }))}\n trigger={\n <ControlledButton\n defaultHtmlButton\n className=\"interactive flex size-6 items-center justify-center rounded-full border border-dashed border-brand-border bg-surface text-brand-emphasis shadow-sm hover:bg-brand-soft focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <MaterialSymbol icon=\"add\" size={16} aria-hidden />\n </ControlledButton>\n }\n />\n </div>\n )}\n </div>\n </EdgeLabelRenderer>\n )}\n </>\n );\n};\n\nexport default FlowConnectionEdge;\n"],"names":[],"mappings":";;;;;;;;;AA0BA,MAAM,qBAAqB;AAM3B,MAAM,qBAA+D,CAAC;AAAA,EACpE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,EAAE,EAAA,IAAM,eAAe,IAAI;AACjC,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAC5C,QAAM,CAAC,UAAU,WAAW,IAAI,SAAS,KAAK;AAC9C,QAAM,CAAC,MAAM,QAAQ,MAAM,IAAI,kBAAkB;AAAA,IAC/C;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,cAAc;AAAA,EAAA,CACf;AAED,QAAM,mBAAmB,MAAM,QAAQ,MAAM,gBAAgB,IAAI,KAAK,mBAAmB,CAAA;AACzF,QAAM,YAAY,QAAQ,MAAM,QAAQ,KAAK,iBAAiB,SAAS;AACvE,QAAM,aAAa,cAAc,WAAW,QAAQ,QAAQ,KAAK;AAEjE,SACE,qBAAA,UAAA,EACE,UAAA;AAAA,IAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,QACA,WAAW,WAAW,wBAAwB;AAAA,UAC5C,kCAAkC;AAAA,UAClC,mCAAmC,MAAM;AAAA,UACzC,+BAA+B,MAAM,kBAAkB;AAAA,UACvD,iCAAiC,MAAM,kBAAkB;AAAA,QAAA,CAC1D;AAAA,MAAA;AAAA,IAAA;AAAA,IAEF,aACC;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,GAAG;AAAA,QACH,MAAK;AAAA,QACL,QAAO;AAAA,QACP,aAAa;AAAA,QACb,cAAc,MAAM,WAAW,IAAI;AAAA,QACnC,cAAc,MAAM,WAAW,KAAK;AAAA,MAAA;AAAA,IAAA;AAAA,KAGtC,MAAM,SAAS,cACf,oBAAC,mBAAA,EACC,UAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,WAAW;AAAA,UACT;AAAA,UACA,YAAY,wBAAwB;AAAA,QAAA;AAAA,QAEtC,OAAO,EAAE,WAAW,mCAAmC,MAAM,OAAO,MAAM,MAAA;AAAA,QAC1E,cAAc,MAAM,WAAW,IAAI;AAAA,QACnC,cAAc,MAAM,WAAW,KAAK;AAAA,QAEnC,UAAA;AAAA,UAAA,MAAM,SACL,oBAAC,cAAA,EAAa,OAAO,KAAK,OAAO,MAAK,WAAU,SAAQ,YAAW,MAAK,MAAK,mBAAiB,MAAC;AAAA,UAEhG,aACC;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,WAAW;AAAA,gBACT;AAAA,gBACA,aAAa,gBAAgB;AAAA,cAAA;AAAA,cAG/B,UAAA;AAAA,gBAAC;AAAA,gBAAA;AAAA,kBACC,WAAU;AAAA,kBACV,gBAAgB,EAAE,uBAAuB;AAAA,kBACzC,cAAc;AAAA,kBACd,OAAO,iBAAiB,IAAI,CAAC,WAAW;AAAA,oBACtC,KAAK,MAAM;AAAA,oBACX,OAAO,MAAM;AAAA,oBACb,MAAM,MAAM;AAAA,oBACZ,SAAS,MAAM,MAAM,WAAW,MAAM,IAAI;AAAA,kBAAA,EAC1C;AAAA,kBACF,SACE;AAAA,oBAAC;AAAA,oBAAA;AAAA,sBACC,mBAAiB;AAAA,sBACjB,WAAU;AAAA,sBAEV,8BAAC,gBAAA,EAAe,MAAK,OAAM,MAAM,IAAI,eAAW,KAAA,CAAC;AAAA,oBAAA;AAAA,kBAAA;AAAA,gBACnD;AAAA,cAAA;AAAA,YAEJ;AAAA,UAAA;AAAA,QACF;AAAA,MAAA;AAAA,IAAA,EAEJ,CACF;AAAA,EAAA,GAEJ;AAEJ;"}
@@ -2,7 +2,7 @@ import { IFlowIssue } from './flowTypes';
2
2
  interface IFlowInspectorProps {
3
3
  issues?: Array<IFlowIssue>;
4
4
  readOnly?: boolean;
5
- /** A floating panel opens with the selection instead of showing an empty state. */
5
+ /** Renders nothing while no block is selected, instead of the empty state. */
6
6
  hideWhenEmpty?: boolean;
7
7
  className?: string;
8
8
  }
@@ -1 +1 @@
1
- {"version":3,"file":"FlowInspector.js","sources":["../../../../src/components/UI/Flow/FlowInspector.tsx"],"sourcesContent":["import { useTranslation } from 'react-i18next';\nimport { twMerge } from 'tailwind-merge';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledInput from '@/components/Form/ControlledInput';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { MaterialSymbol } from '@/components/UI/MaterialSymbol';\nimport NoDataFound from '@/components/UI/NoDataFound';\n\nimport { useFlowBlockRegistry, useFlowEditor, useFlowEditorStore } from './FlowEditorProvider';\nimport type { IFlowIssue } from './flowTypes';\nimport { useFlowLabels } from './useFlowLabels';\n\ninterface IFlowInspectorProps {\n issues?: Array<IFlowIssue>;\n readOnly?: boolean;\n /** A floating panel opens with the selection instead of showing an empty state. */\n hideWhenEmpty?: boolean;\n className?: string;\n}\n\n/** The side panel of the selected block: its name, its own settings panel and its issues. */\nconst FlowInspector: React.FC<IFlowInspectorProps> = ({\n issues = [],\n readOnly = false,\n hideWhenEmpty = false,\n className,\n}) => {\n const { t } = useTranslation('ui');\n const labels = useFlowLabels();\n const registry = useFlowBlockRegistry();\n const store = useFlowEditorStore();\n const selectedNodeIds = useFlowEditor((state) => state.selectedNodeIds);\n const selectedNode = useFlowEditor((state) =>\n state.selectedNodeIds.length === 1\n ? state.graph.nodes.find((node) => node.nodeId === state.selectedNodeIds[0])\n : undefined,\n );\n\n const shellClass = twMerge(\n 'flex h-full min-h-0 flex-col gap-y-3 rounded-xl border border-border bg-surface p-4',\n className,\n );\n\n if (!selectedNode || selectedNodeIds.length !== 1) {\n if (hideWhenEmpty) return null;\n return (\n <aside className={shellClass} aria-label={t('flow.inspector.title')}>\n <NoDataFound size=\"sm\" variant=\"waiting\" message={t('flow.inspector.empty')} />\n </aside>\n );\n }\n\n const definition = registry.get(selectedNode.type);\n const nodeIssues = (Array.isArray(issues) ? issues : []).filter((issue) => issue.nodeId === selectedNode.nodeId);\n const ConfigPanel = definition?.ConfigPanel;\n\n return (\n <aside className={shellClass} aria-label={t('flow.inspector.title')}>\n <div className=\"flex items-center gap-x-2\">\n <span className=\"flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground-muted\">\n <MaterialSymbol\n icon={definition?.resolveIcon?.(selectedNode.config) ?? definition?.icon ?? 'help'}\n size={18}\n aria-hidden\n />\n </span>\n <div className=\"flex min-w-0 flex-col\">\n {definition && (\n <span className=\"text-xxs font-semibold uppercase tracking-wide text-foreground-subtle\">\n {labels.categories[definition.category]}\n </span>\n )}\n <span className=\"truncate text-sm font-semibold text-foreground\">\n {definition?.resolveLabel?.(selectedNode.config) ?? definition?.label ?? selectedNode.type}\n </span>\n </div>\n <ControlledTooltip content={t('flow.inspector.close')} placement=\"left\">\n <ControlledButton\n defaultHtmlButton\n onClick={() => store.getState().setSelection({ nodeIds: [], edgeIds: [] })}\n className=\"interactive ml-auto flex size-8 shrink-0 items-center justify-center rounded-lg text-foreground-muted hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <MaterialSymbol icon=\"close\" size={18} aria-hidden />\n </ControlledButton>\n </ControlledTooltip>\n </div>\n\n <ControlledInput\n label={t('flow.inspector.label')}\n value={selectedNode.label ?? ''}\n disabled={readOnly}\n onChange={(event) => store.getState().updateNodeLabel(selectedNode.nodeId, event.target.value)}\n />\n\n <div className=\"flex min-h-0 flex-1 flex-col gap-y-3 overflow-y-auto\">\n {ConfigPanel ? (\n <ConfigPanel\n config={selectedNode.config}\n readOnly={readOnly}\n onChange={(next) => store.getState().updateNodeConfig(selectedNode.nodeId, next)}\n />\n ) : (\n <NoDataFound compact variant=\"error\" message={t('flow.inspector.unknownBlock')} />\n )}\n\n {nodeIssues.length > 0 && (\n <ul className=\"flex flex-col gap-y-1\">\n {nodeIssues.map((issue) => (\n <li key={`${issue.code}|${issue.edgeId ?? ''}`} className=\"flex items-start gap-x-2\">\n <MaterialSymbol\n icon={issue.severity === 'error' ? 'error' : 'warning'}\n size={16}\n className={issue.severity === 'error' ? 'text-danger-emphasis' : 'text-warning-emphasis'}\n aria-hidden\n />\n <span className=\"text-xs text-foreground-muted\">{labels.issueMessage(issue)}</span>\n </li>\n ))}\n </ul>\n )}\n </div>\n\n {!readOnly && (\n <div className=\"flex gap-x-2\">\n {definition?.category !== 'trigger' && (\n <ControlledButton\n size=\"sm\"\n variant=\"outlined\"\n tone=\"neutral\"\n onClick={() => store.getState().duplicateSelected()}\n >\n <MaterialSymbol icon=\"content_copy\" size={16} aria-hidden />\n {t('flow.inspector.duplicate')}\n </ControlledButton>\n )}\n <ControlledButton\n size=\"sm\"\n variant=\"outlined\"\n tone=\"danger\"\n onClick={() => store.getState().removeSelected()}\n >\n <MaterialSymbol icon=\"delete\" size={16} aria-hidden />\n {t('flow.inspector.delete')}\n </ControlledButton>\n </div>\n )}\n </aside>\n );\n};\n\nexport default FlowInspector;\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"FlowInspector.js","sources":["../../../../src/components/UI/Flow/FlowInspector.tsx"],"sourcesContent":["import { useTranslation } from 'react-i18next';\nimport { twMerge } from 'tailwind-merge';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledInput from '@/components/Form/ControlledInput';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { MaterialSymbol } from '@/components/UI/MaterialSymbol';\nimport NoDataFound from '@/components/UI/NoDataFound';\n\nimport { useFlowBlockRegistry, useFlowEditor, useFlowEditorStore } from './FlowEditorProvider';\nimport type { IFlowIssue } from './flowTypes';\nimport { useFlowLabels } from './useFlowLabels';\n\ninterface IFlowInspectorProps {\n issues?: Array<IFlowIssue>;\n readOnly?: boolean;\n /** Renders nothing while no block is selected, instead of the empty state. */\n hideWhenEmpty?: boolean;\n className?: string;\n}\n\n/** The side panel of the selected block: its name, its own settings panel and its issues. */\nconst FlowInspector: React.FC<IFlowInspectorProps> = ({\n issues = [],\n readOnly = false,\n hideWhenEmpty = false,\n className,\n}) => {\n const { t } = useTranslation('ui');\n const labels = useFlowLabels();\n const registry = useFlowBlockRegistry();\n const store = useFlowEditorStore();\n const selectedNodeIds = useFlowEditor((state) => state.selectedNodeIds);\n const selectedNode = useFlowEditor((state) =>\n state.selectedNodeIds.length === 1\n ? state.graph.nodes.find((node) => node.nodeId === state.selectedNodeIds[0])\n : undefined,\n );\n\n const shellClass = twMerge(\n 'flex h-full min-h-0 flex-col gap-y-3 rounded-xl border border-border bg-surface p-4',\n className,\n );\n\n if (!selectedNode || selectedNodeIds.length !== 1) {\n if (hideWhenEmpty) return null;\n return (\n <aside className={shellClass} aria-label={t('flow.inspector.title')}>\n <NoDataFound size=\"sm\" variant=\"waiting\" message={t('flow.inspector.empty')} />\n </aside>\n );\n }\n\n const definition = registry.get(selectedNode.type);\n const nodeIssues = (Array.isArray(issues) ? issues : []).filter((issue) => issue.nodeId === selectedNode.nodeId);\n const ConfigPanel = definition?.ConfigPanel;\n\n return (\n <aside className={shellClass} aria-label={t('flow.inspector.title')}>\n <div className=\"flex items-center gap-x-2\">\n <span className=\"flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground-muted\">\n <MaterialSymbol\n icon={definition?.resolveIcon?.(selectedNode.config) ?? definition?.icon ?? 'help'}\n size={18}\n aria-hidden\n />\n </span>\n <div className=\"flex min-w-0 flex-col\">\n {definition && (\n <span className=\"text-xxs font-semibold uppercase tracking-wide text-foreground-subtle\">\n {labels.categories[definition.category]}\n </span>\n )}\n <span className=\"truncate text-sm font-semibold text-foreground\">\n {definition?.resolveLabel?.(selectedNode.config) ?? definition?.label ?? selectedNode.type}\n </span>\n </div>\n <ControlledTooltip content={t('flow.inspector.close')} placement=\"left\">\n <ControlledButton\n defaultHtmlButton\n onClick={() => store.getState().setSelection({ nodeIds: [], edgeIds: [] })}\n className=\"interactive ml-auto flex size-8 shrink-0 items-center justify-center rounded-lg text-foreground-muted hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <MaterialSymbol icon=\"close\" size={18} aria-hidden />\n </ControlledButton>\n </ControlledTooltip>\n </div>\n\n <ControlledInput\n label={t('flow.inspector.label')}\n value={selectedNode.label ?? ''}\n disabled={readOnly}\n onChange={(event) => store.getState().updateNodeLabel(selectedNode.nodeId, event.target.value)}\n />\n\n <div className=\"flex min-h-0 flex-1 flex-col gap-y-3 overflow-y-auto\">\n {ConfigPanel ? (\n <ConfigPanel\n config={selectedNode.config}\n readOnly={readOnly}\n onChange={(next) => store.getState().updateNodeConfig(selectedNode.nodeId, next)}\n />\n ) : (\n <NoDataFound compact variant=\"error\" message={t('flow.inspector.unknownBlock')} />\n )}\n\n {nodeIssues.length > 0 && (\n <ul className=\"flex flex-col gap-y-1\">\n {nodeIssues.map((issue) => (\n <li key={`${issue.code}|${issue.edgeId ?? ''}`} className=\"flex items-start gap-x-2\">\n <MaterialSymbol\n icon={issue.severity === 'error' ? 'error' : 'warning'}\n size={16}\n className={issue.severity === 'error' ? 'text-danger-emphasis' : 'text-warning-emphasis'}\n aria-hidden\n />\n <span className=\"text-xs text-foreground-muted\">{labels.issueMessage(issue)}</span>\n </li>\n ))}\n </ul>\n )}\n </div>\n\n {!readOnly && (\n <div className=\"flex gap-x-2\">\n {definition?.category !== 'trigger' && (\n <ControlledButton\n size=\"sm\"\n variant=\"outlined\"\n tone=\"neutral\"\n onClick={() => store.getState().duplicateSelected()}\n >\n <MaterialSymbol icon=\"content_copy\" size={16} aria-hidden />\n {t('flow.inspector.duplicate')}\n </ControlledButton>\n )}\n <ControlledButton\n size=\"sm\"\n variant=\"outlined\"\n tone=\"danger\"\n onClick={() => store.getState().removeSelected()}\n >\n <MaterialSymbol icon=\"delete\" size={16} aria-hidden />\n {t('flow.inspector.delete')}\n </ControlledButton>\n </div>\n )}\n </aside>\n );\n};\n\nexport default FlowInspector;\n"],"names":[],"mappings":"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"}
@@ -125,7 +125,7 @@ const ArcEdge = ({
125
125
  ) })
126
126
  ] });
127
127
  };
128
- const ATTRIBUTION_POSITION = "bottom-left";
128
+ const PRO_OPTIONS = { hideAttribution: true };
129
129
  const NODE_TYPES = { flowLane: LaneNode };
130
130
  const EDGE_TYPES = { flowArc: ArcEdge };
131
131
  const FlowLaneMapContent = ({
@@ -189,7 +189,7 @@ const FlowLaneMapContent = ({
189
189
  maxZoom: 1.5,
190
190
  fitView: true,
191
191
  fitViewOptions: { padding: 0.15, minZoom: 0.75 },
192
- attributionPosition: ATTRIBUTION_POSITION,
192
+ proOptions: PRO_OPTIONS,
193
193
  children: [
194
194
  /* @__PURE__ */ jsx(Background, { variant: BackgroundVariant.Dots, gap: 16, size: 1 }),
195
195
  /* @__PURE__ */ jsx(FlowZoomControls, {})
@@ -1 +1 @@
1
- {"version":3,"file":"FlowLaneMap.js","sources":["../../../../src/components/UI/Flow/FlowLaneMap.tsx"],"sourcesContent":["import { ReactNode, useMemo } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\nimport { twMerge } from 'tailwind-merge';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport {\n Background,\n BackgroundVariant,\n BaseEdge,\n type Edge,\n EdgeLabelRenderer,\n type EdgeProps,\n Handle,\n MarkerType,\n type Node,\n type NodeProps,\n type PanelPosition,\n Position,\n ReactFlow,\n ReactFlowProvider,\n} from '@xyflow/react';\n\nimport OutlinedChip from '../Chips/OutlinedChip';\n\nimport FlowZoomControls from './FlowZoomControls';\n\nimport '@xyflow/react/dist/base.css';\nimport '../../../styles/flow.css';\n\nexport interface IFlowLane {\n laneId: string;\n title: string;\n subtitle?: string;\n /** Counters or chips under the title (rules acting on entry, stage requirements). */\n badges?: ReactNode;\n}\n\nexport type TFlowArcTone = 'default' | 'warning' | 'danger';\n\nexport interface IFlowArc {\n arcId: string;\n sourceLaneId: string;\n targetLaneId: string;\n label: string;\n /** `over` arcs above the lanes, `under` below them; the host decides what each side means. */\n side: 'over' | 'under';\n tone?: TFlowArcTone;\n dashed?: boolean;\n}\n\ninterface IFlowLaneMapProps {\n lanes: Array<IFlowLane>;\n arcs: Array<IFlowArc>;\n onArcClick?: (arcId: string) => void;\n onLaneClick?: (laneId: string) => void;\n selectedArcId?: string | null;\n ariaLabel?: string;\n className?: string;\n}\n\nconst LANE_WIDTH = 192;\nconst LANE_HEIGHT = 168;\nconst LANE_GAP = 80;\n\ninterface ILaneNodeData extends Record<string, unknown> {\n lane: IFlowLane;\n onLaneClick?: (laneId: string) => void;\n}\n\ninterface IArcEdgeData extends Record<string, unknown> {\n arc: IFlowArc;\n}\n\ntype TLaneNode = Node<ILaneNodeData, 'flowLane'>;\ntype TArcEdge = Edge<IArcEdgeData, 'flowArc'>;\n\nconst HIDDEN_HANDLE_CLASS = '!size-px !min-h-0 !min-w-0 !border-0 !bg-transparent';\n\nconst LaneNode: React.FC<NodeProps<TLaneNode>> = ({ data }) => {\n const { lane, onLaneClick } = data;\n const content = (\n <>\n <span className=\"truncate text-sm font-semibold text-foreground\">{lane.title}</span>\n {lane.subtitle && <span className=\"truncate text-xs text-foreground-subtle\">{lane.subtitle}</span>}\n {lane.badges && <div className=\"mt-auto flex flex-wrap gap-1\">{lane.badges}</div>}\n </>\n );\n\n return (\n <div style={{ width: LANE_WIDTH, height: LANE_HEIGHT }} className=\"relative\">\n <Handle\n id=\"over-out\"\n type=\"source\"\n position={Position.Top}\n isConnectable={false}\n className={HIDDEN_HANDLE_CLASS}\n />\n <Handle\n id=\"over-in\"\n type=\"target\"\n position={Position.Top}\n isConnectable={false}\n className={HIDDEN_HANDLE_CLASS}\n />\n {onLaneClick ? (\n <ControlledButton\n defaultHtmlButton\n onClick={() => onLaneClick(lane.laneId)}\n className=\"interactive flex size-full flex-col gap-y-1 rounded-xl border border-border bg-surface p-3 text-left shadow-sm hover:border-border-emphasis focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n {content}\n </ControlledButton>\n ) : (\n <div className=\"flex size-full flex-col gap-y-1 rounded-xl border border-border bg-surface p-3 shadow-sm\">\n {content}\n </div>\n )}\n <Handle\n id=\"under-out\"\n type=\"source\"\n position={Position.Bottom}\n isConnectable={false}\n className={HIDDEN_HANDLE_CLASS}\n />\n <Handle\n id=\"under-in\"\n type=\"target\"\n position={Position.Bottom}\n isConnectable={false}\n className={HIDDEN_HANDLE_CLASS}\n />\n </div>\n );\n};\n\n/** A curve between two lane edges whose height grows with the distance, so long arcs clear the short ones. */\nconst ArcEdge: React.FC<EdgeProps<TArcEdge>> = ({\n id,\n sourceX,\n sourceY,\n targetX,\n targetY,\n data,\n markerEnd,\n selected,\n}) => {\n const arc = data?.arc;\n const isOver = arc?.side !== 'under';\n const isBackward = targetX < sourceX;\n const lift = 40 + Math.abs(targetX - sourceX) * 0.2 + (isBackward ? 32 : 0);\n const controlY = isOver ? Math.min(sourceY, targetY) - lift * 1.3 : Math.max(sourceY, targetY) + lift * 1.3;\n const path = `M ${sourceX},${sourceY} C ${sourceX},${controlY} ${targetX},${controlY} ${targetX},${targetY}`;\n const labelX = (sourceX + targetX) / 2;\n const labelY = 0.125 * (sourceY + targetY) + 0.75 * controlY;\n\n return (\n <>\n <BaseEdge\n id={id}\n path={path}\n markerEnd={markerEnd}\n interactionWidth={16}\n className={classNames('umode-flow-arc', {\n 'umode-flow-arc--warning': arc?.tone === 'warning',\n 'umode-flow-arc--danger': arc?.tone === 'danger',\n 'umode-flow-arc--dashed': arc?.dashed,\n 'umode-flow-arc--selected': selected,\n })}\n />\n {arc?.label && (\n <EdgeLabelRenderer>\n <div\n className=\"nodrag nopan pointer-events-none absolute whitespace-nowrap\"\n style={{ transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)` }}\n >\n <OutlinedChip\n label={arc.label}\n tone={arc.tone === 'warning' ? 'warning' : arc.tone === 'danger' ? 'danger' : 'neutral'}\n variant=\"outlined\"\n size=\"xs\"\n preserveLabelCase\n />\n </div>\n </EdgeLabelRenderer>\n )}\n </>\n );\n};\n\nconst ATTRIBUTION_POSITION: PanelPosition = 'bottom-left';\nconst NODE_TYPES = { flowLane: LaneNode };\nconst EDGE_TYPES = { flowArc: ArcEdge };\n\n/**\n * A read-only map of fixed columns joined by arcs: the shape of a pipeline seen from\n * above. Lanes keep the order they are given, so no graph layout is needed.\n */\nconst FlowLaneMapContent: React.FC<IFlowLaneMapProps> = ({\n lanes,\n arcs,\n onArcClick,\n onLaneClick,\n selectedArcId,\n ariaLabel,\n className,\n}) => {\n const { t } = useTranslation('ui');\n const safeLanes = useMemo(() => (Array.isArray(lanes) ? lanes : []), [lanes]);\n const safeArcs = useMemo(() => (Array.isArray(arcs) ? arcs : []), [arcs]);\n\n const nodes = useMemo<Array<TLaneNode>>(\n () =>\n safeLanes.map((lane, index) => ({\n id: lane.laneId,\n type: 'flowLane',\n position: { x: index * (LANE_WIDTH + LANE_GAP), y: 0 },\n draggable: false,\n selectable: false,\n data: { lane, onLaneClick },\n })),\n [safeLanes, onLaneClick],\n );\n\n const laneIds = useMemo(() => new Set(safeLanes.map((lane) => lane.laneId)), [safeLanes]);\n\n const edges = useMemo<Array<TArcEdge>>(\n () =>\n safeArcs\n .filter(\n (arc) =>\n laneIds.has(arc.sourceLaneId) && laneIds.has(arc.targetLaneId) && arc.sourceLaneId !== arc.targetLaneId,\n )\n .map((arc) => ({\n id: arc.arcId,\n type: 'flowArc',\n source: arc.sourceLaneId,\n sourceHandle: `${arc.side}-out`,\n target: arc.targetLaneId,\n targetHandle: `${arc.side}-in`,\n selected: arc.arcId === selectedArcId,\n markerEnd: { type: MarkerType.ArrowClosed, width: 14, height: 14 },\n data: { arc },\n })),\n [safeArcs, laneIds, selectedArcId],\n );\n\n return (\n <div\n role=\"region\"\n aria-label={ariaLabel ?? t('flow.laneMap.label')}\n className={twMerge('umode-flow relative h-full min-h-[24rem] w-full rounded-xl bg-canvas', className)}\n >\n <ReactFlow<TLaneNode, TArcEdge>\n nodes={nodes}\n edges={edges}\n nodeTypes={NODE_TYPES}\n edgeTypes={EDGE_TYPES}\n nodesDraggable={false}\n nodesConnectable={false}\n deleteKeyCode={null}\n onEdgeClick={(_event, edge) => onArcClick?.(edge.id)}\n minZoom={0.25}\n maxZoom={1.5}\n fitView\n fitViewOptions={{ padding: 0.15, minZoom: 0.75 }}\n attributionPosition={ATTRIBUTION_POSITION}\n >\n <Background variant={BackgroundVariant.Dots} gap={16} size={1} />\n <FlowZoomControls />\n </ReactFlow>\n </div>\n );\n};\n\nconst FlowLaneMap: React.FC<IFlowLaneMapProps> = (props) => (\n <ReactFlowProvider>\n <FlowLaneMapContent {...props} />\n </ReactFlowProvider>\n);\n\nexport default FlowLaneMap;\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"FlowLaneMap.js","sources":["../../../../src/components/UI/Flow/FlowLaneMap.tsx"],"sourcesContent":["import { ReactNode, useMemo } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\nimport { twMerge } from 'tailwind-merge';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport {\n Background,\n BackgroundVariant,\n BaseEdge,\n type Edge,\n EdgeLabelRenderer,\n type EdgeProps,\n Handle,\n MarkerType,\n type Node,\n type NodeProps,\n Position,\n type ProOptions,\n ReactFlow,\n ReactFlowProvider,\n} from '@xyflow/react';\n\nimport OutlinedChip from '../Chips/OutlinedChip';\n\nimport FlowZoomControls from './FlowZoomControls';\n\nimport '@xyflow/react/dist/base.css';\nimport '../../../styles/flow.css';\n\nexport interface IFlowLane {\n laneId: string;\n title: string;\n subtitle?: string;\n /** Counters or chips under the title (rules acting on entry, stage requirements). */\n badges?: ReactNode;\n}\n\nexport type TFlowArcTone = 'default' | 'warning' | 'danger';\n\nexport interface IFlowArc {\n arcId: string;\n sourceLaneId: string;\n targetLaneId: string;\n label: string;\n /** `over` arcs above the lanes, `under` below them; the host decides what each side means. */\n side: 'over' | 'under';\n tone?: TFlowArcTone;\n dashed?: boolean;\n}\n\ninterface IFlowLaneMapProps {\n lanes: Array<IFlowLane>;\n arcs: Array<IFlowArc>;\n onArcClick?: (arcId: string) => void;\n onLaneClick?: (laneId: string) => void;\n selectedArcId?: string | null;\n ariaLabel?: string;\n className?: string;\n}\n\nconst LANE_WIDTH = 192;\nconst LANE_HEIGHT = 168;\nconst LANE_GAP = 80;\n\ninterface ILaneNodeData extends Record<string, unknown> {\n lane: IFlowLane;\n onLaneClick?: (laneId: string) => void;\n}\n\ninterface IArcEdgeData extends Record<string, unknown> {\n arc: IFlowArc;\n}\n\ntype TLaneNode = Node<ILaneNodeData, 'flowLane'>;\ntype TArcEdge = Edge<IArcEdgeData, 'flowArc'>;\n\nconst HIDDEN_HANDLE_CLASS = '!size-px !min-h-0 !min-w-0 !border-0 !bg-transparent';\n\nconst LaneNode: React.FC<NodeProps<TLaneNode>> = ({ data }) => {\n const { lane, onLaneClick } = data;\n const content = (\n <>\n <span className=\"truncate text-sm font-semibold text-foreground\">{lane.title}</span>\n {lane.subtitle && <span className=\"truncate text-xs text-foreground-subtle\">{lane.subtitle}</span>}\n {lane.badges && <div className=\"mt-auto flex flex-wrap gap-1\">{lane.badges}</div>}\n </>\n );\n\n return (\n <div style={{ width: LANE_WIDTH, height: LANE_HEIGHT }} className=\"relative\">\n <Handle\n id=\"over-out\"\n type=\"source\"\n position={Position.Top}\n isConnectable={false}\n className={HIDDEN_HANDLE_CLASS}\n />\n <Handle\n id=\"over-in\"\n type=\"target\"\n position={Position.Top}\n isConnectable={false}\n className={HIDDEN_HANDLE_CLASS}\n />\n {onLaneClick ? (\n <ControlledButton\n defaultHtmlButton\n onClick={() => onLaneClick(lane.laneId)}\n className=\"interactive flex size-full flex-col gap-y-1 rounded-xl border border-border bg-surface p-3 text-left shadow-sm hover:border-border-emphasis focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n {content}\n </ControlledButton>\n ) : (\n <div className=\"flex size-full flex-col gap-y-1 rounded-xl border border-border bg-surface p-3 shadow-sm\">\n {content}\n </div>\n )}\n <Handle\n id=\"under-out\"\n type=\"source\"\n position={Position.Bottom}\n isConnectable={false}\n className={HIDDEN_HANDLE_CLASS}\n />\n <Handle\n id=\"under-in\"\n type=\"target\"\n position={Position.Bottom}\n isConnectable={false}\n className={HIDDEN_HANDLE_CLASS}\n />\n </div>\n );\n};\n\n/** A curve between two lane edges whose height grows with the distance, so long arcs clear the short ones. */\nconst ArcEdge: React.FC<EdgeProps<TArcEdge>> = ({\n id,\n sourceX,\n sourceY,\n targetX,\n targetY,\n data,\n markerEnd,\n selected,\n}) => {\n const arc = data?.arc;\n const isOver = arc?.side !== 'under';\n const isBackward = targetX < sourceX;\n const lift = 40 + Math.abs(targetX - sourceX) * 0.2 + (isBackward ? 32 : 0);\n const controlY = isOver ? Math.min(sourceY, targetY) - lift * 1.3 : Math.max(sourceY, targetY) + lift * 1.3;\n const path = `M ${sourceX},${sourceY} C ${sourceX},${controlY} ${targetX},${controlY} ${targetX},${targetY}`;\n const labelX = (sourceX + targetX) / 2;\n const labelY = 0.125 * (sourceY + targetY) + 0.75 * controlY;\n\n return (\n <>\n <BaseEdge\n id={id}\n path={path}\n markerEnd={markerEnd}\n interactionWidth={16}\n className={classNames('umode-flow-arc', {\n 'umode-flow-arc--warning': arc?.tone === 'warning',\n 'umode-flow-arc--danger': arc?.tone === 'danger',\n 'umode-flow-arc--dashed': arc?.dashed,\n 'umode-flow-arc--selected': selected,\n })}\n />\n {arc?.label && (\n <EdgeLabelRenderer>\n <div\n className=\"nodrag nopan pointer-events-none absolute whitespace-nowrap\"\n style={{ transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)` }}\n >\n <OutlinedChip\n label={arc.label}\n tone={arc.tone === 'warning' ? 'warning' : arc.tone === 'danger' ? 'danger' : 'neutral'}\n variant=\"outlined\"\n size=\"xs\"\n preserveLabelCase\n />\n </div>\n </EdgeLabelRenderer>\n )}\n </>\n );\n};\n\nconst PRO_OPTIONS: ProOptions = { hideAttribution: true };\nconst NODE_TYPES = { flowLane: LaneNode };\nconst EDGE_TYPES = { flowArc: ArcEdge };\n\n/**\n * A read-only map of fixed columns joined by arcs: the shape of a pipeline seen from\n * above. Lanes keep the order they are given, so no graph layout is needed.\n */\nconst FlowLaneMapContent: React.FC<IFlowLaneMapProps> = ({\n lanes,\n arcs,\n onArcClick,\n onLaneClick,\n selectedArcId,\n ariaLabel,\n className,\n}) => {\n const { t } = useTranslation('ui');\n const safeLanes = useMemo(() => (Array.isArray(lanes) ? lanes : []), [lanes]);\n const safeArcs = useMemo(() => (Array.isArray(arcs) ? arcs : []), [arcs]);\n\n const nodes = useMemo<Array<TLaneNode>>(\n () =>\n safeLanes.map((lane, index) => ({\n id: lane.laneId,\n type: 'flowLane',\n position: { x: index * (LANE_WIDTH + LANE_GAP), y: 0 },\n draggable: false,\n selectable: false,\n data: { lane, onLaneClick },\n })),\n [safeLanes, onLaneClick],\n );\n\n const laneIds = useMemo(() => new Set(safeLanes.map((lane) => lane.laneId)), [safeLanes]);\n\n const edges = useMemo<Array<TArcEdge>>(\n () =>\n safeArcs\n .filter(\n (arc) =>\n laneIds.has(arc.sourceLaneId) && laneIds.has(arc.targetLaneId) && arc.sourceLaneId !== arc.targetLaneId,\n )\n .map((arc) => ({\n id: arc.arcId,\n type: 'flowArc',\n source: arc.sourceLaneId,\n sourceHandle: `${arc.side}-out`,\n target: arc.targetLaneId,\n targetHandle: `${arc.side}-in`,\n selected: arc.arcId === selectedArcId,\n markerEnd: { type: MarkerType.ArrowClosed, width: 14, height: 14 },\n data: { arc },\n })),\n [safeArcs, laneIds, selectedArcId],\n );\n\n return (\n <div\n role=\"region\"\n aria-label={ariaLabel ?? t('flow.laneMap.label')}\n className={twMerge('umode-flow relative h-full min-h-[24rem] w-full rounded-xl bg-canvas', className)}\n >\n <ReactFlow<TLaneNode, TArcEdge>\n nodes={nodes}\n edges={edges}\n nodeTypes={NODE_TYPES}\n edgeTypes={EDGE_TYPES}\n nodesDraggable={false}\n nodesConnectable={false}\n deleteKeyCode={null}\n onEdgeClick={(_event, edge) => onArcClick?.(edge.id)}\n minZoom={0.25}\n maxZoom={1.5}\n fitView\n fitViewOptions={{ padding: 0.15, minZoom: 0.75 }}\n proOptions={PRO_OPTIONS}\n >\n <Background variant={BackgroundVariant.Dots} gap={16} size={1} />\n <FlowZoomControls />\n </ReactFlow>\n </div>\n );\n};\n\nconst FlowLaneMap: React.FC<IFlowLaneMapProps> = (props) => (\n <ReactFlowProvider>\n <FlowLaneMapContent {...props} />\n </ReactFlowProvider>\n);\n\nexport default FlowLaneMap;\n"],"names":[],"mappings":"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"}
@@ -14,7 +14,7 @@ export interface IFlowResizablePanelProps {
14
14
  /**
15
15
  * A side panel of the flow editor that the user can widen or narrow by dragging the edge facing
16
16
  * the canvas (arrow keys on the focused handle, double click resets). Below `lg` it takes the full
17
- * width and the handle hides: stacked under the canvas there is nothing to resize against.
17
+ * width of its container and the handle hides; where it sits then is up to the parent layout.
18
18
  */
19
19
  declare const FlowResizablePanel: React.FC<IFlowResizablePanelProps>;
20
20
  export default FlowResizablePanel;
@@ -1 +1 @@
1
- {"version":3,"file":"FlowResizablePanel.js","sources":["../../../../src/components/UI/Flow/FlowResizablePanel.tsx"],"sourcesContent":["import { CSSProperties, ReactNode, useCallback, useEffect, useRef, useState } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\nimport { twMerge } from 'tailwind-merge';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { readUiPreference, writeUiPreference } from '@/helpers/uiPreferences';\n\nconst PANEL_WIDTHS_KEY = 'umode-flow-panel-widths';\nconst KEYBOARD_RESIZE_STEP = 24;\n\nexport interface IFlowResizablePanelProps {\n /** Which edge carries the drag handle: the one that faces the canvas. */\n handleSide: 'left' | 'right';\n /** Widths in px, like any pointer-driven geometry. */\n defaultWidth: number;\n minWidth: number;\n maxWidth: number;\n /** Remembers the width the user chose, per panel, across visits. */\n storageKey?: string;\n className?: string;\n children: ReactNode;\n}\n\nconst readStoredWidths = (): Record<string, number> => readUiPreference<Record<string, number>>(PANEL_WIDTHS_KEY) ?? {};\n\n/**\n * A side panel of the flow editor that the user can widen or narrow by dragging the edge facing\n * the canvas (arrow keys on the focused handle, double click resets). Below `lg` it takes the full\n * width and the handle hides: stacked under the canvas there is nothing to resize against.\n */\nconst FlowResizablePanel: React.FC<IFlowResizablePanelProps> = ({\n handleSide,\n defaultWidth,\n minWidth,\n maxWidth,\n storageKey,\n className,\n children,\n}) => {\n const { t } = useTranslation('ui');\n const clampWidth = useCallback(\n (width: number) => Math.min(maxWidth, Math.max(minWidth, Math.round(width))),\n [minWidth, maxWidth],\n );\n const [width, setWidth] = useState(() => {\n const stored = storageKey ? readStoredWidths()[storageKey] : undefined;\n return clampWidth(typeof stored === 'number' ? stored : defaultWidth);\n });\n const [isResizing, setIsResizing] = useState(false);\n const resizeStartRef = useRef<{ pointerX: number; width: number } | null>(null);\n const widthRef = useRef(width);\n widthRef.current = width;\n\n const persistWidth = useCallback(\n (nextWidth: number) => {\n if (!storageKey) return;\n writeUiPreference(PANEL_WIDTHS_KEY, { ...readStoredWidths(), [storageKey]: nextWidth });\n },\n [storageKey],\n );\n\n useEffect(() => setWidth((current) => clampWidth(current)), [clampWidth]);\n\n const handlePointerDown = (event: React.PointerEvent<HTMLButtonElement>) => {\n event.preventDefault();\n resizeStartRef.current = { pointerX: event.clientX, width: widthRef.current };\n setIsResizing(true);\n event.currentTarget.setPointerCapture(event.pointerId);\n };\n\n const handlePointerMove = (event: React.PointerEvent<HTMLButtonElement>) => {\n const resizeStart = resizeStartRef.current;\n if (!resizeStart) return;\n const pointerDelta = event.clientX - resizeStart.pointerX;\n setWidth(clampWidth(resizeStart.width + (handleSide === 'right' ? pointerDelta : -pointerDelta)));\n };\n\n const handlePointerEnd = (event: React.PointerEvent<HTMLButtonElement>) => {\n if (!resizeStartRef.current) return;\n resizeStartRef.current = null;\n setIsResizing(false);\n persistWidth(widthRef.current);\n if (event.currentTarget.hasPointerCapture(event.pointerId)) {\n event.currentTarget.releasePointerCapture(event.pointerId);\n }\n };\n\n const handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {\n if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return;\n event.preventDefault();\n const pointerDirection = event.key === 'ArrowRight' ? 1 : -1;\n const nextWidth = clampWidth(\n widthRef.current + (handleSide === 'right' ? pointerDirection : -pointerDirection) * KEYBOARD_RESIZE_STEP,\n );\n setWidth(nextWidth);\n persistWidth(nextWidth);\n };\n\n const handleReset = () => {\n const nextWidth = clampWidth(defaultWidth);\n setWidth(nextWidth);\n persistWidth(nextWidth);\n };\n\n return (\n <div\n className={twMerge('relative w-full shrink-0 lg:w-[var(--flow-panel-width)]', className)}\n style={{ '--flow-panel-width': `${width}px` } as CSSProperties}\n >\n {children}\n <ControlledTooltip content={t('flow.resizePanel')} placement={handleSide === 'right' ? 'right' : 'left'}>\n <ControlledButton\n defaultHtmlButton\n onPointerDown={handlePointerDown}\n onPointerMove={handlePointerMove}\n onPointerUp={handlePointerEnd}\n onPointerCancel={handlePointerEnd}\n onKeyDown={handleKeyDown}\n onDoubleClick={handleReset}\n className={classNames(\n 'group/resize absolute inset-y-0 z-10 hidden w-3 cursor-col-resize touch-none select-none items-center justify-center focus:outline-none lg:flex',\n handleSide === 'right' ? '-right-3' : '-left-3',\n )}\n >\n <span\n className={classNames(\n 'h-full w-0.5 rounded-full transition-colors duration-base',\n isResizing ? 'bg-brand-hover' : 'bg-transparent group-hover/resize:bg-brand-hover',\n )}\n />\n </ControlledButton>\n </ControlledTooltip>\n </div>\n );\n};\n\nexport default FlowResizablePanel;\n"],"names":["width"],"mappings":"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"}
1
+ {"version":3,"file":"FlowResizablePanel.js","sources":["../../../../src/components/UI/Flow/FlowResizablePanel.tsx"],"sourcesContent":["import { CSSProperties, ReactNode, useCallback, useEffect, useRef, useState } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\nimport { twMerge } from 'tailwind-merge';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { readUiPreference, writeUiPreference } from '@/helpers/uiPreferences';\n\nconst PANEL_WIDTHS_KEY = 'umode-flow-panel-widths';\nconst KEYBOARD_RESIZE_STEP = 24;\n\nexport interface IFlowResizablePanelProps {\n /** Which edge carries the drag handle: the one that faces the canvas. */\n handleSide: 'left' | 'right';\n /** Widths in px, like any pointer-driven geometry. */\n defaultWidth: number;\n minWidth: number;\n maxWidth: number;\n /** Remembers the width the user chose, per panel, across visits. */\n storageKey?: string;\n className?: string;\n children: ReactNode;\n}\n\nconst readStoredWidths = (): Record<string, number> => readUiPreference<Record<string, number>>(PANEL_WIDTHS_KEY) ?? {};\n\n/**\n * A side panel of the flow editor that the user can widen or narrow by dragging the edge facing\n * the canvas (arrow keys on the focused handle, double click resets). Below `lg` it takes the full\n * width of its container and the handle hides; where it sits then is up to the parent layout.\n */\nconst FlowResizablePanel: React.FC<IFlowResizablePanelProps> = ({\n handleSide,\n defaultWidth,\n minWidth,\n maxWidth,\n storageKey,\n className,\n children,\n}) => {\n const { t } = useTranslation('ui');\n const clampWidth = useCallback(\n (width: number) => Math.min(maxWidth, Math.max(minWidth, Math.round(width))),\n [minWidth, maxWidth],\n );\n const [width, setWidth] = useState(() => {\n const stored = storageKey ? readStoredWidths()[storageKey] : undefined;\n return clampWidth(typeof stored === 'number' ? stored : defaultWidth);\n });\n const [isResizing, setIsResizing] = useState(false);\n const resizeStartRef = useRef<{ pointerX: number; width: number } | null>(null);\n const widthRef = useRef(width);\n widthRef.current = width;\n\n const persistWidth = useCallback(\n (nextWidth: number) => {\n if (!storageKey) return;\n writeUiPreference(PANEL_WIDTHS_KEY, { ...readStoredWidths(), [storageKey]: nextWidth });\n },\n [storageKey],\n );\n\n useEffect(() => setWidth((current) => clampWidth(current)), [clampWidth]);\n\n const handlePointerDown = (event: React.PointerEvent<HTMLButtonElement>) => {\n event.preventDefault();\n resizeStartRef.current = { pointerX: event.clientX, width: widthRef.current };\n setIsResizing(true);\n event.currentTarget.setPointerCapture(event.pointerId);\n };\n\n const handlePointerMove = (event: React.PointerEvent<HTMLButtonElement>) => {\n const resizeStart = resizeStartRef.current;\n if (!resizeStart) return;\n const pointerDelta = event.clientX - resizeStart.pointerX;\n setWidth(clampWidth(resizeStart.width + (handleSide === 'right' ? pointerDelta : -pointerDelta)));\n };\n\n const handlePointerEnd = (event: React.PointerEvent<HTMLButtonElement>) => {\n if (!resizeStartRef.current) return;\n resizeStartRef.current = null;\n setIsResizing(false);\n persistWidth(widthRef.current);\n if (event.currentTarget.hasPointerCapture(event.pointerId)) {\n event.currentTarget.releasePointerCapture(event.pointerId);\n }\n };\n\n const handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {\n if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return;\n event.preventDefault();\n const pointerDirection = event.key === 'ArrowRight' ? 1 : -1;\n const nextWidth = clampWidth(\n widthRef.current + (handleSide === 'right' ? pointerDirection : -pointerDirection) * KEYBOARD_RESIZE_STEP,\n );\n setWidth(nextWidth);\n persistWidth(nextWidth);\n };\n\n const handleReset = () => {\n const nextWidth = clampWidth(defaultWidth);\n setWidth(nextWidth);\n persistWidth(nextWidth);\n };\n\n return (\n <div\n className={twMerge('relative w-full shrink-0 lg:w-[var(--flow-panel-width)]', className)}\n style={{ '--flow-panel-width': `${width}px` } as CSSProperties}\n >\n {children}\n <ControlledTooltip content={t('flow.resizePanel')} placement={handleSide === 'right' ? 'right' : 'left'}>\n <ControlledButton\n defaultHtmlButton\n onPointerDown={handlePointerDown}\n onPointerMove={handlePointerMove}\n onPointerUp={handlePointerEnd}\n onPointerCancel={handlePointerEnd}\n onKeyDown={handleKeyDown}\n onDoubleClick={handleReset}\n className={classNames(\n 'group/resize absolute inset-y-0 z-10 hidden w-3 cursor-col-resize touch-none select-none items-center justify-center focus:outline-none lg:flex',\n handleSide === 'right' ? '-right-3' : '-left-3',\n )}\n >\n <span\n className={classNames(\n 'h-full w-0.5 rounded-full transition-colors duration-base',\n isResizing ? 'bg-brand-hover' : 'bg-transparent group-hover/resize:bg-brand-hover',\n )}\n />\n </ControlledButton>\n </ControlledTooltip>\n </div>\n );\n};\n\nexport default FlowResizablePanel;\n"],"names":["width"],"mappings":"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"}
@@ -1,8 +1,7 @@
1
- import { IFlowBlockDefinition, TFlowBlockCategory } from './flowTypes';
1
+ import { IFlowBlockDefinition, TFlowPortId } from './flowTypes';
2
2
  export interface IFlowBlockRegistry {
3
3
  list: Array<IFlowBlockDefinition>;
4
4
  get: (type: string) => IFlowBlockDefinition | undefined;
5
- byCategory: (category: TFlowBlockCategory) => Array<IFlowBlockDefinition>;
6
5
  }
7
6
  /**
8
7
  * Erases the config type so blocks with different configs live in one registry. The
@@ -10,3 +9,4 @@ export interface IFlowBlockRegistry {
10
9
  */
11
10
  export declare const defineFlowBlock: <TConfig>(definition: IFlowBlockDefinition<TConfig>) => IFlowBlockDefinition;
12
11
  export declare const createFlowBlockRegistry: (definitions: Array<IFlowBlockDefinition>) => IFlowBlockRegistry;
12
+ export declare const flowOutputsOf: (definition: IFlowBlockDefinition | undefined, config: unknown) => Array<TFlowPortId>;
@@ -4,12 +4,17 @@ const createFlowBlockRegistry = (definitions) => {
4
4
  const byType = new Map(safeDefinitions.map((definition) => [definition.type, definition]));
5
5
  return {
6
6
  list: safeDefinitions,
7
- get: (type) => byType.get(type),
8
- byCategory: (category) => safeDefinitions.filter((definition) => definition.category === category)
7
+ get: (type) => byType.get(type)
9
8
  };
10
9
  };
10
+ const flowOutputsOf = (definition, config) => {
11
+ if (!definition) return [];
12
+ const resolved = definition.resolveOutputs?.(config);
13
+ return Array.isArray(resolved) ? resolved : definition.outputs;
14
+ };
11
15
  export {
12
16
  createFlowBlockRegistry,
13
- defineFlowBlock
17
+ defineFlowBlock,
18
+ flowOutputsOf
14
19
  };
15
20
  //# sourceMappingURL=flowBlockRegistry.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"flowBlockRegistry.js","sources":["../../../../src/components/UI/Flow/flowBlockRegistry.ts"],"sourcesContent":["import type { IFlowBlockDefinition, TFlowBlockCategory } from './flowTypes';\n\nexport interface IFlowBlockRegistry {\n list: Array<IFlowBlockDefinition>;\n get: (type: string) => IFlowBlockDefinition | undefined;\n byCategory: (category: TFlowBlockCategory) => Array<IFlowBlockDefinition>;\n}\n\n/**\n * Erases the config type so blocks with different configs live in one registry. The\n * canvas only hands each block the config it created, so the cast never crosses blocks.\n */\nexport const defineFlowBlock = <TConfig>(definition: IFlowBlockDefinition<TConfig>): IFlowBlockDefinition =>\n definition as unknown as IFlowBlockDefinition;\n\nexport const createFlowBlockRegistry = (definitions: Array<IFlowBlockDefinition>): IFlowBlockRegistry => {\n const safeDefinitions = Array.isArray(definitions) ? definitions : [];\n const byType = new Map(safeDefinitions.map((definition) => [definition.type, definition]));\n\n return {\n list: safeDefinitions,\n get: (type) => byType.get(type),\n byCategory: (category) => safeDefinitions.filter((definition) => definition.category === category),\n };\n};\n"],"names":[],"mappings":"AAYO,MAAM,kBAAkB,CAAU,eACvC;AAEK,MAAM,0BAA0B,CAAC,gBAAiE;AACvG,QAAM,kBAAkB,MAAM,QAAQ,WAAW,IAAI,cAAc,CAAA;AACnE,QAAM,SAAS,IAAI,IAAI,gBAAgB,IAAI,CAAC,eAAe,CAAC,WAAW,MAAM,UAAU,CAAC,CAAC;AAEzF,SAAO;AAAA,IACL,MAAM;AAAA,IACN,KAAK,CAAC,SAAS,OAAO,IAAI,IAAI;AAAA,IAC9B,YAAY,CAAC,aAAa,gBAAgB,OAAO,CAAC,eAAe,WAAW,aAAa,QAAQ;AAAA,EAAA;AAErG;"}
1
+ {"version":3,"file":"flowBlockRegistry.js","sources":["../../../../src/components/UI/Flow/flowBlockRegistry.ts"],"sourcesContent":["import type { IFlowBlockDefinition, TFlowPortId } from './flowTypes';\n\nexport interface IFlowBlockRegistry {\n list: Array<IFlowBlockDefinition>;\n get: (type: string) => IFlowBlockDefinition | undefined;\n}\n\n/**\n * Erases the config type so blocks with different configs live in one registry. The\n * canvas only hands each block the config it created, so the cast never crosses blocks.\n */\nexport const defineFlowBlock = <TConfig>(definition: IFlowBlockDefinition<TConfig>): IFlowBlockDefinition =>\n definition as unknown as IFlowBlockDefinition;\n\nexport const createFlowBlockRegistry = (definitions: Array<IFlowBlockDefinition>): IFlowBlockRegistry => {\n const safeDefinitions = Array.isArray(definitions) ? definitions : [];\n const byType = new Map(safeDefinitions.map((definition) => [definition.type, definition]));\n\n return {\n list: safeDefinitions,\n get: (type) => byType.get(type),\n };\n};\n\nexport const flowOutputsOf = (definition: IFlowBlockDefinition | undefined, config: unknown): Array<TFlowPortId> => {\n if (!definition) return [];\n const resolved = definition.resolveOutputs?.(config);\n return Array.isArray(resolved) ? resolved : definition.outputs;\n};\n"],"names":[],"mappings":"AAWO,MAAM,kBAAkB,CAAU,eACvC;AAEK,MAAM,0BAA0B,CAAC,gBAAiE;AACvG,QAAM,kBAAkB,MAAM,QAAQ,WAAW,IAAI,cAAc,CAAA;AACnE,QAAM,SAAS,IAAI,IAAI,gBAAgB,IAAI,CAAC,eAAe,CAAC,WAAW,MAAM,UAAU,CAAC,CAAC;AAEzF,SAAO;AAAA,IACL,MAAM;AAAA,IACN,KAAK,CAAC,SAAS,OAAO,IAAI,IAAI;AAAA,EAAA;AAElC;AAEO,MAAM,gBAAgB,CAAC,YAA8C,WAAwC;AAClH,MAAI,CAAC,WAAY,QAAO,CAAA;AACxB,QAAM,WAAW,WAAW,iBAAiB,MAAM;AACnD,SAAO,MAAM,QAAQ,QAAQ,IAAI,WAAW,WAAW;AACzD;"}
@@ -15,14 +15,19 @@ export interface IFlowEditorState {
15
15
  markClean: () => void;
16
16
  /** Snapshot before a gesture that changes the graph in many small steps (a drag). */
17
17
  recordHistory: () => void;
18
- addNode: (type: string, position?: IFlowPosition) => string | null;
18
+ addNode: (type: string) => string | null;
19
19
  /** Adds a block right of `sourceNodeId` and wires `sourcePort` to it in one undo step. */
20
20
  addNodeFromPort: (type: string, sourceNodeId: string, sourcePort: TFlowPortId) => string | null;
21
+ /**
22
+ * Puts a block in the middle of an edge: the edge now ends at the new block, whose first output
23
+ * leads to the old target. The old target and everything after it move one column right, so the
24
+ * new block takes the place it left. One undo step.
25
+ */
26
+ insertNodeOnEdge: (type: string, edgeId: string) => string | null;
21
27
  updateNodeConfig: (nodeId: string, config: unknown) => void;
22
28
  updateNodeLabel: (nodeId: string, label: string) => void;
23
29
  moveNodes: (positions: Record<string, IFlowPosition>) => void;
24
30
  connect: (source: string, sourcePort: TFlowPortId, target: string) => boolean;
25
- removeEdges: (edgeIds: Array<string>) => void;
26
31
  removeSelected: () => void;
27
32
  duplicateSelected: () => void;
28
33
  copySelected: () => void;