@umodeapporg/ui 0.30.1 → 0.31.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.
Files changed (42) hide show
  1. package/dist/assets/umode-ui.css +32 -0
  2. package/dist/components/UI/BulkActionBar.js +1 -1
  3. package/dist/components/UI/BulkActionBar.js.map +1 -1
  4. package/dist/components/UI/Flow/FlowAnnotationDraftPreview.d.ts +6 -0
  5. package/dist/components/UI/Flow/FlowAnnotationDraftPreview.js +65 -0
  6. package/dist/components/UI/Flow/FlowAnnotationDraftPreview.js.map +1 -0
  7. package/dist/components/UI/Flow/FlowAnnotationNode.d.ts +34 -0
  8. package/dist/components/UI/Flow/FlowAnnotationNode.js +231 -0
  9. package/dist/components/UI/Flow/FlowAnnotationNode.js.map +1 -0
  10. package/dist/components/UI/Flow/FlowAnnotationToolbar.d.ts +32 -0
  11. package/dist/components/UI/Flow/FlowAnnotationToolbar.js +298 -0
  12. package/dist/components/UI/Flow/FlowAnnotationToolbar.js.map +1 -0
  13. package/dist/components/UI/Flow/FlowCanvas.d.ts +2 -0
  14. package/dist/components/UI/Flow/FlowCanvas.js +278 -20
  15. package/dist/components/UI/Flow/FlowCanvas.js.map +1 -1
  16. package/dist/components/UI/Flow/FlowInspector.js +6 -5
  17. package/dist/components/UI/Flow/FlowInspector.js.map +1 -1
  18. package/dist/components/UI/Flow/FlowSelectionBar.d.ts +10 -0
  19. package/dist/components/UI/Flow/FlowSelectionBar.js +56 -0
  20. package/dist/components/UI/Flow/FlowSelectionBar.js.map +1 -0
  21. package/dist/components/UI/Flow/flowAnnotations.d.ts +38 -0
  22. package/dist/components/UI/Flow/flowAnnotations.js +123 -0
  23. package/dist/components/UI/Flow/flowAnnotations.js.map +1 -0
  24. package/dist/components/UI/Flow/flowEditorStore.d.ts +29 -5
  25. package/dist/components/UI/Flow/flowEditorStore.js +123 -23
  26. package/dist/components/UI/Flow/flowEditorStore.js.map +1 -1
  27. package/dist/components/UI/Flow/flowTypes.d.ts +30 -0
  28. package/dist/components/UI/Flow/index.d.ts +2 -1
  29. package/dist/components/UI/Flow/index.js +5 -1
  30. package/dist/components/UI/Flow/index.js.map +1 -1
  31. package/dist/components/UI/Flow/useFlowAnnotationDrawing.d.ts +25 -0
  32. package/dist/components/UI/Flow/useFlowAnnotationDrawing.js +117 -0
  33. package/dist/components/UI/Flow/useFlowAnnotationDrawing.js.map +1 -0
  34. package/dist/locales/en/ui.json.d.ts +41 -0
  35. package/dist/locales/en/ui.json.js +1 -1
  36. package/dist/locales/index.d.ts +82 -0
  37. package/dist/locales/pt/ui.json.d.ts +41 -0
  38. package/dist/locales/pt/ui.json.js +1 -1
  39. package/package.json +1 -1
  40. package/src/assets/fonts/LICENSE-Virgil.txt +45 -0
  41. package/src/assets/fonts/virgil.woff2 +0 -0
  42. package/src/assets/styles/base.css +10 -0
