@umodeapporg/ui 0.24.0 → 0.24.1

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.
@@ -7,6 +7,7 @@ import ControlledTooltip from "../ControlledTooltip.js";
7
7
  import { MaterialSymbol } from "../MaterialSymbol/index.js";
8
8
  import NoDataFound from "../NoDataFound/index.js";
9
9
  import { useFlowBlockRegistry, useFlowEditorStore, useFlowEditor } from "./FlowEditorProvider.js";
10
+ import { isFlowNodeRemovable } from "./flowGraph.js";
10
11
  import { useFlowLabels } from "./useFlowLabels.js";
11
12
  const FlowInspector = ({
12
13
  issues = [],
@@ -33,6 +34,8 @@ const FlowInspector = ({
33
34
  const definition = registry.get(selectedNode.type);
34
35
  const nodeIssues = (Array.isArray(issues) ? issues : []).filter((issue) => issue.nodeId === selectedNode.nodeId);
35
36
  const ConfigPanel = definition?.ConfigPanel;
37
+ const canDuplicate = definition?.category !== "trigger";
38
+ const canRemove = isFlowNodeRemovable(selectedNode, registry);
36
39
  return /* @__PURE__ */ jsxs("aside", { className: shellClass, "aria-label": t("flow.inspector.title"), children: [
37
40
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-x-2", children: [
38
41
  /* @__PURE__ */ jsx("span", { className: "flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground-muted", children: /* @__PURE__ */ jsx(
@@ -88,8 +91,8 @@ const FlowInspector = ({
88
91
  /* @__PURE__ */ jsx("span", { className: "text-xs text-foreground-muted", children: labels.issueMessage(issue) })
89
92
  ] }, `${issue.code}|${issue.edgeId ?? ""}`)) })
90
93
  ] }),
91
- !readOnly && /* @__PURE__ */ jsxs("div", { className: "flex gap-x-2", children: [
92
- definition?.category !== "trigger" && /* @__PURE__ */ jsxs(
94
+ !readOnly && (canDuplicate || canRemove) && /* @__PURE__ */ jsxs("div", { className: "flex gap-x-2", children: [
95
+ canDuplicate && /* @__PURE__ */ jsxs(
93
96
  ControlledButton,
94
97
  {
95
98
  size: "sm",
@@ -102,7 +105,7 @@ const FlowInspector = ({
102
105
  ]
103
106
  }
104
107
  ),
105
- /* @__PURE__ */ jsxs(
108
+ canRemove && /* @__PURE__ */ jsxs(
106
109
  ControlledButton,
107
110
  {
108
111
  size: "sm",
@@ -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 /** 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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"}
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 { isFlowNodeRemovable } from './flowGraph';\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 const canDuplicate = definition?.category !== 'trigger';\n const canRemove = isFlowNodeRemovable(selectedNode, registry);\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 && (canDuplicate || canRemove) && (\n <div className=\"flex gap-x-2\">\n {canDuplicate && (\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 {canRemove && (\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 )}\n </div>\n )}\n </aside>\n );\n};\n\nexport default FlowInspector;\n"],"names":[],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { createStore } from "zustand/vanilla";
2
2
  import { generateUUID } from "../../../helpers/utilityHelpers.js";
3
3
  import { flowOutputsOf } from "./flowBlockRegistry.js";
4
- import { canConnect, reachableFrom, FLOW_LEVEL_GAP, FLOW_SIBLING_GAP, layoutFlowByLevels } from "./flowGraph.js";
4
+ import { isFlowNodeRemovable, canConnect, reachableFrom, FLOW_LEVEL_GAP, FLOW_SIBLING_GAP, layoutFlowByLevels } from "./flowGraph.js";
5
5
  const HISTORY_LIMIT = 50;
6
6
  const PASTE_OFFSET = 32;
7
7
  const COALESCE_WINDOW_MS = 1e3;
@@ -226,11 +226,12 @@ const createFlowEditorStore = (initialGraph, registry) => createStore()((set, ge
226
226
  },
227
227
  removeSelected: () => {
228
228
  const { graph, selectedNodeIds, selectedEdgeIds } = get();
229
- if (selectedNodeIds.length === 0 && selectedEdgeIds.length === 0) return;
229
+ const removableNodeIds = graph.nodes.filter((node) => selectedNodeIds.includes(node.nodeId) && isFlowNodeRemovable(node, currentRegistry)).map((node) => node.nodeId);
230
+ if (removableNodeIds.length === 0 && selectedEdgeIds.length === 0) return;
230
231
  const removedEdges = new Set(selectedEdgeIds);
231
232
  const next = withoutNodes(
232
233
  { ...graph, edges: graph.edges.filter((edge) => !removedEdges.has(edge.edgeId)) },
233
- new Set(selectedNodeIds)
234
+ new Set(removableNodeIds)
234
235
  );
235
236
  commit(next, { nodeIds: [], edgeIds: [] });
236
237
  },
@@ -1 +1 @@
1
- {"version":3,"file":"flowEditorStore.js","sources":["../../../../src/components/UI/Flow/flowEditorStore.ts"],"sourcesContent":["import { createStore, type StoreApi } from 'zustand/vanilla';\n\nimport { generateUUID } from '@/helpers/utilityHelpers';\n\nimport { flowOutputsOf, type IFlowBlockRegistry } from './flowBlockRegistry';\nimport { canConnect, FLOW_LEVEL_GAP, FLOW_SIBLING_GAP, layoutFlowByLevels, reachableFrom } from './flowGraph';\nimport type { IFlowEdge, IFlowGraph, IFlowNode, IFlowPosition, TFlowPortId } from './flowTypes';\n\nconst HISTORY_LIMIT = 50;\nconst PASTE_OFFSET = 32;\n/** Edits to the same field within this window share one undo step (typing a name is one change). */\nconst COALESCE_WINDOW_MS = 1000;\n\nexport interface IFlowEditorState {\n graph: IFlowGraph;\n selectedNodeIds: Array<string>;\n selectedEdgeIds: Array<string>;\n past: Array<IFlowGraph>;\n future: Array<IFlowGraph>;\n clipboard: Array<IFlowNode>;\n isDirty: boolean;\n\n replaceGraph: (graph: IFlowGraph, options?: { keepHistory?: boolean }) => void;\n markClean: () => void;\n /** Snapshot before a gesture that changes the graph in many small steps (a drag). */\n recordHistory: () => void;\n addNode: (type: string) => string | null;\n /** Adds a block right of `sourceNodeId` and wires `sourcePort` to it in one undo step. */\n addNodeFromPort: (type: string, sourceNodeId: string, sourcePort: TFlowPortId) => string | null;\n /**\n * Puts a block in the middle of an edge: the edge now ends at the new block, whose first output\n * leads to the old target. The old target and everything after it move one column right, so the\n * new block takes the place it left. One undo step.\n */\n insertNodeOnEdge: (type: string, edgeId: string) => string | null;\n updateNodeConfig: (nodeId: string, config: unknown) => void;\n updateNodeLabel: (nodeId: string, label: string) => void;\n moveNodes: (positions: Record<string, IFlowPosition>) => void;\n connect: (source: string, sourcePort: TFlowPortId, target: string) => boolean;\n removeSelected: () => void;\n duplicateSelected: () => void;\n copySelected: () => void;\n paste: () => void;\n setSelection: (selection: { nodeIds?: Array<string>; edgeIds?: Array<string> }) => void;\n undo: () => void;\n redo: () => void;\n /** The host may rebuild its registry (labels after a language switch); gestures always read the latest. */\n setRegistry: (registry: IFlowBlockRegistry) => void;\n}\n\nexport type TFlowEditorStore = StoreApi<IFlowEditorState>;\n\nconst isPosition = (position: unknown): position is IFlowPosition =>\n typeof (position as IFlowPosition | undefined)?.x === 'number' &&\n typeof (position as IFlowPosition | undefined)?.y === 'number';\n\n/** Coerces a stored graph; a block saved without a position is placed by the automatic layout. */\nconst normalizeGraph = (graph: IFlowGraph | null | undefined, registry: IFlowBlockRegistry): IFlowGraph => {\n const normalized: IFlowGraph = {\n nodes: Array.isArray(graph?.nodes) ? graph.nodes : [],\n edges: Array.isArray(graph?.edges) ? graph.edges : [],\n };\n if (normalized.nodes.every((node) => isPosition(node.position))) return normalized;\n\n const layout = layoutFlowByLevels(normalized, registry);\n return {\n ...normalized,\n nodes: normalized.nodes.map((node) =>\n isPosition(node.position) ? node : { ...node, position: layout[node.nodeId] ?? { x: 0, y: 0 } },\n ),\n };\n};\n\nconst withoutNodes = (graph: IFlowGraph, nodeIds: Set<string>): IFlowGraph => ({\n nodes: graph.nodes.filter((node) => !nodeIds.has(node.nodeId)),\n edges: graph.edges.filter((edge) => !nodeIds.has(edge.source) && !nodeIds.has(edge.target)),\n});\n\nconst isPositionTaken = (graph: IFlowGraph, position: IFlowPosition): boolean =>\n graph.nodes.some(\n (node) =>\n Math.abs(node.position.x - position.x) < FLOW_LEVEL_GAP &&\n Math.abs(node.position.y - position.y) < FLOW_SIBLING_GAP,\n );\n\n/** Right of the selected block (or the rightmost one), stepping down the column until the slot is free. */\nconst nextFreePosition = (graph: IFlowGraph, selectedNodeIds: Array<string>): IFlowPosition => {\n if (graph.nodes.length === 0) return { x: 0, y: 0 };\n const anchor =\n graph.nodes.find((node) => node.nodeId === selectedNodeIds[0]) ??\n graph.nodes.reduce((current, node) => (node.position.x > current.position.x ? node : current));\n let candidate = { x: anchor.position.x + FLOW_LEVEL_GAP, y: anchor.position.y };\n while (isPositionTaken(graph, candidate)) candidate = { x: candidate.x, y: candidate.y + FLOW_SIBLING_GAP };\n return candidate;\n};\n\n/**\n * One store per editor instance (two canvases on a page never share state). The graph\n * is the source of truth; the canvas only renders it and reports gestures back.\n */\nexport const createFlowEditorStore = (initialGraph: IFlowGraph, registry: IFlowBlockRegistry): TFlowEditorStore =>\n createStore<IFlowEditorState>()((set, get) => {\n let currentRegistry = registry;\n let lastCoalesce: { key: string; at: number } | null = null;\n\n const commit = (\n next: IFlowGraph,\n selection?: { nodeIds?: Array<string>; edgeIds?: Array<string> },\n coalesceKey?: string,\n ) => {\n const { graph, past } = get();\n const now = Date.now();\n const extendsLastStep =\n coalesceKey !== undefined && lastCoalesce?.key === coalesceKey && now - lastCoalesce.at < COALESCE_WINDOW_MS;\n lastCoalesce = coalesceKey === undefined ? null : { key: coalesceKey, at: now };\n set({\n graph: next,\n past: extendsLastStep ? past : [...past, graph].slice(-HISTORY_LIMIT),\n future: [],\n isDirty: true,\n ...(selection?.nodeIds ? { selectedNodeIds: selection.nodeIds } : {}),\n ...(selection?.edgeIds ? { selectedEdgeIds: selection.edgeIds } : {}),\n });\n };\n\n const cloneNodes = (nodes: Array<IFlowNode>, offset: number): Array<IFlowNode> => {\n const instancesByType = new Map<string, number>();\n get().graph.nodes.forEach((node) => instancesByType.set(node.type, (instancesByType.get(node.type) ?? 0) + 1));\n return nodes\n .filter((node) => {\n const definition = currentRegistry.get(node.type);\n if (!definition || definition.category === 'trigger') return false;\n const instances = instancesByType.get(node.type) ?? 0;\n if (definition.maxInstances !== undefined && instances >= definition.maxInstances) return false;\n instancesByType.set(node.type, instances + 1);\n return true;\n })\n .map((node) => ({\n ...node,\n nodeId: generateUUID(),\n config: structuredClone(node.config),\n position: { x: node.position.x + offset, y: node.position.y + offset },\n }));\n };\n\n return {\n graph: normalizeGraph(initialGraph, registry),\n selectedNodeIds: [],\n selectedEdgeIds: [],\n past: [],\n future: [],\n clipboard: [],\n isDirty: false,\n\n replaceGraph: (graph, options) => {\n set((state) => ({\n graph: normalizeGraph(graph, currentRegistry),\n past: options?.keepHistory ? [...state.past, state.graph].slice(-HISTORY_LIMIT) : [],\n future: [],\n selectedNodeIds: [],\n selectedEdgeIds: [],\n isDirty: Boolean(options?.keepHistory),\n }));\n },\n\n markClean: () => set({ isDirty: false }),\n\n recordHistory: () => {\n const { graph, past } = get();\n set({ past: [...past, graph].slice(-HISTORY_LIMIT), future: [] });\n },\n\n addNode: (type) => {\n const definition = currentRegistry.get(type);\n if (!definition) return null;\n\n const { graph, selectedNodeIds } = get();\n const instances = graph.nodes.filter((node) => node.type === type).length;\n if (definition.maxInstances !== undefined && instances >= definition.maxInstances) return null;\n\n const node: IFlowNode = {\n nodeId: generateUUID(),\n type,\n typeVersion: definition.typeVersion,\n config: definition.createDefaultConfig(),\n position: nextFreePosition(graph, selectedNodeIds),\n };\n commit({ ...graph, nodes: [...graph.nodes, node] }, { nodeIds: [node.nodeId], edgeIds: [] });\n return node.nodeId;\n },\n\n addNodeFromPort: (type, sourceNodeId, sourcePort) => {\n const definition = currentRegistry.get(type);\n const { graph } = get();\n const source = graph.nodes.find((node) => node.nodeId === sourceNodeId);\n if (!definition || !source) return null;\n\n const instances = graph.nodes.filter((node) => node.type === type).length;\n if (definition.maxInstances !== undefined && instances >= definition.maxInstances) return null;\n if (graph.edges.some((edge) => edge.source === sourceNodeId && edge.sourcePort === sourcePort)) return null;\n\n const outputs = flowOutputsOf(currentRegistry.get(source.type), source.config);\n if (!outputs.includes(sourcePort)) return null;\n const portIndex = outputs.indexOf(sourcePort);\n let position = {\n x: source.position.x + FLOW_LEVEL_GAP,\n y: source.position.y + (portIndex - (outputs.length - 1) / 2) * FLOW_SIBLING_GAP,\n };\n while (isPositionTaken(graph, position)) position = { x: position.x, y: position.y + FLOW_SIBLING_GAP };\n\n const node: IFlowNode = {\n nodeId: generateUUID(),\n type,\n typeVersion: definition.typeVersion,\n config: definition.createDefaultConfig(),\n position,\n };\n const nodes = [...graph.nodes, node];\n const wires = canConnect({ ...graph, nodes }, currentRegistry, {\n source: sourceNodeId,\n sourcePort,\n target: node.nodeId,\n });\n const edges = wires\n ? [...graph.edges, { edgeId: generateUUID(), source: sourceNodeId, sourcePort, target: node.nodeId }]\n : graph.edges;\n commit({ nodes, edges }, { nodeIds: [node.nodeId], edgeIds: [] });\n return node.nodeId;\n },\n\n insertNodeOnEdge: (type, edgeId) => {\n const definition = currentRegistry.get(type);\n const { graph } = get();\n const edge = graph.edges.find((candidate) => candidate.edgeId === edgeId);\n const target = edge ? graph.nodes.find((node) => node.nodeId === edge.target) : undefined;\n const config = definition?.createDefaultConfig();\n const continuationPort = definition ? flowOutputsOf(definition, config)[0] : undefined;\n if (!definition || !edge || !target || !continuationPort || definition.category === 'trigger') return null;\n\n const instances = graph.nodes.filter((node) => node.type === type).length;\n if (definition.maxInstances !== undefined && instances >= definition.maxInstances) return null;\n\n const shifted = reachableFrom(graph, target.nodeId);\n const node: IFlowNode = {\n nodeId: generateUUID(),\n type,\n typeVersion: definition.typeVersion,\n config,\n position: { ...target.position },\n };\n const nodes = [\n ...graph.nodes.map((current) =>\n shifted.has(current.nodeId)\n ? { ...current, position: { x: current.position.x + FLOW_LEVEL_GAP, y: current.position.y } }\n : current,\n ),\n node,\n ];\n const withoutEdge = graph.edges.filter((current) => current.edgeId !== edgeId);\n const incoming: IFlowEdge = { ...edge, target: node.nodeId };\n const outgoing: IFlowEdge = {\n edgeId: generateUUID(),\n source: node.nodeId,\n sourcePort: continuationPort,\n target: target.nodeId,\n };\n\n const wiresIn = canConnect({ nodes, edges: withoutEdge }, currentRegistry, incoming);\n const wiresOut = canConnect({ nodes, edges: [...withoutEdge, incoming] }, currentRegistry, outgoing);\n if (!wiresIn || !wiresOut) return null;\n\n commit({ nodes, edges: [...withoutEdge, incoming, outgoing] }, { nodeIds: [node.nodeId], edgeIds: [] });\n return node.nodeId;\n },\n\n updateNodeConfig: (nodeId, config) => {\n const { graph } = get();\n const node = graph.nodes.find((candidate) => candidate.nodeId === nodeId);\n const outputs = node ? flowOutputsOf(currentRegistry.get(node.type), config) : [];\n commit(\n {\n nodes: graph.nodes.map((current) => (current.nodeId === nodeId ? { ...current, config } : current)),\n edges: graph.edges.filter((edge) => edge.source !== nodeId || outputs.includes(edge.sourcePort)),\n },\n undefined,\n `config:${nodeId}`,\n );\n },\n\n updateNodeLabel: (nodeId, label) => {\n const { graph } = get();\n const hasText = label.trim().length > 0;\n commit(\n {\n ...graph,\n nodes: graph.nodes.map((node) =>\n node.nodeId === nodeId ? { ...node, label: hasText ? label : undefined } : node,\n ),\n },\n undefined,\n `label:${nodeId}`,\n );\n },\n\n moveNodes: (positions) => {\n const { graph } = get();\n set({\n graph: {\n ...graph,\n nodes: graph.nodes.map((node) =>\n positions[node.nodeId] ? { ...node, position: positions[node.nodeId] } : node,\n ),\n },\n isDirty: true,\n });\n },\n\n connect: (source, sourcePort, target) => {\n const { graph } = get();\n if (!canConnect(graph, currentRegistry, { source, sourcePort, target })) return false;\n const edge: IFlowEdge = { edgeId: generateUUID(), source, sourcePort, target };\n commit({ ...graph, edges: [...graph.edges, edge] });\n return true;\n },\n\n removeSelected: () => {\n const { graph, selectedNodeIds, selectedEdgeIds } = get();\n if (selectedNodeIds.length === 0 && selectedEdgeIds.length === 0) return;\n const removedEdges = new Set(selectedEdgeIds);\n const next = withoutNodes(\n { ...graph, edges: graph.edges.filter((edge) => !removedEdges.has(edge.edgeId)) },\n new Set(selectedNodeIds),\n );\n commit(next, { nodeIds: [], edgeIds: [] });\n },\n\n duplicateSelected: () => {\n const { graph, selectedNodeIds } = get();\n const selected = graph.nodes.filter((node) => selectedNodeIds.includes(node.nodeId));\n const copies = cloneNodes(selected, PASTE_OFFSET);\n if (copies.length === 0) return;\n commit(\n { ...graph, nodes: [...graph.nodes, ...copies] },\n { nodeIds: copies.map((node) => node.nodeId), edgeIds: [] },\n );\n },\n\n copySelected: () => {\n const { graph, selectedNodeIds } = get();\n set({ clipboard: graph.nodes.filter((node) => selectedNodeIds.includes(node.nodeId)) });\n },\n\n paste: () => {\n const { graph, clipboard } = get();\n const copies = cloneNodes(clipboard, PASTE_OFFSET);\n if (copies.length === 0) return;\n commit(\n { ...graph, nodes: [...graph.nodes, ...copies] },\n { nodeIds: copies.map((node) => node.nodeId), edgeIds: [] },\n );\n set({ clipboard: copies });\n },\n\n setSelection: ({ nodeIds, edgeIds }) => {\n set((state) => ({\n selectedNodeIds: nodeIds ?? state.selectedNodeIds,\n selectedEdgeIds: edgeIds ?? state.selectedEdgeIds,\n }));\n },\n\n undo: () => {\n const { graph, past, future } = get();\n const previous = past[past.length - 1];\n if (!previous) return;\n set({\n graph: previous,\n past: past.slice(0, -1),\n future: [graph, ...future].slice(0, HISTORY_LIMIT),\n selectedNodeIds: [],\n selectedEdgeIds: [],\n isDirty: true,\n });\n },\n\n redo: () => {\n const { graph, past, future } = get();\n const following = future[0];\n if (!following) return;\n set({\n graph: following,\n past: [...past, graph].slice(-HISTORY_LIMIT),\n future: future.slice(1),\n selectedNodeIds: [],\n selectedEdgeIds: [],\n isDirty: true,\n });\n },\n\n setRegistry: (nextRegistry) => {\n currentRegistry = nextRegistry;\n },\n };\n });\n"],"names":["node"],"mappings":"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"}
1
+ {"version":3,"file":"flowEditorStore.js","sources":["../../../../src/components/UI/Flow/flowEditorStore.ts"],"sourcesContent":["import { createStore, type StoreApi } from 'zustand/vanilla';\n\nimport { generateUUID } from '@/helpers/utilityHelpers';\n\nimport { flowOutputsOf, type IFlowBlockRegistry } from './flowBlockRegistry';\nimport {\n canConnect,\n FLOW_LEVEL_GAP,\n FLOW_SIBLING_GAP,\n isFlowNodeRemovable,\n layoutFlowByLevels,\n reachableFrom,\n} from './flowGraph';\nimport type { IFlowEdge, IFlowGraph, IFlowNode, IFlowPosition, TFlowPortId } from './flowTypes';\n\nconst HISTORY_LIMIT = 50;\nconst PASTE_OFFSET = 32;\n/** Edits to the same field within this window share one undo step (typing a name is one change). */\nconst COALESCE_WINDOW_MS = 1000;\n\nexport interface IFlowEditorState {\n graph: IFlowGraph;\n selectedNodeIds: Array<string>;\n selectedEdgeIds: Array<string>;\n past: Array<IFlowGraph>;\n future: Array<IFlowGraph>;\n clipboard: Array<IFlowNode>;\n isDirty: boolean;\n\n replaceGraph: (graph: IFlowGraph, options?: { keepHistory?: boolean }) => void;\n markClean: () => void;\n /** Snapshot before a gesture that changes the graph in many small steps (a drag). */\n recordHistory: () => void;\n addNode: (type: string) => string | null;\n /** Adds a block right of `sourceNodeId` and wires `sourcePort` to it in one undo step. */\n addNodeFromPort: (type: string, sourceNodeId: string, sourcePort: TFlowPortId) => string | null;\n /**\n * Puts a block in the middle of an edge: the edge now ends at the new block, whose first output\n * leads to the old target. The old target and everything after it move one column right, so the\n * new block takes the place it left. One undo step.\n */\n insertNodeOnEdge: (type: string, edgeId: string) => string | null;\n updateNodeConfig: (nodeId: string, config: unknown) => void;\n updateNodeLabel: (nodeId: string, label: string) => void;\n moveNodes: (positions: Record<string, IFlowPosition>) => void;\n connect: (source: string, sourcePort: TFlowPortId, target: string) => boolean;\n removeSelected: () => void;\n duplicateSelected: () => void;\n copySelected: () => void;\n paste: () => void;\n setSelection: (selection: { nodeIds?: Array<string>; edgeIds?: Array<string> }) => void;\n undo: () => void;\n redo: () => void;\n /** The host may rebuild its registry (labels after a language switch); gestures always read the latest. */\n setRegistry: (registry: IFlowBlockRegistry) => void;\n}\n\nexport type TFlowEditorStore = StoreApi<IFlowEditorState>;\n\nconst isPosition = (position: unknown): position is IFlowPosition =>\n typeof (position as IFlowPosition | undefined)?.x === 'number' &&\n typeof (position as IFlowPosition | undefined)?.y === 'number';\n\n/** Coerces a stored graph; a block saved without a position is placed by the automatic layout. */\nconst normalizeGraph = (graph: IFlowGraph | null | undefined, registry: IFlowBlockRegistry): IFlowGraph => {\n const normalized: IFlowGraph = {\n nodes: Array.isArray(graph?.nodes) ? graph.nodes : [],\n edges: Array.isArray(graph?.edges) ? graph.edges : [],\n };\n if (normalized.nodes.every((node) => isPosition(node.position))) return normalized;\n\n const layout = layoutFlowByLevels(normalized, registry);\n return {\n ...normalized,\n nodes: normalized.nodes.map((node) =>\n isPosition(node.position) ? node : { ...node, position: layout[node.nodeId] ?? { x: 0, y: 0 } },\n ),\n };\n};\n\nconst withoutNodes = (graph: IFlowGraph, nodeIds: Set<string>): IFlowGraph => ({\n nodes: graph.nodes.filter((node) => !nodeIds.has(node.nodeId)),\n edges: graph.edges.filter((edge) => !nodeIds.has(edge.source) && !nodeIds.has(edge.target)),\n});\n\nconst isPositionTaken = (graph: IFlowGraph, position: IFlowPosition): boolean =>\n graph.nodes.some(\n (node) =>\n Math.abs(node.position.x - position.x) < FLOW_LEVEL_GAP &&\n Math.abs(node.position.y - position.y) < FLOW_SIBLING_GAP,\n );\n\n/** Right of the selected block (or the rightmost one), stepping down the column until the slot is free. */\nconst nextFreePosition = (graph: IFlowGraph, selectedNodeIds: Array<string>): IFlowPosition => {\n if (graph.nodes.length === 0) return { x: 0, y: 0 };\n const anchor =\n graph.nodes.find((node) => node.nodeId === selectedNodeIds[0]) ??\n graph.nodes.reduce((current, node) => (node.position.x > current.position.x ? node : current));\n let candidate = { x: anchor.position.x + FLOW_LEVEL_GAP, y: anchor.position.y };\n while (isPositionTaken(graph, candidate)) candidate = { x: candidate.x, y: candidate.y + FLOW_SIBLING_GAP };\n return candidate;\n};\n\n/**\n * One store per editor instance (two canvases on a page never share state). The graph\n * is the source of truth; the canvas only renders it and reports gestures back.\n */\nexport const createFlowEditorStore = (initialGraph: IFlowGraph, registry: IFlowBlockRegistry): TFlowEditorStore =>\n createStore<IFlowEditorState>()((set, get) => {\n let currentRegistry = registry;\n let lastCoalesce: { key: string; at: number } | null = null;\n\n const commit = (\n next: IFlowGraph,\n selection?: { nodeIds?: Array<string>; edgeIds?: Array<string> },\n coalesceKey?: string,\n ) => {\n const { graph, past } = get();\n const now = Date.now();\n const extendsLastStep =\n coalesceKey !== undefined && lastCoalesce?.key === coalesceKey && now - lastCoalesce.at < COALESCE_WINDOW_MS;\n lastCoalesce = coalesceKey === undefined ? null : { key: coalesceKey, at: now };\n set({\n graph: next,\n past: extendsLastStep ? past : [...past, graph].slice(-HISTORY_LIMIT),\n future: [],\n isDirty: true,\n ...(selection?.nodeIds ? { selectedNodeIds: selection.nodeIds } : {}),\n ...(selection?.edgeIds ? { selectedEdgeIds: selection.edgeIds } : {}),\n });\n };\n\n const cloneNodes = (nodes: Array<IFlowNode>, offset: number): Array<IFlowNode> => {\n const instancesByType = new Map<string, number>();\n get().graph.nodes.forEach((node) => instancesByType.set(node.type, (instancesByType.get(node.type) ?? 0) + 1));\n return nodes\n .filter((node) => {\n const definition = currentRegistry.get(node.type);\n if (!definition || definition.category === 'trigger') return false;\n const instances = instancesByType.get(node.type) ?? 0;\n if (definition.maxInstances !== undefined && instances >= definition.maxInstances) return false;\n instancesByType.set(node.type, instances + 1);\n return true;\n })\n .map((node) => ({\n ...node,\n nodeId: generateUUID(),\n config: structuredClone(node.config),\n position: { x: node.position.x + offset, y: node.position.y + offset },\n }));\n };\n\n return {\n graph: normalizeGraph(initialGraph, registry),\n selectedNodeIds: [],\n selectedEdgeIds: [],\n past: [],\n future: [],\n clipboard: [],\n isDirty: false,\n\n replaceGraph: (graph, options) => {\n set((state) => ({\n graph: normalizeGraph(graph, currentRegistry),\n past: options?.keepHistory ? [...state.past, state.graph].slice(-HISTORY_LIMIT) : [],\n future: [],\n selectedNodeIds: [],\n selectedEdgeIds: [],\n isDirty: Boolean(options?.keepHistory),\n }));\n },\n\n markClean: () => set({ isDirty: false }),\n\n recordHistory: () => {\n const { graph, past } = get();\n set({ past: [...past, graph].slice(-HISTORY_LIMIT), future: [] });\n },\n\n addNode: (type) => {\n const definition = currentRegistry.get(type);\n if (!definition) return null;\n\n const { graph, selectedNodeIds } = get();\n const instances = graph.nodes.filter((node) => node.type === type).length;\n if (definition.maxInstances !== undefined && instances >= definition.maxInstances) return null;\n\n const node: IFlowNode = {\n nodeId: generateUUID(),\n type,\n typeVersion: definition.typeVersion,\n config: definition.createDefaultConfig(),\n position: nextFreePosition(graph, selectedNodeIds),\n };\n commit({ ...graph, nodes: [...graph.nodes, node] }, { nodeIds: [node.nodeId], edgeIds: [] });\n return node.nodeId;\n },\n\n addNodeFromPort: (type, sourceNodeId, sourcePort) => {\n const definition = currentRegistry.get(type);\n const { graph } = get();\n const source = graph.nodes.find((node) => node.nodeId === sourceNodeId);\n if (!definition || !source) return null;\n\n const instances = graph.nodes.filter((node) => node.type === type).length;\n if (definition.maxInstances !== undefined && instances >= definition.maxInstances) return null;\n if (graph.edges.some((edge) => edge.source === sourceNodeId && edge.sourcePort === sourcePort)) return null;\n\n const outputs = flowOutputsOf(currentRegistry.get(source.type), source.config);\n if (!outputs.includes(sourcePort)) return null;\n const portIndex = outputs.indexOf(sourcePort);\n let position = {\n x: source.position.x + FLOW_LEVEL_GAP,\n y: source.position.y + (portIndex - (outputs.length - 1) / 2) * FLOW_SIBLING_GAP,\n };\n while (isPositionTaken(graph, position)) position = { x: position.x, y: position.y + FLOW_SIBLING_GAP };\n\n const node: IFlowNode = {\n nodeId: generateUUID(),\n type,\n typeVersion: definition.typeVersion,\n config: definition.createDefaultConfig(),\n position,\n };\n const nodes = [...graph.nodes, node];\n const wires = canConnect({ ...graph, nodes }, currentRegistry, {\n source: sourceNodeId,\n sourcePort,\n target: node.nodeId,\n });\n const edges = wires\n ? [...graph.edges, { edgeId: generateUUID(), source: sourceNodeId, sourcePort, target: node.nodeId }]\n : graph.edges;\n commit({ nodes, edges }, { nodeIds: [node.nodeId], edgeIds: [] });\n return node.nodeId;\n },\n\n insertNodeOnEdge: (type, edgeId) => {\n const definition = currentRegistry.get(type);\n const { graph } = get();\n const edge = graph.edges.find((candidate) => candidate.edgeId === edgeId);\n const target = edge ? graph.nodes.find((node) => node.nodeId === edge.target) : undefined;\n const config = definition?.createDefaultConfig();\n const continuationPort = definition ? flowOutputsOf(definition, config)[0] : undefined;\n if (!definition || !edge || !target || !continuationPort || definition.category === 'trigger') return null;\n\n const instances = graph.nodes.filter((node) => node.type === type).length;\n if (definition.maxInstances !== undefined && instances >= definition.maxInstances) return null;\n\n const shifted = reachableFrom(graph, target.nodeId);\n const node: IFlowNode = {\n nodeId: generateUUID(),\n type,\n typeVersion: definition.typeVersion,\n config,\n position: { ...target.position },\n };\n const nodes = [\n ...graph.nodes.map((current) =>\n shifted.has(current.nodeId)\n ? { ...current, position: { x: current.position.x + FLOW_LEVEL_GAP, y: current.position.y } }\n : current,\n ),\n node,\n ];\n const withoutEdge = graph.edges.filter((current) => current.edgeId !== edgeId);\n const incoming: IFlowEdge = { ...edge, target: node.nodeId };\n const outgoing: IFlowEdge = {\n edgeId: generateUUID(),\n source: node.nodeId,\n sourcePort: continuationPort,\n target: target.nodeId,\n };\n\n const wiresIn = canConnect({ nodes, edges: withoutEdge }, currentRegistry, incoming);\n const wiresOut = canConnect({ nodes, edges: [...withoutEdge, incoming] }, currentRegistry, outgoing);\n if (!wiresIn || !wiresOut) return null;\n\n commit({ nodes, edges: [...withoutEdge, incoming, outgoing] }, { nodeIds: [node.nodeId], edgeIds: [] });\n return node.nodeId;\n },\n\n updateNodeConfig: (nodeId, config) => {\n const { graph } = get();\n const node = graph.nodes.find((candidate) => candidate.nodeId === nodeId);\n const outputs = node ? flowOutputsOf(currentRegistry.get(node.type), config) : [];\n commit(\n {\n nodes: graph.nodes.map((current) => (current.nodeId === nodeId ? { ...current, config } : current)),\n edges: graph.edges.filter((edge) => edge.source !== nodeId || outputs.includes(edge.sourcePort)),\n },\n undefined,\n `config:${nodeId}`,\n );\n },\n\n updateNodeLabel: (nodeId, label) => {\n const { graph } = get();\n const hasText = label.trim().length > 0;\n commit(\n {\n ...graph,\n nodes: graph.nodes.map((node) =>\n node.nodeId === nodeId ? { ...node, label: hasText ? label : undefined } : node,\n ),\n },\n undefined,\n `label:${nodeId}`,\n );\n },\n\n moveNodes: (positions) => {\n const { graph } = get();\n set({\n graph: {\n ...graph,\n nodes: graph.nodes.map((node) =>\n positions[node.nodeId] ? { ...node, position: positions[node.nodeId] } : node,\n ),\n },\n isDirty: true,\n });\n },\n\n connect: (source, sourcePort, target) => {\n const { graph } = get();\n if (!canConnect(graph, currentRegistry, { source, sourcePort, target })) return false;\n const edge: IFlowEdge = { edgeId: generateUUID(), source, sourcePort, target };\n commit({ ...graph, edges: [...graph.edges, edge] });\n return true;\n },\n\n removeSelected: () => {\n const { graph, selectedNodeIds, selectedEdgeIds } = get();\n const removableNodeIds = graph.nodes\n .filter((node) => selectedNodeIds.includes(node.nodeId) && isFlowNodeRemovable(node, currentRegistry))\n .map((node) => node.nodeId);\n if (removableNodeIds.length === 0 && selectedEdgeIds.length === 0) return;\n const removedEdges = new Set(selectedEdgeIds);\n const next = withoutNodes(\n { ...graph, edges: graph.edges.filter((edge) => !removedEdges.has(edge.edgeId)) },\n new Set(removableNodeIds),\n );\n commit(next, { nodeIds: [], edgeIds: [] });\n },\n\n duplicateSelected: () => {\n const { graph, selectedNodeIds } = get();\n const selected = graph.nodes.filter((node) => selectedNodeIds.includes(node.nodeId));\n const copies = cloneNodes(selected, PASTE_OFFSET);\n if (copies.length === 0) return;\n commit(\n { ...graph, nodes: [...graph.nodes, ...copies] },\n { nodeIds: copies.map((node) => node.nodeId), edgeIds: [] },\n );\n },\n\n copySelected: () => {\n const { graph, selectedNodeIds } = get();\n set({ clipboard: graph.nodes.filter((node) => selectedNodeIds.includes(node.nodeId)) });\n },\n\n paste: () => {\n const { graph, clipboard } = get();\n const copies = cloneNodes(clipboard, PASTE_OFFSET);\n if (copies.length === 0) return;\n commit(\n { ...graph, nodes: [...graph.nodes, ...copies] },\n { nodeIds: copies.map((node) => node.nodeId), edgeIds: [] },\n );\n set({ clipboard: copies });\n },\n\n setSelection: ({ nodeIds, edgeIds }) => {\n set((state) => ({\n selectedNodeIds: nodeIds ?? state.selectedNodeIds,\n selectedEdgeIds: edgeIds ?? state.selectedEdgeIds,\n }));\n },\n\n undo: () => {\n const { graph, past, future } = get();\n const previous = past[past.length - 1];\n if (!previous) return;\n set({\n graph: previous,\n past: past.slice(0, -1),\n future: [graph, ...future].slice(0, HISTORY_LIMIT),\n selectedNodeIds: [],\n selectedEdgeIds: [],\n isDirty: true,\n });\n },\n\n redo: () => {\n const { graph, past, future } = get();\n const following = future[0];\n if (!following) return;\n set({\n graph: following,\n past: [...past, graph].slice(-HISTORY_LIMIT),\n future: future.slice(1),\n selectedNodeIds: [],\n selectedEdgeIds: [],\n isDirty: true,\n });\n },\n\n setRegistry: (nextRegistry) => {\n currentRegistry = nextRegistry;\n },\n };\n });\n"],"names":["node"],"mappings":"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"}
@@ -4,6 +4,7 @@ import { IFlowGraph, IFlowIssue, IFlowNode, IFlowPosition, TFlowPortId } from '.
4
4
  export declare const FLOW_LEVEL_GAP = 336;
5
5
  export declare const FLOW_SIBLING_GAP = 176;
6
6
  export declare const triggerNodesOf: (graph: IFlowGraph, registry: IFlowBlockRegistry) => Array<IFlowNode>;
7
+ export declare const isFlowNodeRemovable: (node: IFlowNode, registry: IFlowBlockRegistry) => boolean;
7
8
  /** Node ids reachable from `startNodeId` following edges, the start included. */
8
9
  export declare const reachableFrom: (graph: IFlowGraph, startNodeId: string) => Set<string>;
9
10
  /** The node ids of the first cycle found, in order, or null when the graph has none. */
@@ -11,6 +11,7 @@ const outgoingByNode = (graph) => {
11
11
  return outgoing;
12
12
  };
13
13
  const triggerNodesOf = (graph, registry) => nodesOf(graph).filter((node) => registry.get(node.type)?.category === "trigger");
14
+ const isFlowNodeRemovable = (node, registry) => registry.get(node.type)?.removable !== false;
14
15
  const reachableFrom = (graph, startNodeId) => {
15
16
  const outgoing = outgoingByNode(graph);
16
17
  const visited = /* @__PURE__ */ new Set();
@@ -147,6 +148,7 @@ export {
147
148
  canConnect,
148
149
  findCycle,
149
150
  hasBlockingIssues,
151
+ isFlowNodeRemovable,
150
152
  layoutFlowByLevels,
151
153
  reachableFrom,
152
154
  triggerNodesOf,
@@ -1 +1 @@
1
- {"version":3,"file":"flowGraph.js","sources":["../../../../src/components/UI/Flow/flowGraph.ts"],"sourcesContent":["import { flowOutputsOf, type IFlowBlockRegistry } from './flowBlockRegistry';\nimport type { IFlowEdge, IFlowGraph, IFlowIssue, IFlowNode, IFlowPosition, TFlowPortId } from './flowTypes';\n\n/** The flow reads left to right: one level per column, siblings stacked in the column. */\nexport const FLOW_LEVEL_GAP = 336;\nexport const FLOW_SIBLING_GAP = 176;\n\nconst nodesOf = (graph: IFlowGraph): Array<IFlowNode> => (Array.isArray(graph?.nodes) ? graph.nodes : []);\nconst edgesOf = (graph: IFlowGraph): Array<IFlowEdge> => (Array.isArray(graph?.edges) ? graph.edges : []);\n\nconst outgoingByNode = (graph: IFlowGraph): Map<string, Array<IFlowEdge>> => {\n const outgoing = new Map<string, Array<IFlowEdge>>();\n for (const edge of edgesOf(graph)) {\n outgoing.set(edge.source, [...(outgoing.get(edge.source) ?? []), edge]);\n }\n return outgoing;\n};\n\nexport const triggerNodesOf = (graph: IFlowGraph, registry: IFlowBlockRegistry): Array<IFlowNode> =>\n nodesOf(graph).filter((node) => registry.get(node.type)?.category === 'trigger');\n\n/** Node ids reachable from `startNodeId` following edges, the start included. */\nexport const reachableFrom = (graph: IFlowGraph, startNodeId: string): Set<string> => {\n const outgoing = outgoingByNode(graph);\n const visited = new Set<string>();\n const pending = [startNodeId];\n while (pending.length > 0) {\n const nodeId = pending.pop();\n if (nodeId === undefined || visited.has(nodeId)) continue;\n visited.add(nodeId);\n for (const edge of outgoing.get(nodeId) ?? []) pending.push(edge.target);\n }\n return visited;\n};\n\n/** The node ids of the first cycle found, in order, or null when the graph has none. */\nexport const findCycle = (graph: IFlowGraph): Array<string> | null => {\n const outgoing = outgoingByNode(graph);\n const state = new Map<string, 'visiting' | 'done'>();\n const path: Array<string> = [];\n\n const visit = (nodeId: string): Array<string> | null => {\n state.set(nodeId, 'visiting');\n path.push(nodeId);\n for (const edge of outgoing.get(nodeId) ?? []) {\n const targetState = state.get(edge.target);\n if (targetState === 'visiting') return path.slice(path.indexOf(edge.target));\n if (targetState === undefined) {\n const cycle = visit(edge.target);\n if (cycle) return cycle;\n }\n }\n path.pop();\n state.set(nodeId, 'done');\n return null;\n };\n\n for (const node of nodesOf(graph)) {\n if (state.has(node.nodeId)) continue;\n const cycle = visit(node.nodeId);\n if (cycle) return cycle;\n }\n return null;\n};\n\nexport interface IFlowConnectionCandidate {\n source: string;\n sourcePort: TFlowPortId;\n target: string;\n}\n\n/**\n * The V0 shape is a tree with no fan-out: one input per block, one edge per output\n * port, no edge into a trigger and no cycle. Keeping this rule here lets the canvas\n * refuse a bad connection while it is being drawn, not only when publishing.\n */\nexport const canConnect = (\n graph: IFlowGraph,\n registry: IFlowBlockRegistry,\n candidate: IFlowConnectionCandidate,\n): boolean => {\n const { source, sourcePort, target } = candidate;\n if (!source || !target || source === target) return false;\n\n const nodes = nodesOf(graph);\n const sourceNode = nodes.find((node) => node.nodeId === source);\n const targetNode = nodes.find((node) => node.nodeId === target);\n if (!sourceNode || !targetNode) return false;\n\n const sourceDefinition = registry.get(sourceNode.type);\n const targetDefinition = registry.get(targetNode.type);\n if (!flowOutputsOf(sourceDefinition, sourceNode.config).includes(sourcePort)) return false;\n if (!targetDefinition || targetDefinition.category === 'trigger') return false;\n\n const edges = edgesOf(graph);\n if (edges.some((edge) => edge.source === source && edge.sourcePort === sourcePort)) return false;\n if (edges.some((edge) => edge.target === target)) return false;\n\n return !reachableFrom(graph, target).has(source);\n};\n\n/** The structural rules of the V0 shape, plus each block's own `validate`. */\nexport const validateFlowGraph = (graph: IFlowGraph, registry: IFlowBlockRegistry): Array<IFlowIssue> => {\n const issues: Array<IFlowIssue> = [];\n const nodes = nodesOf(graph);\n const edges = edgesOf(graph);\n\n for (const node of nodes) {\n const definition = registry.get(node.type);\n if (!definition) {\n issues.push({ code: 'unknownBlock', severity: 'error', nodeId: node.nodeId });\n continue;\n }\n for (const blockIssue of definition.validate(node.config) ?? []) {\n issues.push({ ...blockIssue, nodeId: node.nodeId });\n }\n }\n\n const triggers = triggerNodesOf(graph, registry);\n if (triggers.length === 0) issues.push({ code: 'missingTrigger', severity: 'error' });\n triggers.slice(1).forEach((trigger) => {\n issues.push({ code: 'multipleTriggers', severity: 'error', nodeId: trigger.nodeId });\n });\n\n const firstTrigger = triggers[0];\n if (firstTrigger) {\n const reachable = reachableFrom(graph, firstTrigger.nodeId);\n nodes\n .filter((node) => !reachable.has(node.nodeId) && registry.get(node.type)?.category !== 'trigger')\n .forEach((node) => issues.push({ code: 'unreachableNode', severity: 'error', nodeId: node.nodeId }));\n }\n\n const inputCount = new Map<string, number>();\n const portCount = new Map<string, number>();\n for (const edge of edges) {\n inputCount.set(edge.target, (inputCount.get(edge.target) ?? 0) + 1);\n const portKey = `${edge.source}|${edge.sourcePort}`;\n portCount.set(portKey, (portCount.get(portKey) ?? 0) + 1);\n }\n inputCount.forEach((count, nodeId) => {\n if (count > 1) issues.push({ code: 'multipleInputs', severity: 'error', nodeId });\n });\n edges\n .filter((edge) => (portCount.get(`${edge.source}|${edge.sourcePort}`) ?? 0) > 1)\n .forEach((edge) =>\n issues.push({ code: 'portFanOut', severity: 'error', edgeId: edge.edgeId, nodeId: edge.source }),\n );\n\n const cycle = findCycle(graph);\n if (cycle) cycle.forEach((nodeId) => issues.push({ code: 'cycle', severity: 'error', nodeId }));\n\n nodes\n .filter((node) => flowOutputsOf(registry.get(node.type), node.config).includes('true'))\n .filter((node) => !edges.some((edge) => edge.source === node.nodeId && edge.sourcePort === 'true'))\n .forEach((node) => issues.push({ code: 'missingTrueBranch', severity: 'warning', nodeId: node.nodeId }));\n\n return issues;\n};\n\nexport const hasBlockingIssues = (issues: Array<IFlowIssue>): boolean =>\n (issues ?? []).some((issue) => issue.severity === 'error');\n\n/**\n * Lays the tree out by levels from the trigger: each level one column to the right,\n * siblings stacked in the column. Used only for a flow with no saved positions (one born from a rule) or when\n * the user asks for it; a position the user arranged is data and is never overwritten\n * implicitly.\n */\nexport const layoutFlowByLevels = (graph: IFlowGraph, registry: IFlowBlockRegistry): Record<string, IFlowPosition> => {\n const nodes = nodesOf(graph);\n const outgoing = outgoingByNode(graph);\n const levelOf = new Map<string, number>();\n const roots = triggerNodesOf(graph, registry);\n const queue: Array<string> = roots.map((root) => root.nodeId);\n roots.forEach((root) => levelOf.set(root.nodeId, 0));\n\n while (queue.length > 0) {\n const nodeId = queue.shift();\n if (nodeId === undefined) continue;\n const level = levelOf.get(nodeId) ?? 0;\n for (const edge of outgoing.get(nodeId) ?? []) {\n if (levelOf.has(edge.target)) continue;\n levelOf.set(edge.target, level + 1);\n queue.push(edge.target);\n }\n }\n\n const deepestLevel = Math.max(-1, ...Array.from(levelOf.values()));\n nodes.filter((node) => !levelOf.has(node.nodeId)).forEach((node) => levelOf.set(node.nodeId, deepestLevel + 1));\n\n const nodesByLevel = new Map<number, Array<string>>();\n nodes.forEach((node) => {\n const level = levelOf.get(node.nodeId) ?? 0;\n nodesByLevel.set(level, [...(nodesByLevel.get(level) ?? []), node.nodeId]);\n });\n\n const positions: Record<string, IFlowPosition> = {};\n nodesByLevel.forEach((nodeIds, level) => {\n const columnHeight = (nodeIds.length - 1) * FLOW_SIBLING_GAP;\n nodeIds.forEach((nodeId, index) => {\n positions[nodeId] = { x: level * FLOW_LEVEL_GAP, y: index * FLOW_SIBLING_GAP - columnHeight / 2 };\n });\n });\n return positions;\n};\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"flowGraph.js","sources":["../../../../src/components/UI/Flow/flowGraph.ts"],"sourcesContent":["import { flowOutputsOf, type IFlowBlockRegistry } from './flowBlockRegistry';\nimport type { IFlowEdge, IFlowGraph, IFlowIssue, IFlowNode, IFlowPosition, TFlowPortId } from './flowTypes';\n\n/** The flow reads left to right: one level per column, siblings stacked in the column. */\nexport const FLOW_LEVEL_GAP = 336;\nexport const FLOW_SIBLING_GAP = 176;\n\nconst nodesOf = (graph: IFlowGraph): Array<IFlowNode> => (Array.isArray(graph?.nodes) ? graph.nodes : []);\nconst edgesOf = (graph: IFlowGraph): Array<IFlowEdge> => (Array.isArray(graph?.edges) ? graph.edges : []);\n\nconst outgoingByNode = (graph: IFlowGraph): Map<string, Array<IFlowEdge>> => {\n const outgoing = new Map<string, Array<IFlowEdge>>();\n for (const edge of edgesOf(graph)) {\n outgoing.set(edge.source, [...(outgoing.get(edge.source) ?? []), edge]);\n }\n return outgoing;\n};\n\nexport const triggerNodesOf = (graph: IFlowGraph, registry: IFlowBlockRegistry): Array<IFlowNode> =>\n nodesOf(graph).filter((node) => registry.get(node.type)?.category === 'trigger');\n\nexport const isFlowNodeRemovable = (node: IFlowNode, registry: IFlowBlockRegistry): boolean =>\n registry.get(node.type)?.removable !== false;\n\n/** Node ids reachable from `startNodeId` following edges, the start included. */\nexport const reachableFrom = (graph: IFlowGraph, startNodeId: string): Set<string> => {\n const outgoing = outgoingByNode(graph);\n const visited = new Set<string>();\n const pending = [startNodeId];\n while (pending.length > 0) {\n const nodeId = pending.pop();\n if (nodeId === undefined || visited.has(nodeId)) continue;\n visited.add(nodeId);\n for (const edge of outgoing.get(nodeId) ?? []) pending.push(edge.target);\n }\n return visited;\n};\n\n/** The node ids of the first cycle found, in order, or null when the graph has none. */\nexport const findCycle = (graph: IFlowGraph): Array<string> | null => {\n const outgoing = outgoingByNode(graph);\n const state = new Map<string, 'visiting' | 'done'>();\n const path: Array<string> = [];\n\n const visit = (nodeId: string): Array<string> | null => {\n state.set(nodeId, 'visiting');\n path.push(nodeId);\n for (const edge of outgoing.get(nodeId) ?? []) {\n const targetState = state.get(edge.target);\n if (targetState === 'visiting') return path.slice(path.indexOf(edge.target));\n if (targetState === undefined) {\n const cycle = visit(edge.target);\n if (cycle) return cycle;\n }\n }\n path.pop();\n state.set(nodeId, 'done');\n return null;\n };\n\n for (const node of nodesOf(graph)) {\n if (state.has(node.nodeId)) continue;\n const cycle = visit(node.nodeId);\n if (cycle) return cycle;\n }\n return null;\n};\n\nexport interface IFlowConnectionCandidate {\n source: string;\n sourcePort: TFlowPortId;\n target: string;\n}\n\n/**\n * The V0 shape is a tree with no fan-out: one input per block, one edge per output\n * port, no edge into a trigger and no cycle. Keeping this rule here lets the canvas\n * refuse a bad connection while it is being drawn, not only when publishing.\n */\nexport const canConnect = (\n graph: IFlowGraph,\n registry: IFlowBlockRegistry,\n candidate: IFlowConnectionCandidate,\n): boolean => {\n const { source, sourcePort, target } = candidate;\n if (!source || !target || source === target) return false;\n\n const nodes = nodesOf(graph);\n const sourceNode = nodes.find((node) => node.nodeId === source);\n const targetNode = nodes.find((node) => node.nodeId === target);\n if (!sourceNode || !targetNode) return false;\n\n const sourceDefinition = registry.get(sourceNode.type);\n const targetDefinition = registry.get(targetNode.type);\n if (!flowOutputsOf(sourceDefinition, sourceNode.config).includes(sourcePort)) return false;\n if (!targetDefinition || targetDefinition.category === 'trigger') return false;\n\n const edges = edgesOf(graph);\n if (edges.some((edge) => edge.source === source && edge.sourcePort === sourcePort)) return false;\n if (edges.some((edge) => edge.target === target)) return false;\n\n return !reachableFrom(graph, target).has(source);\n};\n\n/** The structural rules of the V0 shape, plus each block's own `validate`. */\nexport const validateFlowGraph = (graph: IFlowGraph, registry: IFlowBlockRegistry): Array<IFlowIssue> => {\n const issues: Array<IFlowIssue> = [];\n const nodes = nodesOf(graph);\n const edges = edgesOf(graph);\n\n for (const node of nodes) {\n const definition = registry.get(node.type);\n if (!definition) {\n issues.push({ code: 'unknownBlock', severity: 'error', nodeId: node.nodeId });\n continue;\n }\n for (const blockIssue of definition.validate(node.config) ?? []) {\n issues.push({ ...blockIssue, nodeId: node.nodeId });\n }\n }\n\n const triggers = triggerNodesOf(graph, registry);\n if (triggers.length === 0) issues.push({ code: 'missingTrigger', severity: 'error' });\n triggers.slice(1).forEach((trigger) => {\n issues.push({ code: 'multipleTriggers', severity: 'error', nodeId: trigger.nodeId });\n });\n\n const firstTrigger = triggers[0];\n if (firstTrigger) {\n const reachable = reachableFrom(graph, firstTrigger.nodeId);\n nodes\n .filter((node) => !reachable.has(node.nodeId) && registry.get(node.type)?.category !== 'trigger')\n .forEach((node) => issues.push({ code: 'unreachableNode', severity: 'error', nodeId: node.nodeId }));\n }\n\n const inputCount = new Map<string, number>();\n const portCount = new Map<string, number>();\n for (const edge of edges) {\n inputCount.set(edge.target, (inputCount.get(edge.target) ?? 0) + 1);\n const portKey = `${edge.source}|${edge.sourcePort}`;\n portCount.set(portKey, (portCount.get(portKey) ?? 0) + 1);\n }\n inputCount.forEach((count, nodeId) => {\n if (count > 1) issues.push({ code: 'multipleInputs', severity: 'error', nodeId });\n });\n edges\n .filter((edge) => (portCount.get(`${edge.source}|${edge.sourcePort}`) ?? 0) > 1)\n .forEach((edge) =>\n issues.push({ code: 'portFanOut', severity: 'error', edgeId: edge.edgeId, nodeId: edge.source }),\n );\n\n const cycle = findCycle(graph);\n if (cycle) cycle.forEach((nodeId) => issues.push({ code: 'cycle', severity: 'error', nodeId }));\n\n nodes\n .filter((node) => flowOutputsOf(registry.get(node.type), node.config).includes('true'))\n .filter((node) => !edges.some((edge) => edge.source === node.nodeId && edge.sourcePort === 'true'))\n .forEach((node) => issues.push({ code: 'missingTrueBranch', severity: 'warning', nodeId: node.nodeId }));\n\n return issues;\n};\n\nexport const hasBlockingIssues = (issues: Array<IFlowIssue>): boolean =>\n (issues ?? []).some((issue) => issue.severity === 'error');\n\n/**\n * Lays the tree out by levels from the trigger: each level one column to the right,\n * siblings stacked in the column. Used only for a flow with no saved positions (one born from a rule) or when\n * the user asks for it; a position the user arranged is data and is never overwritten\n * implicitly.\n */\nexport const layoutFlowByLevels = (graph: IFlowGraph, registry: IFlowBlockRegistry): Record<string, IFlowPosition> => {\n const nodes = nodesOf(graph);\n const outgoing = outgoingByNode(graph);\n const levelOf = new Map<string, number>();\n const roots = triggerNodesOf(graph, registry);\n const queue: Array<string> = roots.map((root) => root.nodeId);\n roots.forEach((root) => levelOf.set(root.nodeId, 0));\n\n while (queue.length > 0) {\n const nodeId = queue.shift();\n if (nodeId === undefined) continue;\n const level = levelOf.get(nodeId) ?? 0;\n for (const edge of outgoing.get(nodeId) ?? []) {\n if (levelOf.has(edge.target)) continue;\n levelOf.set(edge.target, level + 1);\n queue.push(edge.target);\n }\n }\n\n const deepestLevel = Math.max(-1, ...Array.from(levelOf.values()));\n nodes.filter((node) => !levelOf.has(node.nodeId)).forEach((node) => levelOf.set(node.nodeId, deepestLevel + 1));\n\n const nodesByLevel = new Map<number, Array<string>>();\n nodes.forEach((node) => {\n const level = levelOf.get(node.nodeId) ?? 0;\n nodesByLevel.set(level, [...(nodesByLevel.get(level) ?? []), node.nodeId]);\n });\n\n const positions: Record<string, IFlowPosition> = {};\n nodesByLevel.forEach((nodeIds, level) => {\n const columnHeight = (nodeIds.length - 1) * FLOW_SIBLING_GAP;\n nodeIds.forEach((nodeId, index) => {\n positions[nodeId] = { x: level * FLOW_LEVEL_GAP, y: index * FLOW_SIBLING_GAP - columnHeight / 2 };\n });\n });\n return positions;\n};\n"],"names":[],"mappings":"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"}
@@ -61,6 +61,8 @@ export interface IFlowBlockDefinition<TConfig = unknown> {
61
61
  resolveOutputs?: (config: TConfig) => Array<TFlowPortId>;
62
62
  /** How many of this block a flow may hold; omit for no limit. */
63
63
  maxInstances?: number;
64
+ /** `false` keeps the block on the canvas: Delete and the inspector skip it. Defaults to `true`. */
65
+ removable?: boolean;
64
66
  createDefaultConfig: () => TConfig;
65
67
  ConfigPanel: ComponentType<IFlowBlockConfigPanelProps<TConfig>>;
66
68
  /** When one block stands for several kinds (chosen inside it), the icon and name of the chosen kind. */
@@ -2,7 +2,7 @@ export { createFlowBlockRegistry, defineFlowBlock, flowOutputsOf, type IFlowBloc
2
2
  export { default as FlowCanvas, type IFlowCanvasProps } from './FlowCanvas';
3
3
  export { default as FlowEditorProvider, useFlowBlockRegistry, useFlowEditor, useFlowEditorStore, } from './FlowEditorProvider';
4
4
  export { createFlowEditorStore, type IFlowEditorState, type TFlowEditorStore } from './flowEditorStore';
5
- export { canConnect, findCycle, FLOW_LEVEL_GAP, FLOW_SIBLING_GAP, hasBlockingIssues, layoutFlowByLevels, reachableFrom, triggerNodesOf, validateFlowGraph, } from './flowGraph';
5
+ export { canConnect, findCycle, FLOW_LEVEL_GAP, FLOW_SIBLING_GAP, hasBlockingIssues, isFlowNodeRemovable, layoutFlowByLevels, reachableFrom, triggerNodesOf, validateFlowGraph, } from './flowGraph';
6
6
  export { default as FlowInspector } from './FlowInspector';
7
7
  export { default as FlowIssueList } from './FlowIssueList';
8
8
  export { default as FlowLaneMap, type IFlowArc, type IFlowLane, type TFlowArcTone } from './FlowLaneMap';
@@ -2,7 +2,7 @@ import { createFlowBlockRegistry, defineFlowBlock, flowOutputsOf } from "./flowB
2
2
  import { default as default2 } from "./FlowCanvas.js";
3
3
  import { default as default3, useFlowBlockRegistry, useFlowEditor, useFlowEditorStore } from "./FlowEditorProvider.js";
4
4
  import { createFlowEditorStore } from "./flowEditorStore.js";
5
- import { FLOW_LEVEL_GAP, FLOW_SIBLING_GAP, canConnect, findCycle, hasBlockingIssues, layoutFlowByLevels, reachableFrom, triggerNodesOf, validateFlowGraph } from "./flowGraph.js";
5
+ import { FLOW_LEVEL_GAP, FLOW_SIBLING_GAP, canConnect, findCycle, hasBlockingIssues, isFlowNodeRemovable, layoutFlowByLevels, reachableFrom, triggerNodesOf, validateFlowGraph } from "./flowGraph.js";
6
6
  import { default as default4 } from "./FlowInspector.js";
7
7
  import { default as default5 } from "./FlowIssueList.js";
8
8
  import { default as default6 } from "./FlowLaneMap.js";
@@ -27,6 +27,7 @@ export {
27
27
  findCycle,
28
28
  flowOutputsOf,
29
29
  hasBlockingIssues,
30
+ isFlowNodeRemovable,
30
31
  layoutFlowByLevels,
31
32
  reachableFrom,
32
33
  triggerNodesOf,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umodeapporg/ui",
3
- "version": "0.24.0",
3
+ "version": "0.24.1",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "sideEffects": [