@calliopelabs/ui 0.7.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/README.md +266 -3
  2. package/dist/appearance/AppearancePicker.js +4 -2
  3. package/dist/appearance/AppearanceProvider.d.ts +10 -2
  4. package/dist/appearance/AppearanceProvider.js +6 -2
  5. package/dist/appearance/AppearanceScript.d.ts +15 -4
  6. package/dist/appearance/AppearanceScript.js +11 -5
  7. package/dist/appearance/appearance.js +1 -1
  8. package/dist/appearance/boot.d.ts +13 -2
  9. package/dist/appearance/boot.js +35 -5
  10. package/dist/appearance/product.d.ts +13 -0
  11. package/dist/appearance/product.js +2 -2
  12. package/dist/appearance/runtime-brand.d.ts +106 -0
  13. package/dist/appearance/runtime-brand.js +420 -0
  14. package/dist/css/appearance.js +147 -0
  15. package/dist/css/calliope.css +1221 -0
  16. package/dist/css/components.css +729 -0
  17. package/dist/css/tokens.css +491 -0
  18. package/dist/data/DefinitionList.d.ts +16 -0
  19. package/dist/data/DefinitionList.js +9 -0
  20. package/dist/data/Stat.d.ts +25 -0
  21. package/dist/data/Stat.js +19 -0
  22. package/dist/data/Tag.d.ts +21 -0
  23. package/dist/data/Tag.js +18 -0
  24. package/dist/data/types.d.ts +3 -2
  25. package/dist/data/use-local-table.d.ts +36 -0
  26. package/dist/data/use-local-table.js +127 -0
  27. package/dist/index.d.ts +44 -3
  28. package/dist/index.js +40 -1
  29. package/dist/nav/Nav.d.ts +2 -1
  30. package/dist/nav/Nav.js +34 -15
  31. package/dist/nav/model.d.ts +21 -0
  32. package/dist/page/PageHeader.d.ts +5 -3
  33. package/dist/page/PageHeader.js +5 -4
  34. package/dist/page/Panel.d.ts +19 -0
  35. package/dist/page/Panel.js +12 -0
  36. package/dist/primitives/Avatar.d.ts +37 -0
  37. package/dist/primitives/Avatar.js +43 -0
  38. package/dist/primitives/Field.d.ts +33 -0
  39. package/dist/primitives/Field.js +30 -0
  40. package/dist/primitives/Segmented.d.ts +30 -0
  41. package/dist/primitives/Segmented.js +20 -0
  42. package/dist/primitives/Select.d.ts +40 -0
  43. package/dist/primitives/Select.js +47 -0
  44. package/dist/primitives/Switch.d.ts +7 -0
  45. package/dist/primitives/Switch.js +8 -0
  46. package/dist/primitives/Textarea.d.ts +3 -0
  47. package/dist/primitives/Textarea.js +6 -0
  48. package/dist/provider/messages.d.ts +430 -0
  49. package/dist/provider/messages.js +666 -0
  50. package/dist/shell/BrandMark.d.ts +25 -3
  51. package/dist/shell/BrandMark.js +34 -4
  52. package/dist/shell/RailPanel.js +2 -2
  53. package/dist/shell/RailTree.js +2 -2
  54. package/dist/shell/Sidebar.d.ts +9 -2
  55. package/dist/shell/Sidebar.js +10 -4
  56. package/dist/shell/UserMenu.d.ts +3 -1
  57. package/dist/shell/UserMenu.js +3 -11
  58. package/dist/shell/rail-boot.d.ts +28 -0
  59. package/dist/shell/rail-boot.js +45 -0
  60. package/dist/shell/rail-state.d.ts +6 -1
  61. package/dist/shell/rail-state.js +18 -2
  62. package/dist/styles/preset.css +4 -0
  63. package/dist/styles/theme.css +36 -13
  64. package/dist/theme/css-layer.d.ts +19 -0
  65. package/dist/theme/css-layer.js +171 -0
  66. package/dist/theme/css.d.ts +1 -0
  67. package/dist/theme/css.js +45 -9
  68. package/dist/workflow/ArtifactCard.d.ts +47 -0
  69. package/dist/workflow/ArtifactCard.js +45 -0
  70. package/dist/workflow/DocumentReview.d.ts +74 -0
  71. package/dist/workflow/DocumentReview.js +54 -0
  72. package/dist/workflow/FormReview.d.ts +53 -0
  73. package/dist/workflow/FormReview.js +83 -0
  74. package/dist/workflow/GateBadge.d.ts +11 -0
  75. package/dist/workflow/GateBadge.js +14 -0
  76. package/dist/workflow/MediaReview.d.ts +63 -0
  77. package/dist/workflow/MediaReview.js +166 -0
  78. package/dist/workflow/ReviewPanel.d.ts +41 -0
  79. package/dist/workflow/ReviewPanel.js +59 -0
  80. package/dist/workflow/RunTimeline.d.ts +25 -0
  81. package/dist/workflow/RunTimeline.js +56 -0
  82. package/dist/workflow/WorkflowCard.d.ts +39 -0
  83. package/dist/workflow/WorkflowCard.js +56 -0
  84. package/dist/workflow/WorkflowGallery.d.ts +43 -0
  85. package/dist/workflow/WorkflowGallery.js +69 -0
  86. package/dist/workflow/WorkflowQuestionCard.d.ts +18 -0
  87. package/dist/workflow/WorkflowQuestionCard.js +78 -0
  88. package/dist/workflow/canvas/WorkflowCanvas.d.ts +39 -0
  89. package/dist/workflow/canvas/WorkflowCanvas.js +648 -0
  90. package/dist/workflow/definition.d.ts +408 -0
  91. package/dist/workflow/definition.js +411 -0
  92. package/dist/workflow/editor/WorkflowDiff.d.ts +17 -0
  93. package/dist/workflow/editor/WorkflowDiff.js +25 -0
  94. package/dist/workflow/editor/WorkflowImportPreview.d.ts +19 -0
  95. package/dist/workflow/editor/WorkflowImportPreview.js +50 -0
  96. package/dist/workflow/editor/WorkflowInspector.d.ts +40 -0
  97. package/dist/workflow/editor/WorkflowInspector.js +349 -0
  98. package/dist/workflow/editor/WorkflowJsonView.d.ts +16 -0
  99. package/dist/workflow/editor/WorkflowJsonView.js +40 -0
  100. package/dist/workflow/editor/WorkflowPalette.d.ts +16 -0
  101. package/dist/workflow/editor/WorkflowPalette.js +36 -0
  102. package/dist/workflow/editor/WorkflowProblems.d.ts +20 -0
  103. package/dist/workflow/editor/WorkflowProblems.js +51 -0
  104. package/dist/workflow/editor/WorkflowWorkspace.d.ts +49 -0
  105. package/dist/workflow/editor/WorkflowWorkspace.js +154 -0
  106. package/dist/workflow/gallery.d.ts +21 -0
  107. package/dist/workflow/gallery.js +29 -0
  108. package/dist/workflow/layout.d.ts +49 -0
  109. package/dist/workflow/layout.js +162 -0
  110. package/dist/workflow/model.d.ts +50 -0
  111. package/dist/workflow/model.js +7 -0
  112. package/dist/workflow/palette.d.ts +34 -0
  113. package/dist/workflow/palette.js +70 -0
  114. package/dist/workflow/portable.d.ts +162 -0
  115. package/dist/workflow/portable.js +547 -0
  116. package/dist/workflow/problems.d.ts +38 -0
  117. package/dist/workflow/problems.js +409 -0
  118. package/dist/workflow/question.d.ts +26 -0
  119. package/dist/workflow/question.js +1 -0
  120. package/dist/workflow/run.d.ts +94 -0
  121. package/dist/workflow/run.js +31 -0
  122. package/dist/workflow/workspace.d.ts +33 -0
  123. package/dist/workflow/workspace.js +17 -0
  124. package/package.json +6 -1