@@ -1 +1 @@
1
- {"version":3,"file":"FlowInspector.js","sources":["../../../../src/components/UI/Flow/FlowInspector.tsx"],"sourcesContent":["import { useTranslation } from 'react-i18next';\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';\nimport { mergeClasses } from '@/helpers/mergeClasses';\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 = mergeClasses(\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 aria-label={t('flow.inspector.close')}\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 className=\"normal-case\"\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 className=\"normal-case\"\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
+ {"version":3,"file":"FlowInspector.js","sources":["../../../../src/components/UI/Flow/FlowInspector.tsx"],"sourcesContent":["import { useTranslation } from 'react-i18next';\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';\nimport { mergeClasses } from '@/helpers/mergeClasses';\n\nimport { useFlowBlockRegistry, useFlowEditor, useFlowEditorStore } from './FlowEditorProvider';\nimport { selectFlowInspectedNodeId } from './flowEditorStore';\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 selectedNode = useFlowEditor((state) => {\n const inspectedNodeId = selectFlowInspectedNodeId(state);\n return inspectedNodeId ? state.graph.nodes.find((node) => node.nodeId === inspectedNodeId) : undefined;\n });\n\n const shellClass = mergeClasses(\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) {\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 aria-label={t('flow.inspector.close')}\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 className=\"normal-case\"\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 className=\"normal-case\"\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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"}
@@ -0,0 +1,10 @@
1
+ interface IFlowSelectionBarProps {
2
+ selectedCount: number;
3
+ canDelete: boolean;
4
+ onDuplicate: () => void;
5
+ onDelete: () => void;
6
+ onClear: () => void;
7
+ }
8
+ /** What can be done with several selected items at once: blocks and annotations alike. */
9
+ declare const FlowSelectionBar: React.FC<IFlowSelectionBarProps>;
10
+ export default FlowSelectionBar;
@@ -0,0 +1,56 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { useTranslation } from "react-i18next";
3
+ import ControlledButton from "../../Form/ControlledButton.js";
4
+ import ControlledTooltip from "../ControlledTooltip.js";
5
+ import { MaterialSymbol } from "../MaterialSymbol/index.js";
6
+ const SelectionAction = ({ icon, label, onClick, disabled, danger }) => /* @__PURE__ */ jsxs(
7
+ ControlledButton,
8
+ {
9
+ defaultHtmlButton: true,
10
+ onClick,
11
+ disabled,
12
+ className: danger ? "interactive flex h-8 items-center gap-x-1.5 rounded-lg px-2.5 text-sm font-medium text-danger-emphasis hover:bg-danger-soft focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-40" : "interactive flex h-8 items-center gap-x-1.5 rounded-lg px-2.5 text-sm font-medium text-foreground-muted hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-40",
13
+ children: [
14
+ /* @__PURE__ */ jsx(MaterialSymbol, { icon, size: 18, "aria-hidden": true }),
15
+ /* @__PURE__ */ jsx("span", { children: label })
16
+ ]
17
+ }
18
+ );
19
+ const FlowSelectionBar = ({
20
+ selectedCount,
21
+ canDelete,
22
+ onDuplicate,
23
+ onDelete,
24
+ onClear
25
+ }) => {
26
+ const { t } = useTranslation("ui");
27
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-x-1 rounded-xl border border-border bg-surface p-1 shadow-md", children: [
28
+ /* @__PURE__ */ jsx("span", { className: "px-2 text-sm font-medium text-foreground", children: t("flow.selection.count", { count: selectedCount }) }),
29
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "h-5 w-px bg-border" }),
30
+ /* @__PURE__ */ jsx(SelectionAction, { icon: "content_copy", label: t("flow.selection.duplicate"), onClick: onDuplicate }),
31
+ /* @__PURE__ */ jsx(
32
+ SelectionAction,
33
+ {
34
+ icon: "delete",
35
+ label: t("flow.selection.delete"),
36
+ onClick: onDelete,
37
+ disabled: !canDelete,
38
+ danger: true
39
+ }
40
+ ),
41
+ /* @__PURE__ */ jsx(ControlledTooltip, { content: t("flow.selection.clear"), placement: "top", children: /* @__PURE__ */ jsx(
42
+ ControlledButton,
43
+ {
44
+ "aria-label": t("flow.selection.clear"),
45
+ defaultHtmlButton: true,
46
+ onClick: onClear,
47
+ className: "interactive flex size-8 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",
48
+ children: /* @__PURE__ */ jsx(MaterialSymbol, { icon: "close", size: 18, "aria-hidden": true })
49
+ }
50
+ ) })
51
+ ] });
52
+ };
53
+ export {
54
+ FlowSelectionBar as default
55
+ };
56
+ //# sourceMappingURL=FlowSelectionBar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FlowSelectionBar.js","sources":["../../../../src/components/UI/Flow/FlowSelectionBar.tsx"],"sourcesContent":["import { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { MaterialSymbol, SymbolCodepoints } from '@/components/UI/MaterialSymbol';\n\ninterface IFlowSelectionBarProps {\n selectedCount: number;\n canDelete: boolean;\n onDuplicate: () => void;\n onDelete: () => void;\n onClear: () => void;\n}\n\ninterface ISelectionActionProps {\n icon: SymbolCodepoints;\n label: string;\n onClick: () => void;\n disabled?: boolean;\n danger?: boolean;\n}\n\nconst SelectionAction: React.FC<ISelectionActionProps> = ({ icon, label, onClick, disabled, danger }) => (\n <ControlledButton\n defaultHtmlButton\n onClick={onClick}\n disabled={disabled}\n className={\n danger\n ? 'interactive flex h-8 items-center gap-x-1.5 rounded-lg px-2.5 text-sm font-medium text-danger-emphasis hover:bg-danger-soft focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-40'\n : 'interactive flex h-8 items-center gap-x-1.5 rounded-lg px-2.5 text-sm font-medium text-foreground-muted hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-40'\n }\n >\n <MaterialSymbol icon={icon} size={18} aria-hidden />\n <span>{label}</span>\n </ControlledButton>\n);\n\n/** What can be done with several selected items at once: blocks and annotations alike. */\nconst FlowSelectionBar: React.FC<IFlowSelectionBarProps> = ({\n selectedCount,\n canDelete,\n onDuplicate,\n onDelete,\n onClear,\n}) => {\n const { t } = useTranslation('ui');\n\n return (\n <div className=\"flex items-center gap-x-1 rounded-xl border border-border bg-surface p-1 shadow-md\">\n <span className=\"px-2 text-sm font-medium text-foreground\">\n {t('flow.selection.count', { count: selectedCount })}\n </span>\n <span aria-hidden className=\"h-5 w-px bg-border\" />\n <SelectionAction icon=\"content_copy\" label={t('flow.selection.duplicate')} onClick={onDuplicate} />\n <SelectionAction\n icon=\"delete\"\n label={t('flow.selection.delete')}\n onClick={onDelete}\n disabled={!canDelete}\n danger\n />\n <ControlledTooltip content={t('flow.selection.clear')} placement=\"top\">\n <ControlledButton\n aria-label={t('flow.selection.clear')}\n defaultHtmlButton\n onClick={onClear}\n className=\"interactive flex size-8 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};\n\nexport default FlowSelectionBar;\n"],"names":[],"mappings":";;;;;AAsBA,MAAM,kBAAmD,CAAC,EAAE,MAAM,OAAO,SAAS,UAAU,aAC1F;AAAA,EAAC;AAAA,EAAA;AAAA,IACC,mBAAiB;AAAA,IACjB;AAAA,IACA;AAAA,IACA,WACE,SACI,yPACA;AAAA,IAGN,UAAA;AAAA,MAAA,oBAAC,gBAAA,EAAe,MAAY,MAAM,IAAI,eAAW,MAAC;AAAA,MAClD,oBAAC,UAAM,UAAA,MAAA,CAAM;AAAA,IAAA;AAAA,EAAA;AACf;AAIF,MAAM,mBAAqD,CAAC;AAAA,EAC1D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,EAAE,EAAA,IAAM,eAAe,IAAI;AAEjC,SACE,qBAAC,OAAA,EAAI,WAAU,sFACb,UAAA;AAAA,IAAA,oBAAC,QAAA,EAAK,WAAU,4CACb,UAAA,EAAE,wBAAwB,EAAE,OAAO,cAAA,CAAe,EAAA,CACrD;AAAA,IACA,oBAAC,QAAA,EAAK,eAAW,MAAC,WAAU,sBAAqB;AAAA,IACjD,oBAAC,mBAAgB,MAAK,gBAAe,OAAO,EAAE,0BAA0B,GAAG,SAAS,aAAa;AAAA,IACjG;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,MAAK;AAAA,QACL,OAAO,EAAE,uBAAuB;AAAA,QAChC,SAAS;AAAA,QACT,UAAU,CAAC;AAAA,QACX,QAAM;AAAA,MAAA;AAAA,IAAA;AAAA,wBAEP,mBAAA,EAAkB,SAAS,EAAE,sBAAsB,GAAG,WAAU,OAC/D,UAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,cAAY,EAAE,sBAAsB;AAAA,QACpC,mBAAiB;AAAA,QACjB,SAAS;AAAA,QACT,WAAU;AAAA,QAEV,8BAAC,gBAAA,EAAe,MAAK,SAAQ,MAAM,IAAI,eAAW,KAAA,CAAC;AAAA,MAAA;AAAA,IAAA,EACrD,CACF;AAAA,EAAA,GACF;AAEJ;"}
@@ -0,0 +1,38 @@
1
+ import { IFlowAnnotation, IFlowPosition, IFlowSize, TFlowAnnotationKind, TFlowAnnotationStroke, TFlowAnnotationTone } from './flowTypes';
2
+ export declare const FLOW_ANNOTATION_KINDS: Array<TFlowAnnotationKind>;
3
+ export declare const FLOW_ANNOTATION_TONES: Array<TFlowAnnotationTone>;
4
+ export declare const FLOW_ANNOTATION_STROKES: Array<TFlowAnnotationStroke>;
5
+ export declare const FLOW_ANNOTATION_STROKE_WIDTH: Record<TFlowAnnotationStroke, number>;
6
+ /** A box never collapses to nothing: a zero-wide arrow or a dot still has to be grabbed. */
7
+ export declare const FLOW_ANNOTATION_MIN_SIZE = 8;
8
+ /** Puts the first line of a new note on the point that was clicked, not below it. */
9
+ export declare const FLOW_TEXT_CARET_OFFSET = 14;
10
+ /** Keeps a long scribble from bloating the saved flow. */
11
+ export declare const FLOW_DRAWING_MAX_POINTS = 2000;
12
+ export declare const strokeWidthOf: (annotation: Pick<IFlowAnnotation, "stroke">) => number;
13
+ /** The number a new step takes: one past the highest on the canvas. */
14
+ export declare const nextFlowStepNumber: (annotations: Array<IFlowAnnotation>) => number;
15
+ /**
16
+ * A freehand stroke as a smooth path: each recorded point becomes the control of a curve through the
17
+ * midpoints, which takes the jitter of the pointer out without moving the stroke.
18
+ */
19
+ export declare const smoothStrokePath: (points: Array<IFlowPosition>) => string;
20
+ /** Coerces what a host stored: an entry the canvas cannot draw is dropped, never rendered half-broken. */
21
+ export declare const normalizeFlowAnnotations: (annotations: unknown) => Array<IFlowAnnotation>;
22
+ /**
23
+ * Turns points in canvas coordinates into an annotation box: its position is the top-left corner
24
+ * (padded so the stroke is not clipped) and the points become relative to it.
25
+ */
26
+ export declare const boxAroundPoints: (points: Array<IFlowPosition>, padding: number) => {
27
+ position: IFlowPosition;
28
+ size: IFlowSize;
29
+ points: Array<IFlowPosition>;
30
+ };
31
+ /** A box spanning two corners dragged in any direction. */
32
+ export declare const boxBetween: (start: IFlowPosition, end: IFlowPosition) => {
33
+ position: IFlowPosition;
34
+ size: IFlowSize;
35
+ };
36
+ /** Scales a stroke with its box when the box is resized. */
37
+ export declare const scaleAnnotationPoints: (points: Array<IFlowPosition> | undefined, from: IFlowSize, to: IFlowSize) => Array<IFlowPosition> | undefined;
38
+ export declare const cloneFlowAnnotations: (annotations: Array<IFlowAnnotation>, offset: number) => Array<IFlowAnnotation>;
@@ -0,0 +1,123 @@
1
+ import { generateUUID } from "../../../helpers/utilityHelpers.js";
2
+ const FLOW_ANNOTATION_KINDS = [
3
+ "text",
4
+ "rectangle",
5
+ "ellipse",
6
+ "line",
7
+ "arrow",
8
+ "step",
9
+ "drawing"
10
+ ];
11
+ const FLOW_ANNOTATION_TONES = [
12
+ "danger",
13
+ "warning",
14
+ "success",
15
+ "info",
16
+ "brand",
17
+ "neutral"
18
+ ];
19
+ const FLOW_ANNOTATION_STROKES = ["thin", "medium", "thick"];
20
+ const FLOW_ANNOTATION_STROKE_WIDTH = {
21
+ thin: 2,
22
+ medium: 3.5,
23
+ thick: 6
24
+ };
25
+ const FLOW_ANNOTATION_MIN_SIZE = 8;
26
+ const FLOW_TEXT_CARET_OFFSET = 14;
27
+ const FLOW_DRAWING_MAX_POINTS = 2e3;
28
+ const isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
29
+ const isPoint = (value) => isFiniteNumber(value?.x) && isFiniteNumber(value?.y);
30
+ const strokeWidthOf = (annotation) => FLOW_ANNOTATION_STROKE_WIDTH[annotation.stroke ?? "medium"] ?? FLOW_ANNOTATION_STROKE_WIDTH.medium;
31
+ const nextFlowStepNumber = (annotations) => annotations.reduce((highest, annotation) => Math.max(highest, annotation.step ?? 0), 0) + 1;
32
+ const smoothStrokePath = (points) => {
33
+ const [first, ...rest] = points;
34
+ if (!first) return "";
35
+ if (rest.length === 0) return `M ${first.x} ${first.y} l 0 0`;
36
+ const curves = rest.map((point, index) => {
37
+ const previous = points[index] ?? first;
38
+ return `Q ${previous.x} ${previous.y} ${(previous.x + point.x) / 2} ${(previous.y + point.y) / 2}`;
39
+ });
40
+ const last = rest[rest.length - 1] ?? first;
41
+ return `M ${first.x} ${first.y} ${curves.join(" ")} L ${last.x} ${last.y}`;
42
+ };
43
+ const normalizeFlowAnnotations = (annotations) => {
44
+ if (!Array.isArray(annotations)) return [];
45
+ return annotations.flatMap((candidate) => {
46
+ const annotation = candidate;
47
+ if (!annotation || typeof annotation.annotationId !== "string" || !annotation.annotationId) return [];
48
+ if (!FLOW_ANNOTATION_KINDS.includes(annotation.kind)) return [];
49
+ if (!isPoint(annotation.position)) return [];
50
+ const width = isFiniteNumber(annotation.size?.width) ? annotation.size.width : FLOW_ANNOTATION_MIN_SIZE;
51
+ const height = isFiniteNumber(annotation.size?.height) ? annotation.size.height : FLOW_ANNOTATION_MIN_SIZE;
52
+ const points = Array.isArray(annotation.points) ? annotation.points.filter(isPoint) : void 0;
53
+ const needsTwoPoints = annotation.kind === "arrow" || annotation.kind === "line";
54
+ if (needsTwoPoints && (points?.length ?? 0) < 2 || annotation.kind === "drawing" && !points?.length) return [];
55
+ return [
56
+ {
57
+ annotationId: annotation.annotationId,
58
+ kind: annotation.kind,
59
+ position: { x: annotation.position.x, y: annotation.position.y },
60
+ size: {
61
+ width: Math.max(width, FLOW_ANNOTATION_MIN_SIZE),
62
+ height: Math.max(height, FLOW_ANNOTATION_MIN_SIZE)
63
+ },
64
+ tone: FLOW_ANNOTATION_TONES.includes(annotation.tone) ? annotation.tone : "neutral",
65
+ ...FLOW_ANNOTATION_STROKES.includes(annotation.stroke) ? { stroke: annotation.stroke } : {},
66
+ ...typeof annotation.text === "string" ? { text: annotation.text } : {},
67
+ ...isFiniteNumber(annotation.step) ? { step: annotation.step } : {},
68
+ ...points ? { points } : {}
69
+ }
70
+ ];
71
+ });
72
+ };
73
+ const boxAroundPoints = (points, padding) => {
74
+ const xs = points.map((point) => point.x);
75
+ const ys = points.map((point) => point.y);
76
+ const left = Math.min(...xs) - padding;
77
+ const top = Math.min(...ys) - padding;
78
+ return {
79
+ position: { x: left, y: top },
80
+ size: {
81
+ width: Math.max(Math.max(...xs) - left + padding, FLOW_ANNOTATION_MIN_SIZE),
82
+ height: Math.max(Math.max(...ys) - top + padding, FLOW_ANNOTATION_MIN_SIZE)
83
+ },
84
+ points: points.map((point) => ({ x: point.x - left, y: point.y - top }))
85
+ };
86
+ };
87
+ const boxBetween = (start, end) => ({
88
+ position: { x: Math.min(start.x, end.x), y: Math.min(start.y, end.y) },
89
+ size: {
90
+ width: Math.max(Math.abs(end.x - start.x), FLOW_ANNOTATION_MIN_SIZE),
91
+ height: Math.max(Math.abs(end.y - start.y), FLOW_ANNOTATION_MIN_SIZE)
92
+ }
93
+ });
94
+ const scaleAnnotationPoints = (points, from, to) => {
95
+ if (!points) return void 0;
96
+ const scaleX = from.width > 0 ? to.width / from.width : 1;
97
+ const scaleY = from.height > 0 ? to.height / from.height : 1;
98
+ return points.map((point) => ({ x: point.x * scaleX, y: point.y * scaleY }));
99
+ };
100
+ const cloneFlowAnnotations = (annotations, offset) => annotations.map((annotation) => ({
101
+ ...annotation,
102
+ annotationId: generateUUID(),
103
+ position: { x: annotation.position.x + offset, y: annotation.position.y + offset },
104
+ points: annotation.points?.map((point) => ({ ...point }))
105
+ }));
106
+ export {
107
+ FLOW_ANNOTATION_KINDS,
108
+ FLOW_ANNOTATION_MIN_SIZE,
109
+ FLOW_ANNOTATION_STROKES,
110
+ FLOW_ANNOTATION_STROKE_WIDTH,
111
+ FLOW_ANNOTATION_TONES,
112
+ FLOW_DRAWING_MAX_POINTS,
113
+ FLOW_TEXT_CARET_OFFSET,
114
+ boxAroundPoints,
115
+ boxBetween,
116
+ cloneFlowAnnotations,
117
+ nextFlowStepNumber,
118
+ normalizeFlowAnnotations,
119
+ scaleAnnotationPoints,
120
+ smoothStrokePath,
121
+ strokeWidthOf
122
+ };
123
+ //# sourceMappingURL=flowAnnotations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flowAnnotations.js","sources":["../../../../src/components/UI/Flow/flowAnnotations.ts"],"sourcesContent":["import { generateUUID } from '@/helpers/utilityHelpers';\n\nimport type {\n IFlowAnnotation,\n IFlowPosition,\n IFlowSize,\n TFlowAnnotationKind,\n TFlowAnnotationStroke,\n TFlowAnnotationTone,\n} from './flowTypes';\n\nexport const FLOW_ANNOTATION_KINDS: Array<TFlowAnnotationKind> = [\n 'text',\n 'rectangle',\n 'ellipse',\n 'line',\n 'arrow',\n 'step',\n 'drawing',\n];\n\nexport const FLOW_ANNOTATION_TONES: Array<TFlowAnnotationTone> = [\n 'danger',\n 'warning',\n 'success',\n 'info',\n 'brand',\n 'neutral',\n];\n\nexport const FLOW_ANNOTATION_STROKES: Array<TFlowAnnotationStroke> = ['thin', 'medium', 'thick'];\n\nexport const FLOW_ANNOTATION_STROKE_WIDTH: Record<TFlowAnnotationStroke, number> = {\n thin: 2,\n medium: 3.5,\n thick: 6,\n};\n\n/** A box never collapses to nothing: a zero-wide arrow or a dot still has to be grabbed. */\nexport const FLOW_ANNOTATION_MIN_SIZE = 8;\n/** Puts the first line of a new note on the point that was clicked, not below it. */\nexport const FLOW_TEXT_CARET_OFFSET = 14;\n/** Keeps a long scribble from bloating the saved flow. */\nexport const FLOW_DRAWING_MAX_POINTS = 2000;\n\nconst isFiniteNumber = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value);\n\nconst isPoint = (value: unknown): value is IFlowPosition =>\n isFiniteNumber((value as IFlowPosition | undefined)?.x) && isFiniteNumber((value as IFlowPosition | undefined)?.y);\n\nexport const strokeWidthOf = (annotation: Pick<IFlowAnnotation, 'stroke'>): number =>\n FLOW_ANNOTATION_STROKE_WIDTH[annotation.stroke ?? 'medium'] ?? FLOW_ANNOTATION_STROKE_WIDTH.medium;\n\n/** The number a new step takes: one past the highest on the canvas. */\nexport const nextFlowStepNumber = (annotations: Array<IFlowAnnotation>): number =>\n annotations.reduce((highest, annotation) => Math.max(highest, annotation.step ?? 0), 0) + 1;\n\n/**\n * A freehand stroke as a smooth path: each recorded point becomes the control of a curve through the\n * midpoints, which takes the jitter of the pointer out without moving the stroke.\n */\nexport const smoothStrokePath = (points: Array<IFlowPosition>): string => {\n const [first, ...rest] = points;\n if (!first) return '';\n if (rest.length === 0) return `M ${first.x} ${first.y} l 0 0`;\n const curves = rest.map((point, index) => {\n const previous = points[index] ?? first;\n return `Q ${previous.x} ${previous.y} ${(previous.x + point.x) / 2} ${(previous.y + point.y) / 2}`;\n });\n const last = rest[rest.length - 1] ?? first;\n return `M ${first.x} ${first.y} ${curves.join(' ')} L ${last.x} ${last.y}`;\n};\n\n/** Coerces what a host stored: an entry the canvas cannot draw is dropped, never rendered half-broken. */\nexport const normalizeFlowAnnotations = (annotations: unknown): Array<IFlowAnnotation> => {\n if (!Array.isArray(annotations)) return [];\n return annotations.flatMap((candidate): Array<IFlowAnnotation> => {\n const annotation = candidate as Partial<IFlowAnnotation> | null;\n if (!annotation || typeof annotation.annotationId !== 'string' || !annotation.annotationId) return [];\n if (!FLOW_ANNOTATION_KINDS.includes(annotation.kind as TFlowAnnotationKind)) return [];\n if (!isPoint(annotation.position)) return [];\n const width = isFiniteNumber(annotation.size?.width) ? annotation.size.width : FLOW_ANNOTATION_MIN_SIZE;\n const height = isFiniteNumber(annotation.size?.height) ? annotation.size.height : FLOW_ANNOTATION_MIN_SIZE;\n const points = Array.isArray(annotation.points) ? annotation.points.filter(isPoint) : undefined;\n const needsTwoPoints = annotation.kind === 'arrow' || annotation.kind === 'line';\n if ((needsTwoPoints && (points?.length ?? 0) < 2) || (annotation.kind === 'drawing' && !points?.length)) return [];\n return [\n {\n annotationId: annotation.annotationId,\n kind: annotation.kind as TFlowAnnotationKind,\n position: { x: annotation.position.x, y: annotation.position.y },\n size: {\n width: Math.max(width, FLOW_ANNOTATION_MIN_SIZE),\n height: Math.max(height, FLOW_ANNOTATION_MIN_SIZE),\n },\n tone: FLOW_ANNOTATION_TONES.includes(annotation.tone as TFlowAnnotationTone)\n ? (annotation.tone as TFlowAnnotationTone)\n : 'neutral',\n ...(FLOW_ANNOTATION_STROKES.includes(annotation.stroke as TFlowAnnotationStroke)\n ? { stroke: annotation.stroke }\n : {}),\n ...(typeof annotation.text === 'string' ? { text: annotation.text } : {}),\n ...(isFiniteNumber(annotation.step) ? { step: annotation.step } : {}),\n ...(points ? { points } : {}),\n },\n ];\n });\n};\n\n/**\n * Turns points in canvas coordinates into an annotation box: its position is the top-left corner\n * (padded so the stroke is not clipped) and the points become relative to it.\n */\nexport const boxAroundPoints = (\n points: Array<IFlowPosition>,\n padding: number,\n): { position: IFlowPosition; size: IFlowSize; points: Array<IFlowPosition> } => {\n const xs = points.map((point) => point.x);\n const ys = points.map((point) => point.y);\n const left = Math.min(...xs) - padding;\n const top = Math.min(...ys) - padding;\n return {\n position: { x: left, y: top },\n size: {\n width: Math.max(Math.max(...xs) - left + padding, FLOW_ANNOTATION_MIN_SIZE),\n height: Math.max(Math.max(...ys) - top + padding, FLOW_ANNOTATION_MIN_SIZE),\n },\n points: points.map((point) => ({ x: point.x - left, y: point.y - top })),\n };\n};\n\n/** A box spanning two corners dragged in any direction. */\nexport const boxBetween = (start: IFlowPosition, end: IFlowPosition): { position: IFlowPosition; size: IFlowSize } => ({\n position: { x: Math.min(start.x, end.x), y: Math.min(start.y, end.y) },\n size: {\n width: Math.max(Math.abs(end.x - start.x), FLOW_ANNOTATION_MIN_SIZE),\n height: Math.max(Math.abs(end.y - start.y), FLOW_ANNOTATION_MIN_SIZE),\n },\n});\n\n/** Scales a stroke with its box when the box is resized. */\nexport const scaleAnnotationPoints = (\n points: Array<IFlowPosition> | undefined,\n from: IFlowSize,\n to: IFlowSize,\n): Array<IFlowPosition> | undefined => {\n if (!points) return undefined;\n const scaleX = from.width > 0 ? to.width / from.width : 1;\n const scaleY = from.height > 0 ? to.height / from.height : 1;\n return points.map((point) => ({ x: point.x * scaleX, y: point.y * scaleY }));\n};\n\nexport const cloneFlowAnnotations = (annotations: Array<IFlowAnnotation>, offset: number): Array<IFlowAnnotation> =>\n annotations.map((annotation) => ({\n ...annotation,\n annotationId: generateUUID(),\n position: { x: annotation.position.x + offset, y: annotation.position.y + offset },\n points: annotation.points?.map((point) => ({ ...point })),\n }));\n"],"names":[],"mappings":"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"}
@@ -1,14 +1,28 @@
1
1
  import { StoreApi } from 'zustand/vanilla';
2
2
  import { IFlowBlockRegistry } from './flowBlockRegistry';
3
- import { IFlowGraph, IFlowNode, IFlowPosition, TFlowPortId } from './flowTypes';
3
+ import { IFlowAnnotation, IFlowGraph, IFlowNode, IFlowPosition, IFlowSize, TFlowPortId } from './flowTypes';
4
+ export interface IFlowSelection {
5
+ nodeIds?: Array<string>;
6
+ edgeIds?: Array<string>;
7
+ annotationIds?: Array<string>;
8
+ }
9
+ export interface IFlowAnnotationGeometry {
10
+ position?: IFlowPosition;
11
+ size?: IFlowSize;
12
+ points?: Array<IFlowPosition>;
13
+ }
4
14
  export interface IFlowEditorState {
5
15
  graph: IFlowGraph;
6
16
  selectedNodeIds: Array<string>;
7
17
  selectedEdgeIds: Array<string>;
18
+ selectedAnnotationIds: Array<string>;
8
19
  past: Array<IFlowGraph>;
9
20
  future: Array<IFlowGraph>;
10
21
  clipboard: Array<IFlowNode>;
22
+ annotationClipboard: Array<IFlowAnnotation>;
11
23
  isDirty: boolean;
24
+ /** A marquee is being drawn: the selection is still changing, so nothing should react to it yet. */
25
+ isAreaSelecting: boolean;
12
26
  replaceGraph: (graph: IFlowGraph, options?: {
13
27
  keepHistory?: boolean;
14
28
  }) => void;
@@ -27,15 +41,20 @@ export interface IFlowEditorState {
27
41
  updateNodeConfig: (nodeId: string, config: unknown) => void;
28
42
  updateNodeLabel: (nodeId: string, label: string) => void;
29
43
  moveNodes: (positions: Record<string, IFlowPosition>) => void;
44
+ /** Adds an annotation and selects it, in one undo step. */
45
+ addAnnotation: (annotation: Omit<IFlowAnnotation, 'annotationId'>) => string;
46
+ /** Text and colour changes. Typing in the same note within a second is one undo step. */
47
+ updateAnnotations: (annotationIds: Array<string>, patch: Partial<Pick<IFlowAnnotation, 'text' | 'tone' | 'stroke'>>) => void;
48
+ /** Live geometry while dragging or resizing: no undo step, like `moveNodes`. */
49
+ setAnnotationGeometry: (geometry: Record<string, IFlowAnnotationGeometry>) => void;
50
+ removeAnnotations: (annotationIds: Array<string>) => void;
30
51
  connect: (source: string, sourcePort: TFlowPortId, target: string) => boolean;
31
52
  removeSelected: () => void;
32
53
  duplicateSelected: () => void;
33
54
  copySelected: () => void;
34
55
  paste: () => void;
35
- setSelection: (selection: {
36
- nodeIds?: Array<string>;
37
- edgeIds?: Array<string>;
38
- }) => void;
56
+ setSelection: (selection: IFlowSelection) => void;
57
+ setAreaSelecting: (active: boolean) => void;
39
58
  undo: () => void;
40
59
  redo: () => void;
41
60
  /** The host may rebuild its registry (labels after a language switch); gestures always read the latest. */
@@ -47,3 +66,8 @@ export type TFlowEditorStore = StoreApi<IFlowEditorState>;
47
66
  * is the source of truth; the canvas only renders it and reports gestures back.
48
67
  */
49
68
  export declare const createFlowEditorStore: (initialGraph: IFlowGraph, registry: IFlowBlockRegistry) => TFlowEditorStore;
69
+ /**
70
+ * The block the side panel edits: exactly one block selected, nothing else, and no marquee still
71
+ * being drawn. `null` otherwise. Hosts that dock the panel themselves gate it on this.
72
+ */
73
+ export declare const selectFlowInspectedNodeId: (state: IFlowEditorState) => string | null;
@@ -1,5 +1,6 @@
1
1
  import { createStore } from "zustand/vanilla";
2
2
  import { generateUUID } from "../../../helpers/utilityHelpers.js";
3
+ import { cloneFlowAnnotations, normalizeFlowAnnotations } from "./flowAnnotations.js";
3
4
  import { flowOutputsOf } from "./flowBlockRegistry.js";
4
5
  import { isFlowNodeRemovable, canConnect, reachableFrom, FLOW_LEVEL_GAP, FLOW_SIBLING_GAP, layoutFlowByLevels } from "./flowGraph.js";
5
6
  const HISTORY_LIMIT = 50;
@@ -9,7 +10,8 @@ const isPosition = (position) => typeof position?.x === "number" && typeof posit
9
10
  const normalizeGraph = (graph, registry) => {
10
11
  const normalized = {
11
12
  nodes: Array.isArray(graph?.nodes) ? graph.nodes : [],
12
- edges: Array.isArray(graph?.edges) ? graph.edges : []
13
+ edges: Array.isArray(graph?.edges) ? graph.edges : [],
14
+ annotations: normalizeFlowAnnotations(graph?.annotations)
13
15
  };
14
16
  if (normalized.nodes.every((node) => isPosition(node.position))) return normalized;
15
17
  const layout = layoutFlowByLevels(normalized, registry);
@@ -21,6 +23,7 @@ const normalizeGraph = (graph, registry) => {
21
23
  };
22
24
  };
23
25
  const withoutNodes = (graph, nodeIds) => ({
26
+ ...graph,
24
27
  nodes: graph.nodes.filter((node) => !nodeIds.has(node.nodeId)),
25
28
  edges: graph.edges.filter((edge) => !nodeIds.has(edge.source) && !nodeIds.has(edge.target))
26
29
  });
@@ -39,18 +42,21 @@ const createFlowEditorStore = (initialGraph, registry) => createStore()((set, ge
39
42
  let lastCoalesce = null;
40
43
  const commit = (next, selection, coalesceKey) => {
41
44
  const { graph, past } = get();
45
+ const nextGraph = next.annotations === void 0 ? { ...next, annotations: graph.annotations ?? [] } : next;
42
46
  const now = Date.now();
43
47
  const extendsLastStep = coalesceKey !== void 0 && lastCoalesce?.key === coalesceKey && now - lastCoalesce.at < COALESCE_WINDOW_MS;
44
48
  lastCoalesce = coalesceKey === void 0 ? null : { key: coalesceKey, at: now };
45
49
  set({
46
- graph: next,
50
+ graph: nextGraph,
47
51
  past: extendsLastStep ? past : [...past, graph].slice(-HISTORY_LIMIT),
48
52
  future: [],
49
53
  isDirty: true,
50
54
  ...selection?.nodeIds ? { selectedNodeIds: selection.nodeIds } : {},
51
- ...selection?.edgeIds ? { selectedEdgeIds: selection.edgeIds } : {}
55
+ ...selection?.edgeIds ? { selectedEdgeIds: selection.edgeIds } : {},
56
+ ...selection?.annotationIds ? { selectedAnnotationIds: selection.annotationIds } : {}
52
57
  });
53
58
  };
59
+ const annotationsOf = (graph) => graph.annotations ?? [];
54
60
  const cloneNodes = (nodes, offset) => {
55
61
  const instancesByType = /* @__PURE__ */ new Map();
56
62
  get().graph.nodes.forEach((node) => instancesByType.set(node.type, (instancesByType.get(node.type) ?? 0) + 1));
@@ -72,10 +78,13 @@ const createFlowEditorStore = (initialGraph, registry) => createStore()((set, ge
72
78
  graph: normalizeGraph(initialGraph, registry),
73
79
  selectedNodeIds: [],
74
80
  selectedEdgeIds: [],
81
+ selectedAnnotationIds: [],
75
82
  past: [],
76
83
  future: [],
77
84
  clipboard: [],
85
+ annotationClipboard: [],
78
86
  isDirty: false,
87
+ isAreaSelecting: false,
79
88
  replaceGraph: (graph, options) => {
80
89
  set((state) => ({
81
90
  graph: normalizeGraph(graph, currentRegistry),
@@ -83,6 +92,7 @@ const createFlowEditorStore = (initialGraph, registry) => createStore()((set, ge
83
92
  future: [],
84
93
  selectedNodeIds: [],
85
94
  selectedEdgeIds: [],
95
+ selectedAnnotationIds: [],
86
96
  isDirty: Boolean(options?.keepHistory)
87
97
  }));
88
98
  },
@@ -217,6 +227,51 @@ const createFlowEditorStore = (initialGraph, registry) => createStore()((set, ge
217
227
  isDirty: true
218
228
  });
219
229
  },
230
+ addAnnotation: (annotation) => {
231
+ const { graph } = get();
232
+ const created = { ...annotation, annotationId: generateUUID() };
233
+ commit(
234
+ { ...graph, annotations: [...annotationsOf(graph), created] },
235
+ { nodeIds: [], edgeIds: [], annotationIds: [created.annotationId] }
236
+ );
237
+ return created.annotationId;
238
+ },
239
+ updateAnnotations: (annotationIds, patch) => {
240
+ const { graph } = get();
241
+ const targets = new Set(annotationIds);
242
+ if (targets.size === 0) return;
243
+ commit(
244
+ {
245
+ ...graph,
246
+ annotations: annotationsOf(graph).map(
247
+ (annotation) => targets.has(annotation.annotationId) ? { ...annotation, ...patch } : annotation
248
+ )
249
+ },
250
+ void 0,
251
+ patch.text !== void 0 ? `annotation-text:${annotationIds.join(",")}` : void 0
252
+ );
253
+ },
254
+ setAnnotationGeometry: (geometry) => {
255
+ const { graph } = get();
256
+ set({
257
+ graph: {
258
+ ...graph,
259
+ annotations: annotationsOf(graph).map(
260
+ (annotation) => geometry[annotation.annotationId] ? { ...annotation, ...geometry[annotation.annotationId] } : annotation
261
+ )
262
+ },
263
+ isDirty: true
264
+ });
265
+ },
266
+ removeAnnotations: (annotationIds) => {
267
+ const { graph, selectedAnnotationIds } = get();
268
+ const removed = new Set(annotationIds);
269
+ if (!annotationsOf(graph).some((annotation) => removed.has(annotation.annotationId))) return;
270
+ commit(
271
+ { ...graph, annotations: annotationsOf(graph).filter((annotation) => !removed.has(annotation.annotationId)) },
272
+ { annotationIds: selectedAnnotationIds.filter((id) => !removed.has(id)) }
273
+ );
274
+ },
220
275
  connect: (source, sourcePort, target) => {
221
276
  const { graph } = get();
222
277
  if (!canConnect(graph, currentRegistry, { source, sourcePort, target })) return false;
@@ -225,46 +280,87 @@ const createFlowEditorStore = (initialGraph, registry) => createStore()((set, ge
225
280
  return true;
226
281
  },
227
282
  removeSelected: () => {
228
- const { graph, selectedNodeIds, selectedEdgeIds } = get();
283
+ const { graph, selectedNodeIds, selectedEdgeIds, selectedAnnotationIds } = get();
229
284
  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;
231
- const removedEdges = new Set(selectedEdgeIds);
285
+ const removedNodes = new Set(removableNodeIds);
286
+ const selectedNodes = new Set(selectedNodeIds);
287
+ const removedEdges = new Set(
288
+ graph.edges.filter((edge) => selectedEdgeIds.includes(edge.edgeId)).filter(
289
+ (edge) => removedNodes.has(edge.source) || removedNodes.has(edge.target) || !selectedNodes.has(edge.source) && !selectedNodes.has(edge.target)
290
+ ).map((edge) => edge.edgeId)
291
+ );
292
+ const removedAnnotations = new Set(selectedAnnotationIds);
293
+ const annotations = annotationsOf(graph).filter(
294
+ (annotation) => !removedAnnotations.has(annotation.annotationId)
295
+ );
296
+ const removesAnnotations = annotations.length !== annotationsOf(graph).length;
297
+ if (removedNodes.size === 0 && removedEdges.size === 0 && !removesAnnotations) return;
232
298
  const next = withoutNodes(
233
- { ...graph, edges: graph.edges.filter((edge) => !removedEdges.has(edge.edgeId)) },
234
- new Set(removableNodeIds)
299
+ { ...graph, edges: graph.edges.filter((edge) => !removedEdges.has(edge.edgeId)), annotations },
300
+ removedNodes
235
301
  );
236
- commit(next, { nodeIds: [], edgeIds: [] });
302
+ commit(next, { nodeIds: [], edgeIds: [], annotationIds: [] });
237
303
  },
238
304
  duplicateSelected: () => {
239
- const { graph, selectedNodeIds } = get();
305
+ const { graph, selectedNodeIds, selectedAnnotationIds } = get();
240
306
  const selected = graph.nodes.filter((node) => selectedNodeIds.includes(node.nodeId));
241
307
  const copies = cloneNodes(selected, PASTE_OFFSET);
242
- if (copies.length === 0) return;
308
+ const annotationCopies = cloneFlowAnnotations(
309
+ annotationsOf(graph).filter((annotation) => selectedAnnotationIds.includes(annotation.annotationId)),
310
+ PASTE_OFFSET
311
+ );
312
+ if (copies.length === 0 && annotationCopies.length === 0) return;
243
313
  commit(
244
- { ...graph, nodes: [...graph.nodes, ...copies] },
245
- { nodeIds: copies.map((node) => node.nodeId), edgeIds: [] }
314
+ {
315
+ ...graph,
316
+ nodes: [...graph.nodes, ...copies],
317
+ annotations: [...annotationsOf(graph), ...annotationCopies]
318
+ },
319
+ {
320
+ nodeIds: copies.map((node) => node.nodeId),
321
+ edgeIds: [],
322
+ annotationIds: annotationCopies.map((annotation) => annotation.annotationId)
323
+ }
246
324
  );
247
325
  },
248
326
  copySelected: () => {
249
- const { graph, selectedNodeIds } = get();
250
- set({ clipboard: graph.nodes.filter((node) => selectedNodeIds.includes(node.nodeId)) });
327
+ const { graph, selectedNodeIds, selectedAnnotationIds } = get();
328
+ set({
329
+ clipboard: graph.nodes.filter((node) => selectedNodeIds.includes(node.nodeId)),
330
+ annotationClipboard: annotationsOf(graph).filter(
331
+ (annotation) => selectedAnnotationIds.includes(annotation.annotationId)
332
+ )
333
+ });
251
334
  },
252
335
  paste: () => {
253
- const { graph, clipboard } = get();
336
+ const { graph, clipboard, annotationClipboard } = get();
254
337
  const copies = cloneNodes(clipboard, PASTE_OFFSET);
255
- if (copies.length === 0) return;
338
+ const annotationCopies = cloneFlowAnnotations(annotationClipboard, PASTE_OFFSET);
339
+ if (copies.length === 0 && annotationCopies.length === 0) return;
256
340
  commit(
257
- { ...graph, nodes: [...graph.nodes, ...copies] },
258
- { nodeIds: copies.map((node) => node.nodeId), edgeIds: [] }
341
+ {
342
+ ...graph,
343
+ nodes: [...graph.nodes, ...copies],
344
+ annotations: [...annotationsOf(graph), ...annotationCopies]
345
+ },
346
+ {
347
+ nodeIds: copies.map((node) => node.nodeId),
348
+ edgeIds: [],
349
+ annotationIds: annotationCopies.map((annotation) => annotation.annotationId)
350
+ }
259
351
  );
260
- set({ clipboard: copies });
352
+ set({ clipboard: copies, annotationClipboard: annotationCopies });
261
353
  },
262
- setSelection: ({ nodeIds, edgeIds }) => {
354
+ setSelection: ({ nodeIds, edgeIds, annotationIds }) => {
263
355
  set((state) => ({
264
356
  selectedNodeIds: nodeIds ?? state.selectedNodeIds,
265
- selectedEdgeIds: edgeIds ?? state.selectedEdgeIds
357
+ selectedEdgeIds: edgeIds ?? state.selectedEdgeIds,
358
+ selectedAnnotationIds: annotationIds ?? state.selectedAnnotationIds
266
359
  }));
267
360
  },
361
+ setAreaSelecting: (active) => {
362
+ if (get().isAreaSelecting !== active) set({ isAreaSelecting: active });
363
+ },
268
364
  undo: () => {
269
365
  const { graph, past, future } = get();
270
366
  const previous = past[past.length - 1];
@@ -275,6 +371,7 @@ const createFlowEditorStore = (initialGraph, registry) => createStore()((set, ge
275
371
  future: [graph, ...future].slice(0, HISTORY_LIMIT),
276
372
  selectedNodeIds: [],
277
373
  selectedEdgeIds: [],
374
+ selectedAnnotationIds: [],
278
375
  isDirty: true
279
376
  });
280
377
  },
@@ -288,6 +385,7 @@ const createFlowEditorStore = (initialGraph, registry) => createStore()((set, ge
288
385
  future: future.slice(1),
289
386
  selectedNodeIds: [],
290
387
  selectedEdgeIds: [],
388
+ selectedAnnotationIds: [],
291
389
  isDirty: true
292
390
  });
293
391
  },
@@ -296,7 +394,9 @@ const createFlowEditorStore = (initialGraph, registry) => createStore()((set, ge
296
394
  }
297
395
  };
298
396
  });
397
+ const selectFlowInspectedNodeId = (state) => state.selectedNodeIds.length === 1 && state.selectedAnnotationIds.length === 0 && !state.isAreaSelecting ? state.selectedNodeIds[0] ?? null : null;
299
398
  export {
300
- createFlowEditorStore
399
+ createFlowEditorStore,
400
+ selectFlowInspectedNodeId
301
401
  };
302
402
  //# sourceMappingURL=flowEditorStore.js.map