@@ -0,0 +1,648 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ /**
4
+ * The one place the package draws on @xyflow/react: it is wrapped here, as
5
+ * Radix is wrapped in src/primitives, and nothing else imports it.
6
+ */
7
+ import { Background, Handle, MarkerType, Position, ReactFlow, ReactFlowProvider, applyNodeChanges, useReactFlow, } from "@xyflow/react";
8
+ import { CableIcon, ChevronDownIcon, ChevronRightIcon, LayoutGridIcon, MaximizeIcon, PlusIcon, Trash2Icon, ZoomInIcon, ZoomOutIcon, } from "lucide-react";
9
+ import * as React from "react";
10
+ import { cn } from "../../lib/cn.js";
11
+ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "../../overlays/Dialog.js";
12
+ import { Button } from "../../primitives/Button.js";
13
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "../../primitives/DropdownMenu.js";
14
+ import { Field } from "../../primitives/Field.js";
15
+ import { Select } from "../../primitives/Select.js";
16
+ import { useUI } from "../../provider/UIProvider.js";
17
+ import { IN, LOOP_IN, LOOP_OUT, OUT, canConnect, edgeId, exitLabel, exitsOf, findGateKind, findPreset, inputsOf, moveNode, outputsOf, portOf, removeEdge, removeNode, withFlow, } from "../definition.js";
18
+ import { NODE_HEADER, NODE_WIDTH, PORT_ROW, arrangeFlow, layoutFlow, nodeSize, regionsOf, } from "../layout.js";
19
+ import { insertPaletteItem, paletteOf } from "../palette.js";
20
+ /** The canvas never zooms out past this: below it a node's words stop being readable. */
21
+ export const READABLE_ZOOM = 0.7;
22
+ const GRID = 16;
23
+ const PAD = 16;
24
+ const REGION_HEAD = 32;
25
+ const EMPTY_FLOW = { nodes: [], edges: [] };
26
+ const HANDLE = "size-2.5! rounded-full! border! border-muted-foreground! bg-card!";
27
+ const DATA_HANDLE = "size-2.5! rounded-xs! border! border-muted-foreground! bg-muted!";
28
+ const LOOP_HANDLE = "size-2.5! rounded-full! border! border-info! bg-card!";
29
+ const rowTop = (i) => NODE_HEADER + 4 + i * PORT_ROW + PORT_ROW / 2;
30
+ const HANDLE_SIZE = 10;
31
+ /**
32
+ * Where a card's handles sit, declared up front so edges draw before the
33
+ * browser measures anything (and in a server render or a test).
34
+ */
35
+ function cardHandles(d, width, height) {
36
+ const h = HANDLE_SIZE / 2;
37
+ const at = (id, type, position, x, y) => ({
38
+ id,
39
+ type,
40
+ position,
41
+ x: x - h,
42
+ y: y - h,
43
+ width: HANDLE_SIZE,
44
+ height: HANDLE_SIZE,
45
+ });
46
+ return [
47
+ at(IN, "target", Position.Left, 0, NODE_HEADER / 2),
48
+ ...d.inputs.map((p, i) => at(p.handle, "target", Position.Left, 0, rowTop(i))),
49
+ ...(d.singleExit ? [at(OUT, "source", Position.Right, width, NODE_HEADER / 2)] : []),
50
+ ...d.right.map((p, i) => at(p.handle, "source", Position.Right, width, rowTop(i))),
51
+ ...(d.node.kind === "goto"
52
+ ? [at("jump", "source", Position.Right, width, NODE_HEADER / 2)]
53
+ : []),
54
+ ...(d.loops ? [at(LOOP_OUT, "source", Position.Bottom, width * 0.7, height)] : []),
55
+ at(LOOP_IN, "target", Position.Bottom, width * 0.3, height),
56
+ ];
57
+ }
58
+ function regionHandles(width, height) {
59
+ const h = HANDLE_SIZE / 2;
60
+ return [
61
+ {
62
+ id: IN,
63
+ type: "target",
64
+ position: Position.Left,
65
+ x: -h,
66
+ y: height / 2 - h,
67
+ width: HANDLE_SIZE,
68
+ height: HANDLE_SIZE,
69
+ },
70
+ {
71
+ id: OUT,
72
+ type: "source",
73
+ position: Position.Right,
74
+ x: width - h,
75
+ y: height / 2 - h,
76
+ width: HANDLE_SIZE,
77
+ height: HANDLE_SIZE,
78
+ },
79
+ {
80
+ id: LOOP_IN,
81
+ type: "target",
82
+ position: Position.Bottom,
83
+ x: width * 0.3 - h,
84
+ y: height - h,
85
+ width: HANDLE_SIZE,
86
+ height: HANDLE_SIZE,
87
+ },
88
+ {
89
+ id: LOOP_OUT,
90
+ type: "source",
91
+ position: Position.Bottom,
92
+ x: width * 0.7 - h,
93
+ y: height - h,
94
+ width: HANDLE_SIZE,
95
+ height: HANDLE_SIZE,
96
+ },
97
+ ];
98
+ }
99
+ function NodeCard({ data }) {
100
+ const d = data;
101
+ return (_jsxs("div", { "data-slot": "workflow-node", className: cn("relative flex flex-col rounded-lg border bg-card text-card-foreground shadow-xs", d.tone === "selected" && "border-primary ring-2 ring-primary/40", d.tone === "blocking" && "border-danger-border", d.tone === "default" && "border-border"), style: { width: d.width, height: d.height }, children: [_jsxs("div", { className: "flex min-w-0 flex-col gap-0.5 px-3 py-2", style: { height: NODE_HEADER }, children: [_jsxs("div", { className: "flex items-center gap-1.5 text-2xs font-medium tracking-wide text-muted-foreground uppercase", children: [_jsx("span", { className: "truncate", children: d.kindLabel }), d.blocking ? (_jsx("span", { className: "ml-auto rounded-full bg-danger-bg px-1.5 text-danger-fg", "aria-hidden": true, children: d.blocking })) : d.warnings ? (_jsx("span", { className: "ml-auto rounded-full bg-warning-bg px-1.5 text-warning-fg", "aria-hidden": true, children: d.warnings })) : null, d.toggle ? (_jsx("button", { type: "button", className: cn("nodrag nopan rounded-md p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground", !d.blocking && !d.warnings && "ml-auto"), "aria-label": d.toggle.label, "aria-expanded": d.toggle.open, onClick: d.toggle.onToggle, children: d.toggle.open ? (_jsx(ChevronDownIcon, { className: "size-3.5" })) : (_jsx(ChevronRightIcon, { className: "size-3.5" })) })) : null] }), _jsx("p", { className: "truncate text-sm leading-5 font-medium", children: d.node.label }), _jsxs("p", { className: "truncate text-xs text-muted-foreground", children: [d.notRun ? _jsxs("span", { className: "text-warning-fg", children: [d.notRunLabel, " \u00B7 "] }) : null, d.summary] })] }), d.inputs.length || d.right.length ? (_jsxs("div", { className: "relative text-2xs text-muted-foreground", children: [d.inputs.map((p, i) => (_jsx("span", { className: "absolute left-3 max-w-[45%] truncate", style: { top: i * PORT_ROW + 4 }, title: p.title, children: p.label }, p.handle))), d.right.map((p, i) => (_jsx("span", { className: cn("absolute right-3 max-w-[45%] truncate text-right", p.kind === "exit" && "text-foreground"), style: { top: i * PORT_ROW + 4 }, title: p.title, children: p.label }, p.handle)))] })) : null, _jsx(Handle, { type: "target", id: IN, position: Position.Left, isConnectable: d.connectable, className: HANDLE, style: { top: NODE_HEADER / 2 }, title: d.handleTitles.in }), d.inputs.map((p, i) => (_jsx(Handle, { type: "target", id: p.handle, position: Position.Left, isConnectable: d.connectable, className: DATA_HANDLE, style: { top: rowTop(i) }, title: p.title }, p.handle))), d.singleExit ? (_jsx(Handle, { type: "source", id: OUT, position: Position.Right, isConnectable: d.connectable, className: HANDLE, style: { top: NODE_HEADER / 2 } })) : null, d.right.map((p, i) => (_jsx(Handle, { type: "source", id: p.handle, position: Position.Right, isConnectable: d.connectable, className: p.kind === "data" ? DATA_HANDLE : HANDLE, style: { top: rowTop(i) }, title: p.title }, p.handle))), d.node.kind === "goto" ? (_jsx(Handle, { type: "source", id: "jump", position: Position.Right, isConnectable: false, className: "opacity-0!", style: { top: NODE_HEADER / 2 } })) : null, d.loops ? (_jsx(Handle, { type: "source", id: LOOP_OUT, position: Position.Bottom, isConnectable: d.connectable, className: LOOP_HANDLE, style: { left: "70%" }, title: d.handleTitles.loop })) : null, _jsx(Handle, { type: "target", id: LOOP_IN, position: Position.Bottom, isConnectable: d.connectable, className: LOOP_HANDLE, style: { left: "30%" }, title: d.handleTitles.loopIn })] }));
102
+ }
103
+ function RegionBox({ data }) {
104
+ return (_jsxs("div", { "data-slot": "workflow-region", className: cn("h-full w-full rounded-lg border border-dashed border-border", data.folded ? "border-solid bg-card shadow-xs" : "bg-muted/40"), children: [_jsxs("div", { className: "flex items-center gap-1.5 px-2 py-1.5 text-xs text-muted-foreground", children: [_jsx("button", { type: "button", className: "nodrag nopan rounded-md p-0.5 hover:bg-muted hover:text-foreground",
105
+ // The region itself takes no pointer: only its toggle does.
106
+ style: { pointerEvents: "all" }, "aria-label": data.action, "aria-expanded": !data.folded, onClick: data.onToggle, children: data.folded ? (_jsx(ChevronRightIcon, { className: "size-3.5" })) : (_jsx(ChevronDownIcon, { className: "size-3.5" })) }), _jsx("span", { className: "truncate", children: data.title })] }), data.folded ? (_jsxs(_Fragment, { children: [_jsx(Handle, { type: "target", id: IN, position: Position.Left, isConnectable: false, className: HANDLE }), _jsx(Handle, { type: "source", id: OUT, position: Position.Right, isConnectable: false, className: HANDLE }), _jsx(Handle, { type: "target", id: LOOP_IN, position: Position.Bottom, isConnectable: false, className: "opacity-0!" }), _jsx(Handle, { type: "source", id: LOOP_OUT, position: Position.Bottom, isConnectable: false, className: "opacity-0!" })] })) : null] }));
107
+ }
108
+ const NODE_TYPES = { card: NodeCard, region: RegionBox };
109
+ /** The source handles a node offers, and the targets in a flow each can reach, for the keyboard connect. */
110
+ function connectOptions(flow, from, registry, definition, names) {
111
+ const sources = [
112
+ ...exitsOf(from).map((e) => ({ value: e, label: names.exit(exitLabel(from, e)) })),
113
+ ...(from.kind === "goto" ? [] : [{ value: LOOP_OUT, label: names.loop }]),
114
+ ...outputsOf(from, registry, definition).map((p) => ({
115
+ value: `output:${p.id}`,
116
+ label: names.port(p.label, p.type),
117
+ })),
118
+ ];
119
+ return sources
120
+ .map((s) => ({
121
+ ...s,
122
+ targets: flow.nodes.flatMap((b) => {
123
+ const targets = [
124
+ { value: IN, label: b.label },
125
+ { value: LOOP_IN, label: `${b.label}, ${names.loopIn}` },
126
+ ...inputsOf(b, registry, definition).map((p) => ({
127
+ value: `input:${p.id}`,
128
+ label: `${b.label}, ${names.port(p.label, p.type)}`,
129
+ })),
130
+ ];
131
+ return targets
132
+ .filter((t) => canConnect(flow, { from: from.id, source: s.value, to: b.id, target: t.value }, registry, definition).ok)
133
+ .map((t) => ({ value: JSON.stringify([b.id, t.value]), label: t.label }));
134
+ }),
135
+ }))
136
+ .filter((s) => s.targets.length);
137
+ }
138
+ function ConnectDialog({ open, onOpenChange, from, options, onConnect, }) {
139
+ const { messages } = useUI();
140
+ const m = messages.editor;
141
+ const [source, setSource] = React.useState(options[0]?.value ?? "");
142
+ const current = options.find((o) => o.value === source) ?? options[0];
143
+ const [target, setTarget] = React.useState(current?.targets[0]?.value ?? "");
144
+ const chosen = current?.targets.find((t) => t.value === target) ?? current?.targets[0];
145
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: m.connectTitle(from.label) }), _jsx(DialogDescription, { children: m.connectDescription })] }), current && chosen ? (_jsxs("form", { className: "flex flex-col gap-4", onSubmit: (e) => {
146
+ e.preventDefault();
147
+ const [to, t] = JSON.parse(chosen.value);
148
+ onConnect(current.value, to, t);
149
+ }, children: [_jsx(Field, { label: m.connectFrom, children: _jsx(Select, { value: current.value, onValueChange: (v) => {
150
+ setSource(v);
151
+ setTarget(options.find((o) => o.value === v)?.targets[0]?.value ?? "");
152
+ }, options: options.map((o) => ({ value: o.value, label: o.label })) }) }), _jsx(Field, { label: m.connectTo, children: _jsx(Select, { value: chosen.value, onValueChange: setTarget, options: current.targets }) }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "outline", onClick: () => onOpenChange(false), children: m.cancel }), _jsx(Button, { type: "submit", children: m.connectAction })] })] })) : (_jsxs(_Fragment, { children: [_jsx("p", { className: "text-sm text-muted-foreground", children: m.connectNone }), _jsx(DialogFooter, { children: _jsx(Button, { type: "button", variant: "outline", onClick: () => onOpenChange(false), children: m.cancel }) })] }))] }) }));
153
+ }
154
+ /**
155
+ * One flow of a workflow definition, drawn and edited on a pannable,
156
+ * zoomable canvas. The run goes left to right: work comes into a node's
157
+ * input on the left and leaves by its exits on the right; typed inputs and
158
+ * outputs sit in rows below the header; a loop leaves from the bottom right
159
+ * and goes back into an earlier node's bottom left. `canConnect` decides
160
+ * what snaps, so an invalid edge never lands. Loop bodies and fan-out
161
+ * branches fold; a sub-flow shows in place. Every drag has a keyboard
162
+ * equivalent: Tab to a node, Enter to select, arrows to move, Delete to
163
+ * remove; Add and Connect in the toolbar.
164
+ */
165
+ export function WorkflowCanvas(props) {
166
+ return (_jsx(ReactFlowProvider, { children: _jsx(CanvasInner, { ...props }) }));
167
+ }
168
+ function CanvasInner({ definition, flow: flowProp, registry, onChange, readOnly: readOnlyProp = false, selection = null, onSelectionChange, problems = [], folded: foldedProp, defaultFolded = [], onFoldedChange, defaultInPlace = [], label, className, }) {
169
+ const { messages } = useUI();
170
+ const m = messages.editor;
171
+ const view = useReactFlow();
172
+ const pane = React.useRef(null);
173
+ /**
174
+ * Fits the flow when it fits at a readable zoom; otherwise holds the
175
+ * readable floor and starts at the flow's beginning, so a long flow scrolls
176
+ * instead of shrinking or losing its start off the left edge.
177
+ */
178
+ const place = React.useCallback(() => {
179
+ const el = pane.current;
180
+ const nodes = view.getNodes().filter((n) => !n.parentId);
181
+ if (!el || !nodes.length)
182
+ return;
183
+ const b = view.getNodesBounds(nodes);
184
+ const w = el.clientWidth;
185
+ const h = el.clientHeight;
186
+ if (!w || !h || !b.width)
187
+ return;
188
+ const pad = 32;
189
+ const zoom = Math.min(1, Math.max(READABLE_ZOOM, Math.min((w - pad * 2) / b.width, (h - pad * 2) / b.height)));
190
+ const x = b.width * zoom <= w - pad * 2 ? (w - b.width * zoom) / 2 - b.x * zoom : pad - b.x * zoom;
191
+ const y = b.height * zoom <= h - pad * 2 ? (h - b.height * zoom) / 2 - b.y * zoom : pad - b.y * zoom;
192
+ void view.setViewport({ x, y, zoom });
193
+ }, [view]);
194
+ const flowName = flowProp && definition.flows[flowProp] ? flowProp : definition.main;
195
+ const flow = definition.flows[flowName] ?? EMPTY_FLOW;
196
+ const readOnly = readOnlyProp || !onChange;
197
+ const [foldedState, setFoldedState] = React.useState(defaultFolded);
198
+ const folded = foldedProp ?? foldedState;
199
+ const [inPlace, setInPlace] = React.useState(defaultInPlace);
200
+ const [announcement, setAnnouncement] = React.useState("");
201
+ const [connecting, setConnecting] = React.useState(false);
202
+ const setFolded = React.useCallback((next) => {
203
+ if (foldedProp === undefined)
204
+ setFoldedState(next);
205
+ onFoldedChange?.(next);
206
+ }, [foldedProp, onFoldedChange]);
207
+ const change = React.useCallback((next) => onChange?.(withFlow(definition, flowName, next)), [onChange, definition, flowName]);
208
+ const select = React.useCallback((next) => {
209
+ const same = next === selection ||
210
+ (next &&
211
+ selection &&
212
+ next.type === selection.type &&
213
+ next.id === selection.id &&
214
+ next.flow === selection.flow);
215
+ if (!same)
216
+ onSelectionChange?.(next);
217
+ }, [onSelectionChange, selection]);
218
+ const selectedNode = selection?.type === "node" && selection.flow === flowName
219
+ ? flow.nodes.find((n) => n.id === selection.id)
220
+ : undefined;
221
+ const selectedEdge = selection?.type === "edge" && selection.flow === flowName
222
+ ? flow.edges.find((e) => edgeId(e) === selection.id)
223
+ : undefined;
224
+ const built = React.useMemo(() => {
225
+ const here = problems.filter((p) => (p.flow ?? definition.main) === flowName);
226
+ const positions = layoutFlow(flow, registry, { definition });
227
+ const regions = regionsOf(flow);
228
+ const label = (id) => flow.nodes.find((n) => n.id === id)?.label ?? id;
229
+ const regionTitle = (r) => m.regionName(r.kind, label(r.head), r.members.length);
230
+ const hiddenBy = new Map();
231
+ for (const r of regions)
232
+ if (folded.includes(r.id) && !r.members.some((id) => hiddenBy.has(id)))
233
+ for (const id of r.members)
234
+ hiddenBy.set(id, r);
235
+ const card = (n, inner, extra = {}) => {
236
+ const blocking = inner
237
+ ? 0
238
+ : here.filter((p) => p.node === n.id && p.level === "blocking").length;
239
+ const warnings = inner
240
+ ? 0
241
+ : here.filter((p) => p.node === n.id && p.level === "warning").length;
242
+ const notRun = here.some((p) => p.node === n.id && p.level === "not-run");
243
+ const exits = exitsOf(n);
244
+ const preset = n.kind === "step" || n.kind === "variants" ? findPreset(registry, n.preset) : null;
245
+ const kindLabel = m.kind(n.kind);
246
+ const size = nodeSize(n, registry, definition);
247
+ const isSelected = !inner && selectedNode?.id === n.id;
248
+ return {
249
+ node: n,
250
+ kindLabel,
251
+ summary: m.summary(n, {
252
+ ...(preset ? { preset: preset.preset.label, module: preset.module.label } : {}),
253
+ ...(n.kind === "gate" ? { gate: findGateKind(registry, n.gate)?.label ?? n.gate } : {}),
254
+ ...(n.kind === "goto" && n.target ? { target: label(n.target) } : {}),
255
+ }),
256
+ name: m.nodeName(kindLabel, n.label, blocking, warnings, notRun),
257
+ blocking,
258
+ warnings,
259
+ notRun,
260
+ notRunLabel: m.notRun,
261
+ tone: isSelected ? "selected" : blocking ? "blocking" : "default",
262
+ inputs: inputsOf(n, registry, definition).map((p) => ({
263
+ handle: `input:${p.id}`,
264
+ label: p.label,
265
+ title: m.port(p.label, p.type),
266
+ kind: "data",
267
+ })),
268
+ right: [
269
+ ...(exits.length > 1
270
+ ? exits.map((e) => ({
271
+ handle: e,
272
+ label: m.exit(exitLabel(n, e)),
273
+ title: m.exit(exitLabel(n, e)),
274
+ kind: "exit",
275
+ }))
276
+ : []),
277
+ ...outputsOf(n, registry, definition).map((p) => ({
278
+ handle: `output:${p.id}`,
279
+ label: p.label,
280
+ title: m.port(p.label, p.type),
281
+ kind: "data",
282
+ })),
283
+ ],
284
+ singleExit: exits.length === 1,
285
+ loops: n.kind !== "goto",
286
+ connectable: !readOnly && !inner,
287
+ handleTitles: { in: m.inHandle, loop: m.loopHandle, loopIn: m.loopInHandle },
288
+ width: size.width,
289
+ height: size.height,
290
+ ...extra,
291
+ };
292
+ };
293
+ const nodes = [];
294
+ const edges = [];
295
+ const sizeOf = (n) => nodeSize(n, registry, definition);
296
+ // Regions: a folded one stands in for its members; an open one is drawn behind them.
297
+ for (const r of regions) {
298
+ const isFolded = hiddenBy.get(r.head) === r;
299
+ const hiddenElsewhere = r.members.some((id) => hiddenBy.has(id)) && !isFolded;
300
+ if (hiddenElsewhere)
301
+ continue;
302
+ const boxes = r.members.flatMap((id) => {
303
+ const n = flow.nodes.find((x) => x.id === id);
304
+ const at = positions[id];
305
+ return n && at ? [{ ...at, ...sizeOf(n) }] : [];
306
+ });
307
+ if (!boxes.length)
308
+ continue;
309
+ const x = Math.min(...boxes.map((b) => b.x));
310
+ const y = Math.min(...boxes.map((b) => b.y));
311
+ const title = regionTitle(r);
312
+ const data = {
313
+ title,
314
+ action: isFolded ? m.unfold(title) : m.fold(title),
315
+ folded: isFolded,
316
+ onToggle: () => setFolded(isFolded ? folded.filter((f) => f !== r.id) : [...folded, r.id]),
317
+ };
318
+ nodes.push(isFolded
319
+ ? {
320
+ id: `region:${r.id}`,
321
+ type: "region",
322
+ position: { x, y },
323
+ width: NODE_WIDTH,
324
+ height: NODE_HEADER,
325
+ handles: regionHandles(NODE_WIDTH, NODE_HEADER),
326
+ data,
327
+ selectable: false,
328
+ draggable: false,
329
+ connectable: false,
330
+ deletable: false,
331
+ ariaLabel: title,
332
+ }
333
+ : {
334
+ id: `region:${r.id}`,
335
+ type: "region",
336
+ position: { x: x - PAD, y: y - PAD - REGION_HEAD },
337
+ width: Math.max(...boxes.map((b) => b.x + b.width)) - x + PAD * 2,
338
+ height: Math.max(...boxes.map((b) => b.y + b.height)) - y + PAD * 2 + REGION_HEAD,
339
+ data,
340
+ zIndex: -1,
341
+ selectable: false,
342
+ draggable: false,
343
+ connectable: false,
344
+ deletable: false,
345
+ focusable: false,
346
+ });
347
+ }
348
+ const shown = (id) => {
349
+ const r = hiddenBy.get(id);
350
+ return r ? `region:${r.id}` : id;
351
+ };
352
+ for (const n of flow.nodes) {
353
+ if (hiddenBy.has(n.id))
354
+ continue;
355
+ const at = positions[n.id] ?? { x: 0, y: 0 };
356
+ const sub = n.kind === "subflow" ? definition.flows[n.flow] : undefined;
357
+ const open = !!sub && n.kind === "subflow" && n.flow !== flowName && inPlace.includes(n.id);
358
+ const toggle = sub && n.kind === "subflow" && n.flow !== flowName
359
+ ? {
360
+ label: open ? m.hideInPlace(n.label) : m.showInPlace(n.label),
361
+ open,
362
+ onToggle: () => setInPlace((ids) => (open ? ids.filter((i) => i !== n.id) : [...ids, n.id])),
363
+ }
364
+ : undefined;
365
+ if (open && sub) {
366
+ const innerAt = layoutFlow(sub, registry, { definition, keep: false });
367
+ const base = sizeOf(n);
368
+ const innerW = Math.max(0, ...sub.nodes.map((c) => (innerAt[c.id]?.x ?? 0) + NODE_WIDTH));
369
+ const innerH = Math.max(0, ...sub.nodes.map((c) => (innerAt[c.id]?.y ?? 0) + sizeOf(c).height));
370
+ const width = Math.max(base.width, innerW + PAD * 2);
371
+ const height = base.height + innerH + PAD * 2;
372
+ const data = card(n, false, { toggle, width, height });
373
+ nodes.push({
374
+ id: n.id,
375
+ type: "card",
376
+ position: at,
377
+ width,
378
+ height,
379
+ handles: cardHandles(data, width, height),
380
+ data,
381
+ selected: data.tone === "selected",
382
+ draggable: !readOnly,
383
+ deletable: !readOnly,
384
+ ariaLabel: data.name,
385
+ });
386
+ for (const c of sub.nodes) {
387
+ const cd = card(c, true);
388
+ nodes.push({
389
+ id: `${n.id}/${c.id}`,
390
+ type: "card",
391
+ parentId: n.id,
392
+ extent: "parent",
393
+ position: {
394
+ x: (innerAt[c.id]?.x ?? 0) + PAD,
395
+ y: base.height + PAD + (innerAt[c.id]?.y ?? 0),
396
+ },
397
+ width: cd.width,
398
+ height: cd.height,
399
+ handles: cardHandles(cd, cd.width, cd.height),
400
+ data: cd,
401
+ selectable: false,
402
+ draggable: false,
403
+ connectable: false,
404
+ deletable: false,
405
+ focusable: false,
406
+ });
407
+ }
408
+ for (const e of sub.edges)
409
+ if (e.kind !== "data")
410
+ edges.push({
411
+ id: `${n.id}/${edgeId(e)}`,
412
+ source: `${n.id}/${e.from}`,
413
+ target: `${n.id}/${e.to}`,
414
+ sourceHandle: e.kind === "loop" ? LOOP_OUT : portOf(e),
415
+ targetHandle: e.kind === "loop" ? LOOP_IN : IN,
416
+ selectable: false,
417
+ deletable: false,
418
+ focusable: false,
419
+ style: { stroke: "var(--muted-foreground)", strokeWidth: 1.25 },
420
+ });
421
+ continue;
422
+ }
423
+ const data = card(n, false, toggle ? { toggle } : {});
424
+ nodes.push({
425
+ id: n.id,
426
+ type: "card",
427
+ position: at,
428
+ width: data.width,
429
+ height: data.height,
430
+ handles: cardHandles(data, data.width, data.height),
431
+ data,
432
+ selected: data.tone === "selected",
433
+ draggable: !readOnly,
434
+ deletable: !readOnly,
435
+ ariaLabel: data.name,
436
+ });
437
+ }
438
+ for (const e of flow.edges) {
439
+ const id = edgeId(e);
440
+ const a = shown(e.from);
441
+ const b = shown(e.to);
442
+ if (a === b)
443
+ continue;
444
+ const rerouted = a !== e.from || b !== e.to;
445
+ const from = flow.nodes.find((n) => n.id === e.from);
446
+ const to = flow.nodes.find((n) => n.id === e.to);
447
+ if (!from || !to)
448
+ continue;
449
+ const bad = here.some((p) => p.edge === id && p.level === "blocking");
450
+ const isSelected = selectedEdge ? edgeId(selectedEdge) === id : false;
451
+ const tone = isSelected
452
+ ? "var(--primary)"
453
+ : bad
454
+ ? "var(--danger)"
455
+ : e.kind === "loop"
456
+ ? "var(--info)"
457
+ : e.kind === "data"
458
+ ? "var(--chart-3)"
459
+ : "var(--muted-foreground)";
460
+ const exit = e.kind === "next" && portOf(e) !== OUT ? m.exit(exitLabel(from, portOf(e))) : "";
461
+ // An exit is named on its node's row; the edge only carries a loop's limit or its own label.
462
+ const text = e.kind === "loop" ? m.loopMax(e.max) : (e.label ?? "");
463
+ const detail = e.kind === "data"
464
+ ? `${outputsOf(from, registry, definition).find((p) => p.id === e.port)?.label ?? e.port ?? ""}`
465
+ : text || exit;
466
+ edges.push({
467
+ id: rerouted ? `via:${id}` : id,
468
+ source: a,
469
+ target: b,
470
+ sourceHandle: a !== e.from
471
+ ? e.kind === "loop"
472
+ ? LOOP_OUT
473
+ : OUT
474
+ : e.kind === "data"
475
+ ? `output:${e.port}`
476
+ : portOf(e),
477
+ targetHandle: b !== e.to
478
+ ? e.kind === "loop"
479
+ ? LOOP_IN
480
+ : IN
481
+ : e.kind === "data"
482
+ ? `input:${e.input}`
483
+ : e.kind === "loop"
484
+ ? LOOP_IN
485
+ : IN,
486
+ type: e.kind === "next" ? "smoothstep" : "default",
487
+ label: text || undefined,
488
+ selected: isSelected,
489
+ selectable: !rerouted,
490
+ deletable: !readOnly && !rerouted,
491
+ focusable: !rerouted,
492
+ ariaLabel: m.edgeName(e.kind, from.label, to.label, detail || undefined),
493
+ style: {
494
+ stroke: tone,
495
+ strokeWidth: isSelected ? 2.5 : 1.5,
496
+ ...(e.kind === "loop"
497
+ ? { strokeDasharray: "6 4" }
498
+ : e.kind === "data"
499
+ ? { strokeDasharray: "2 3" }
500
+ : {}),
501
+ },
502
+ markerEnd: { type: MarkerType.ArrowClosed, color: tone, width: 14, height: 14 },
503
+ labelStyle: { fill: "var(--muted-foreground)", fontSize: 11 },
504
+ labelBgStyle: { fill: "var(--background)" },
505
+ });
506
+ }
507
+ for (const n of flow.nodes)
508
+ if (n.kind === "goto" && n.target && flow.nodes.some((x) => x.id === n.target)) {
509
+ const a = shown(n.id);
510
+ const b = shown(n.target);
511
+ if (a === b)
512
+ continue;
513
+ edges.push({
514
+ id: `goto:${n.id}`,
515
+ source: a,
516
+ target: b,
517
+ sourceHandle: a === n.id ? "jump" : OUT,
518
+ targetHandle: IN,
519
+ selectable: false,
520
+ deletable: false,
521
+ focusable: false,
522
+ label: m.loopMax(n.max),
523
+ style: { stroke: "var(--info)", strokeWidth: 1.25, strokeDasharray: "2 4" },
524
+ labelStyle: { fill: "var(--muted-foreground)", fontSize: 11 },
525
+ labelBgStyle: { fill: "var(--background)" },
526
+ });
527
+ }
528
+ return { nodes, edges };
529
+ }, [
530
+ definition,
531
+ flow,
532
+ flowName,
533
+ registry,
534
+ problems,
535
+ folded,
536
+ inPlace,
537
+ readOnly,
538
+ selectedNode,
539
+ selectedEdge,
540
+ m,
541
+ setFolded,
542
+ ]);
543
+ // The canvas moves nodes in its own state while dragging; a new document from the host replaces them.
544
+ const [nodes, setNodes] = React.useState(built.nodes);
545
+ const [from, setFrom] = React.useState(built.nodes);
546
+ if (from !== built.nodes) {
547
+ setFrom(built.nodes);
548
+ setNodes(built.nodes);
549
+ }
550
+ const isReal = (id) => flow.nodes.some((n) => n.id === id);
551
+ const realEdge = (id) => flow.edges.find((e) => edgeId(e) === id);
552
+ const onNodesChange = (changes) => {
553
+ setNodes((ns) => applyNodeChanges(changes.filter((c) => c.type !== "select" && c.type !== "remove"), ns));
554
+ const picked = changes.find((c) => c.type === "select" && c.selected && isReal(c.id));
555
+ if (picked && picked.type === "select")
556
+ select({ type: "node", flow: flowName, id: picked.id });
557
+ else if (changes.some((c) => c.type === "select" && !c.selected && c.id === selectedNode?.id))
558
+ select(null);
559
+ if (readOnly)
560
+ return;
561
+ let next = flow;
562
+ let moved = false;
563
+ for (const c of changes)
564
+ if (c.type === "position" && c.dragging === false && c.position && isReal(c.id)) {
565
+ next = moveNode(next, c.id, c.position);
566
+ moved = true;
567
+ }
568
+ if (moved)
569
+ change(next);
570
+ };
571
+ const onEdgesChange = (changes) => {
572
+ const picked = changes.find((c) => c.type === "select" && c.selected && realEdge(c.id));
573
+ if (picked && picked.type === "select")
574
+ select({ type: "edge", flow: flowName, id: picked.id });
575
+ else if (changes.some((c) => c.type === "select" && !c.selected && selectedEdge && c.id === edgeId(selectedEdge)))
576
+ select(null);
577
+ };
578
+ const connect = (c) => {
579
+ const r = canConnect(flow, { from: c.source, source: c.sourceHandle ?? OUT, to: c.target, target: c.targetHandle ?? IN }, registry, definition);
580
+ if (!r.ok) {
581
+ setAnnouncement(m.connect(r.reason, r.values));
582
+ return;
583
+ }
584
+ change({ ...flow, edges: [...flow.edges, r.edge] });
585
+ select({ type: "edge", flow: flowName, id: edgeId(r.edge) });
586
+ const label = (id) => flow.nodes.find((n) => n.id === id)?.label ?? id;
587
+ setAnnouncement(m.connected(label(r.edge.from), label(r.edge.to)));
588
+ };
589
+ const remove = (nodeIds, edgeIds) => {
590
+ let next = flow;
591
+ for (const id of edgeIds)
592
+ next = removeEdge(next, id);
593
+ for (const id of nodeIds)
594
+ next = removeNode(next, id);
595
+ // A deleted node takes its edges with it; count what the person deleted.
596
+ const count = nodeIds.length || edgeIds.length;
597
+ if (!count)
598
+ return;
599
+ change(next);
600
+ select(null);
601
+ setAnnouncement(m.deleted(count));
602
+ };
603
+ const add = (item) => {
604
+ const name = item.label || (item.template.kind === "step" ? m.customStep : m.kind(item.template.kind));
605
+ const r = insertPaletteItem(definition, flowName, item, registry, name, selectedNode?.id);
606
+ onChange?.(r.definition);
607
+ select({ type: "node", flow: flowName, id: r.node.id });
608
+ setAnnouncement(m.added(r.node.label));
609
+ };
610
+ const palette = React.useMemo(() => paletteOf(registry), [registry]);
611
+ const options = selectedNode
612
+ ? connectOptions(flow, selectedNode, registry, definition, {
613
+ exit: m.exit,
614
+ port: m.port,
615
+ loop: m.loopHandle,
616
+ loopIn: m.loopInHandle,
617
+ })
618
+ : [];
619
+ return (_jsxs("section", { "aria-label": label ?? m.canvas, "data-slot": "workflow-canvas", className: cn("relative flex h-full min-h-80 flex-col overflow-hidden rounded-lg border bg-background", className), children: [_jsxs("div", { role: "toolbar", "aria-label": m.canvasTools, className: "flex flex-wrap items-center gap-1 border-b bg-card px-2 py-1.5", children: [readOnly ? (_jsx("span", { className: "px-1 text-xs text-muted-foreground", children: m.readOnly })) : (_jsxs(_Fragment, { children: [_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { size: "sm", variant: "outline", children: [_jsx(PlusIcon, { "aria-hidden": true }), m.add] }) }), _jsx(DropdownMenuContent, { align: "start", className: "max-h-96 overflow-y-auto", children: palette.map((g, gi) => (_jsxs(DropdownMenuGroup, { children: [gi ? _jsx(DropdownMenuSeparator, {}) : null, _jsx(DropdownMenuLabel, { children: g.module
620
+ ? g.label
621
+ : m.paletteGroup(g.key) }), g.items.map((item) => (_jsxs(DropdownMenuItem, { onSelect: () => add(item), children: [item.label ||
622
+ (item.template.kind === "step"
623
+ ? m.customStep
624
+ : m.kind(item.template.kind)), item.available ? null : (_jsx("span", { className: "ml-auto text-2xs text-muted-foreground", children: m.notRun }))] }, item.key)))] }, g.key))) })] }), _jsxs(Button, { size: "sm", variant: "outline", disabled: !selectedNode, onClick: () => setConnecting(true), children: [_jsx(CableIcon, { "aria-hidden": true }), m.connectAction] }), _jsxs(Button, { size: "sm", variant: "outline", disabled: !selectedNode && !selectedEdge, onClick: () => remove(selectedNode ? [selectedNode.id] : [], selectedEdge ? [edgeId(selectedEdge)] : []), children: [_jsx(Trash2Icon, { "aria-hidden": true }), m.deleteSelection] }), _jsxs(Button, { size: "sm", variant: "outline", disabled: !flow.nodes.length, onClick: () => {
625
+ change(arrangeFlow(flow, registry, definition));
626
+ window.requestAnimationFrame(place);
627
+ }, children: [_jsx(LayoutGridIcon, { "aria-hidden": true }), m.arrange] })] })), _jsxs("span", { className: "ml-auto flex items-center gap-1", children: [_jsx(Button, { size: "icon-sm", variant: "ghost", "aria-label": m.zoomOut, title: m.zoomOut, onClick: () => void view.zoomOut(), children: _jsx(ZoomOutIcon, { "aria-hidden": true }) }), _jsx(Button, { size: "icon-sm", variant: "ghost", "aria-label": m.zoomIn, title: m.zoomIn, onClick: () => void view.zoomIn(), children: _jsx(ZoomInIcon, { "aria-hidden": true }) }), _jsx(Button, { size: "icon-sm", variant: "ghost", "aria-label": m.fit, title: m.fit, onClick: place, children: _jsx(MaximizeIcon, { "aria-hidden": true }) })] })] }), _jsx("div", { ref: pane, className: "relative min-h-0 flex-1", children: _jsx(ReactFlow, { nodes: nodes, edges: built.edges, nodeTypes: NODE_TYPES, onInit: place, minZoom: READABLE_ZOOM, maxZoom: 1.5, panOnScroll: true, snapToGrid: true, snapGrid: [GRID, GRID], nodesDraggable: !readOnly, nodesConnectable: !readOnly, nodesFocusable: true, edgesFocusable: true, elementsSelectable: true, multiSelectionKeyCode: null, selectionKeyCode: null, deleteKeyCode: readOnly ? null : ["Backspace", "Delete"], ariaLabelConfig: {
628
+ "node.a11yDescription.default": m.nodeHelp,
629
+ "node.a11yDescription.keyboardDisabled": m.nodeHelp,
630
+ "node.a11yDescription.ariaLiveMessage": ({ direction, x, y }) => m.movedLive(direction, x, y),
631
+ "edge.a11yDescription.default": m.edgeHelp,
632
+ }, onNodesChange: onNodesChange, onEdgesChange: onEdgesChange, onPaneClick: () => select(null), isValidConnection: (c) => canConnect(flow, {
633
+ from: c.source,
634
+ source: c.sourceHandle ?? OUT,
635
+ to: c.target,
636
+ target: c.targetHandle ?? IN,
637
+ }, registry, definition).ok, onConnect: connect,
638
+ // One callback for a Delete key press: nodes and the edges touching them go in one edit.
639
+ onDelete: ({ nodes: ns, edges: es }) => remove(ns.map((n) => n.id).filter(isReal), es.map((e) => e.id).filter((id) => realEdge(id))), children: _jsx(Background, { gap: GRID, size: 1, color: "var(--border)" }) }) }), _jsx("p", { role: "status", "data-slot": "workflow-canvas-live", className: "sr-only", children: announcement }), selectedNode && connecting ? (_jsx(ConnectDialog, { open: connecting, onOpenChange: setConnecting, from: selectedNode, options: options, onConnect: (source, to, target) => {
640
+ connect({
641
+ source: selectedNode.id,
642
+ sourceHandle: source,
643
+ target: to,
644
+ targetHandle: target,
645
+ });
646
+ setConnecting(false);
647
+ } }, selectedNode.id)) : null] }));
648
+ }