@tangle-network/sandbox-ui 0.109.0 → 0.110.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.
@@ -16,8 +16,10 @@ import {
16
16
  isStructuralKind,
17
17
  providerLabel,
18
18
  shortModel,
19
- triggerNodeIndex
20
- } from "./chunk-LHUBJPZR.js";
19
+ triggerNodeIndex,
20
+ wfEdgeId,
21
+ worstSeverity
22
+ } from "./chunk-AI25G5MX.js";
21
23
  import {
22
24
  ModelBrandStack,
23
25
  modelBrandFor
@@ -42,7 +44,7 @@ import {
42
44
  useNodesState
43
45
  } from "@xyflow/react";
44
46
  import "@xyflow/react/dist/style.css";
45
- import { Maximize2, Minimize2 } from "lucide-react";
47
+ import { Maximize2, Minimize2, Plus, X } from "lucide-react";
46
48
  import {
47
49
  createContext,
48
50
  useCallback,
@@ -68,9 +70,17 @@ function isNodeStateMap(arg) {
68
70
  (v) => v !== null && typeof v === "object" && "status" in v
69
71
  );
70
72
  }
73
+ var FLOW_GRAPH_OPTION_KEYS = [
74
+ "nodeState",
75
+ "direction",
76
+ "compact",
77
+ "wrap",
78
+ "edges",
79
+ "reserveEdgeInsert"
80
+ ];
71
81
  function normalizeFlowGraphOptions(arg) {
72
82
  if (!arg) return {};
73
- if ("nodeState" in arg || "direction" in arg || "compact" in arg || "wrap" in arg || "edges" in arg) {
83
+ if (FLOW_GRAPH_OPTION_KEYS.some((key) => key in arg)) {
74
84
  return arg;
75
85
  }
76
86
  return isNodeStateMap(arg) ? { nodeState: arg } : {};
@@ -85,7 +95,8 @@ function buildFlowGraph(yaml, optionsOrNodeState) {
85
95
  direction,
86
96
  compact,
87
97
  ...options.wrap ? { wrap: true } : {},
88
- ...options.edges ? { edges: options.edges } : {}
98
+ ...options.edges ? { edges: options.edges } : {},
99
+ ...options.reserveEdgeInsert ? { reserveEdgeInsert: true } : {}
89
100
  });
90
101
  const isLR = direction === "LR";
91
102
  const sourcePosition = isLR ? Position.Right : Position.Bottom;
@@ -149,6 +160,69 @@ function mergeRunState(prev, baseDataById, nodeState) {
149
160
  return sameRunState(n.data.state, nextData.state) ? n : { ...n, data: nextData };
150
161
  });
151
162
  }
163
+ function indexProblems(problems) {
164
+ const byNode = /* @__PURE__ */ new Map();
165
+ const byEdge = /* @__PURE__ */ new Map();
166
+ const named = (value) => typeof value === "string" && value !== "";
167
+ for (const problem of problems ?? []) {
168
+ const into = problem.anchor === "node" ? named(problem.node) ? byNode : null : problem.anchor === "edge" && named(problem.from) && named(problem.to) ? byEdge : null;
169
+ if (!into) continue;
170
+ const key = problem.anchor === "node" ? problem.node : wfEdgeId(problem.from, problem.to);
171
+ const at = into.get(key);
172
+ if (at) at.push(problem);
173
+ else into.set(key, [problem]);
174
+ }
175
+ return { byNode, byEdge };
176
+ }
177
+ var INSERT_CLEARANCE = 8;
178
+ var CLUSTER_HALF_SIZE = {
179
+ /** The insert control alone: a 20-unit square. */
180
+ control: { width: 10, height: 10 },
181
+ /**
182
+ * A problem chip — and, since the two stack in one column, the chip WITH the
183
+ * control under it, which is the taller of the two and so the one the bound
184
+ * has to cover. Measured on the rendered page at 160 x 38.5 flow units (the
185
+ * chip's `max-w-40` cap, over chip + gap + control), and rounded outward: a
186
+ * bound that understates is a bound that lets the cluster lap over a card.
187
+ */
188
+ chip: { width: 80, height: 20 }
189
+ };
190
+ var INSERT_MAX_CARDS_CROSSED = 2;
191
+ function clearOfNodeBoxes(point, boxes, direction, half) {
192
+ const isLR = direction === "LR";
193
+ const base = isLR ? point.y : point.x;
194
+ const main = isLR ? point.x : point.y;
195
+ const padMain = isLR ? half.width : half.height;
196
+ const padCross = isLR ? half.height : half.width;
197
+ const column = boxes.filter((b) => {
198
+ const lo = (isLR ? b.x : b.y) - padMain;
199
+ const size = (isLR ? b.width : b.height) + padMain * 2;
200
+ return main >= lo && main <= lo + size;
201
+ });
202
+ if (column.length === 0) return 0;
203
+ const walk = (sign) => {
204
+ let at = base;
205
+ for (let crossed = 0; crossed <= INSERT_MAX_CARDS_CROSSED; crossed += 1) {
206
+ const hit = column.find((b) => {
207
+ const lo2 = (isLR ? b.y : b.x) - padCross;
208
+ const size2 = (isLR ? b.height : b.width) + padCross * 2;
209
+ return at >= lo2 && at <= lo2 + size2;
210
+ });
211
+ if (!hit) return at - base;
212
+ if (crossed === INSERT_MAX_CARDS_CROSSED) return null;
213
+ const lo = isLR ? hit.y : hit.x;
214
+ const size = isLR ? hit.height : hit.width;
215
+ at = sign > 0 ? lo + size + padCross + INSERT_CLEARANCE : lo - padCross - INSERT_CLEARANCE;
216
+ }
217
+ return null;
218
+ };
219
+ const up = walk(-1);
220
+ const down = walk(1);
221
+ if (up === null && down === null) return null;
222
+ if (up === null) return down;
223
+ if (down === null) return up;
224
+ return Math.abs(up) <= Math.abs(down) ? up : down;
225
+ }
152
226
 
153
227
  // src/workflows/node-output.tsx
154
228
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -284,6 +358,7 @@ function NodeOutputBody({
284
358
 
285
359
  // src/workflows/node-ui.tsx
286
360
  import {
361
+ AlertTriangle,
287
362
  Bell,
288
363
  Box,
289
364
  Cable,
@@ -395,6 +470,73 @@ var KIND_ICON = {
395
470
  "script.run": Code,
396
471
  "trace.analyze": ScanSearch
397
472
  };
473
+ var PROBLEM_SURFACE_BY_SEVERITY = {
474
+ error: {
475
+ background: "var(--surface-danger-bg)",
476
+ color: "var(--surface-danger-text)",
477
+ borderColor: "var(--surface-danger-border)"
478
+ },
479
+ warning: {
480
+ background: "var(--surface-warning-bg)",
481
+ color: "var(--surface-warning-text)",
482
+ borderColor: "var(--surface-warning-border)"
483
+ }
484
+ };
485
+ function problemSurface(severity) {
486
+ return PROBLEM_SURFACE_BY_SEVERITY[severity] ?? PROBLEM_SURFACE_BY_SEVERITY.error;
487
+ }
488
+ function problemBorder(severity) {
489
+ const color = problemSurface(severity).color;
490
+ return {
491
+ borderColor: color,
492
+ boxShadow: `0 0 0 1px color-mix(in srgb, ${color} 35%, transparent)`
493
+ };
494
+ }
495
+ var SEVERITY_WORD = {
496
+ error: "Error",
497
+ warning: "Warning"
498
+ };
499
+ function problemSentence(problem) {
500
+ const raw = typeof problem.message === "string" ? problem.message : "";
501
+ const message = raw.split("\n").map((line) => line.trim()).filter(Boolean).join(" ");
502
+ const word = SEVERITY_WORD[problem.severity] ?? SEVERITY_WORD.error;
503
+ return message ? `${word}: ${message}` : word;
504
+ }
505
+ function joinSentences(parts) {
506
+ return parts.reduce(
507
+ (joined, part) => joined ? `${joined}${/[.!?;,]$/.test(joined) ? "" : "."} ${part}` : part,
508
+ ""
509
+ );
510
+ }
511
+ function problemTitle(problems) {
512
+ return problems.map(problemSentence).join("\n");
513
+ }
514
+ function ProblemMessages({ problems }) {
515
+ if (problems.length === 0) return null;
516
+ return /* @__PURE__ */ jsx2("span", { className: "sr-only", children: problems.length === 1 ? problemSentence(problems[0]) : `${problems.length} problems. ${joinSentences(problems.map(problemSentence))}` });
517
+ }
518
+ function ProblemMarker({
519
+ problems,
520
+ className = ""
521
+ }) {
522
+ const severity = worstSeverity(problems) ?? "error";
523
+ return /* @__PURE__ */ jsxs2(
524
+ "span",
525
+ {
526
+ "data-testid": "wf-node-problem",
527
+ "data-severity": severity,
528
+ className: `flex shrink-0 items-center gap-0.5 rounded-full border px-1 py-[1px] font-medium text-[10px] leading-none ${className}`,
529
+ style: problemSurface(severity),
530
+ children: [
531
+ /* @__PURE__ */ jsxs2("span", { "aria-hidden": true, className: "flex items-center gap-0.5", title: problemTitle(problems), children: [
532
+ /* @__PURE__ */ jsx2(AlertTriangle, { size: 9 }),
533
+ problems.length > 1 && /* @__PURE__ */ jsx2("span", { children: problems.length })
534
+ ] }),
535
+ /* @__PURE__ */ jsx2(ProblemMessages, { problems })
536
+ ]
537
+ }
538
+ );
539
+ }
398
540
  function StatusPill({ status }) {
399
541
  return /* @__PURE__ */ jsx2(
400
542
  "span",
@@ -567,6 +709,9 @@ var SELECTION_OUTLINE = {
567
709
  outlineOffset: 2
568
710
  };
569
711
  var ConnectableContext = createContext(false);
712
+ var NodeProblemsContext = createContext(/* @__PURE__ */ new Map());
713
+ var TriggerDeleteContext = createContext(null);
714
+ var EdgeInsertContext = createContext(null);
570
715
  function isEditableEdge(edge) {
571
716
  return edge.data?.kind !== "fork" && triggerNodeIndex(edge.source) === null;
572
717
  }
@@ -609,6 +754,10 @@ function WorkflowNode({
609
754
  const runMode = useContext(RunModeContext);
610
755
  const connectable = useContext(ConnectableContext);
611
756
  const hostSelection = useContext(SelectedNodeContext);
757
+ const problems = useContext(NodeProblemsContext).get(id);
758
+ const problemSeverity = worstSeverity(problems);
759
+ const removeTrigger = useContext(TriggerDeleteContext);
760
+ const isTrigger = triggerNodeIndex(id) !== null;
612
761
  const selected = hostSelection !== void 0 && hostSelection === id;
613
762
  const isLR = direction === "LR";
614
763
  const targetPos = targetPosition ?? (isLR ? Position2.Left : Position2.Top);
@@ -637,6 +786,21 @@ function WorkflowNode({
637
786
  );
638
787
  return shape.kind === "empty" ? null : { shape, tone: source.tone, label: source.label };
639
788
  })();
789
+ const triggerDeleteButton = isTrigger && removeTrigger ? /* @__PURE__ */ jsx3(
790
+ "button",
791
+ {
792
+ type: "button",
793
+ "data-testid": "wf-trigger-delete",
794
+ title: "Remove this trigger",
795
+ "aria-label": "Remove this trigger",
796
+ className: "nodrag nopan -m-1 flex h-6 w-6 shrink-0 items-center justify-center p-1 text-muted-foreground transition hover:text-[var(--surface-danger-text)]",
797
+ onClick: (event) => {
798
+ event.stopPropagation();
799
+ removeTrigger(id);
800
+ },
801
+ children: /* @__PURE__ */ jsx3("span", { className: "flex h-4 w-4 items-center justify-center rounded-full border border-border bg-card shadow-sm", children: /* @__PURE__ */ jsx3(X, { size: 10, "aria-hidden": true }) })
802
+ }
803
+ ) : null;
640
804
  const handleClass = connectable ? HANDLE_CONNECTABLE_CLASS : HANDLE_CLASS;
641
805
  const handles = /* @__PURE__ */ jsxs3(Fragment, { children: [
642
806
  !d.isRoot && /* @__PURE__ */ jsx3(
@@ -668,11 +832,12 @@ function WorkflowNode({
668
832
  /* @__PURE__ */ jsxs3(
669
833
  "span",
670
834
  {
835
+ "data-testid": "wf-node-card",
671
836
  className: "relative flex items-center justify-center rounded-xl border bg-card shadow-sm transition-colors",
672
837
  style: {
673
838
  width: COMPACT_TILE,
674
839
  height: COMPACT_TILE,
675
- ...state ? statusBorder(state.status) : {
840
+ ...state ? statusBorder(state.status) : problemSeverity ? problemBorder(problemSeverity) : {
676
841
  borderColor: `color-mix(in srgb, ${accent} 40%, hsl(var(--border)))`
677
842
  },
678
843
  // The TILE is the compact node (the name beside it is unboxed), so
@@ -690,6 +855,14 @@ function WorkflowNode({
690
855
  tile: Math.round(COMPACT_TILE * 0.62)
691
856
  }
692
857
  ) }),
858
+ problems && problemSeverity && /* @__PURE__ */ jsx3(
859
+ ProblemMarker,
860
+ {
861
+ problems,
862
+ className: "-left-1.5 -top-1.5 absolute"
863
+ }
864
+ ),
865
+ triggerDeleteButton && /* @__PURE__ */ jsx3("span", { className: "-right-1.5 -top-1.5 absolute", children: triggerDeleteButton }),
693
866
  d.badge && !state && /* @__PURE__ */ jsx3("span", { className: "-right-1.5 -top-1.5 absolute rounded-full border border-border bg-card px-1.5 py-[1px] font-medium text-[10px] text-muted-foreground", children: d.badge }),
694
867
  state && /* @__PURE__ */ jsx3(
695
868
  "span",
@@ -738,9 +911,10 @@ function WorkflowNode({
738
911
  const cardShell = (children) => /* @__PURE__ */ jsxs3(
739
912
  "div",
740
913
  {
914
+ "data-testid": "wf-node-card",
741
915
  className: "relative flex h-full w-full flex-col overflow-hidden rounded-xl border bg-card shadow-sm transition-colors",
742
916
  style: {
743
- ...runStatus ? statusBorder(runStatus) : {
917
+ ...runStatus ? statusBorder(runStatus) : problemSeverity ? problemBorder(problemSeverity) : {
744
918
  borderColor: `color-mix(in srgb, ${accent} 40%, hsl(var(--border)))`
745
919
  },
746
920
  ...selected ? SELECTION_OUTLINE : {}
@@ -780,7 +954,9 @@ function WorkflowNode({
780
954
  }
781
955
  )
782
956
  ] }),
783
- runStatus ? /* @__PURE__ */ jsx3(StatusPill, { status: runStatus }) : d.badge && /* @__PURE__ */ jsx3("span", { className: "shrink-0 rounded-full border border-border bg-surface-container-high px-2 py-[1px] font-medium text-[10px] text-muted-foreground", children: d.badge })
957
+ problems && problemSeverity && /* @__PURE__ */ jsx3(ProblemMarker, { problems }),
958
+ runStatus ? /* @__PURE__ */ jsx3(StatusPill, { status: runStatus }) : d.badge && /* @__PURE__ */ jsx3("span", { className: "shrink-0 rounded-full border border-border bg-surface-container-high px-2 py-[1px] font-medium text-[10px] text-muted-foreground", children: d.badge }),
959
+ triggerDeleteButton
784
960
  ] });
785
961
  const footer = runStatus ? /* @__PURE__ */ jsx3(
786
962
  StatusFooter,
@@ -981,9 +1157,25 @@ function WorkflowNode({
981
1157
  ] })
982
1158
  );
983
1159
  }
1160
+ function releasePoint(event) {
1161
+ if (!event) return null;
1162
+ const touch = "changedTouches" in event ? event.changedTouches[0] : null;
1163
+ const source = touch ?? ("clientX" in event ? event : null);
1164
+ return source ? { x: source.clientX, y: source.clientY } : null;
1165
+ }
1166
+ var CANVAS_SURFACE_SELECTOR = ".react-flow__pane, .react-flow__background";
1167
+ function releasedOnCanvas(at, frame) {
1168
+ if (!frame) return false;
1169
+ const under = typeof document === "undefined" ? null : document.elementFromPoint(at.x, at.y);
1170
+ if (!under?.matches(CANVAS_SURFACE_SELECTOR)) return false;
1171
+ return frame.contains(under);
1172
+ }
1173
+ var NodeBoxesContext = createContext([]);
984
1174
  var EDGE_CHIP_CLASS = "rounded-full border border-border bg-card/90 px-1.5 py-[1px] text-[10px] leading-tight backdrop-blur";
985
1175
  function WfEdgeRenderer({
986
1176
  id,
1177
+ source,
1178
+ target,
987
1179
  sourceX,
988
1180
  sourceY,
989
1181
  targetX,
@@ -994,6 +1186,7 @@ function WfEdgeRenderer({
994
1186
  markerEnd,
995
1187
  style
996
1188
  }) {
1189
+ const insertStep = useContext(EdgeInsertContext);
997
1190
  const [path, labelX, labelY] = getSmoothStepPath({
998
1191
  sourceX,
999
1192
  sourceY,
@@ -1005,6 +1198,25 @@ function WfEdgeRenderer({
1005
1198
  const backEdge = data?.backEdge === true;
1006
1199
  const whenLabel = data?.whenLabel;
1007
1200
  const visits = data?.maxNodeVisits;
1201
+ const problems = data?.problems;
1202
+ const problemSeverity = worstSeverity(problems);
1203
+ const offersInsert = data?.insertable === true && insertStep !== null;
1204
+ const nodeBoxes = useContext(NodeBoxesContext);
1205
+ const direction = useContext(DirectionContext);
1206
+ const cleared = offersInsert || problemSeverity ? clearOfNodeBoxes(
1207
+ { x: labelX, y: labelY },
1208
+ nodeBoxes,
1209
+ direction,
1210
+ // A chip is far wider than the control, and in "TB" that width is what
1211
+ // has to clear the cards — so the cluster is measured by what it
1212
+ // actually carries rather than by its tallest member.
1213
+ problemSeverity ? CLUSTER_HALF_SIZE.chip : CLUSTER_HALF_SIZE.control
1214
+ ) : 0;
1215
+ const clearance = cleared ?? 0;
1216
+ const stealsPointer = cleared === null;
1217
+ const isLR = direction === "LR";
1218
+ const clusterX = labelX + (isLR ? 0 : clearance);
1219
+ const clusterY = labelY + (isLR ? clearance : 0);
1008
1220
  return /* @__PURE__ */ jsxs3(Fragment, { children: [
1009
1221
  /* @__PURE__ */ jsx3(BaseEdge, { id, path, markerEnd, style }),
1010
1222
  /* @__PURE__ */ jsx3(EdgeLabelRenderer, { children: /* @__PURE__ */ jsxs3(
@@ -1012,9 +1224,23 @@ function WfEdgeRenderer({
1012
1224
  {
1013
1225
  className: "nodrag nopan absolute flex flex-col items-center gap-0.5",
1014
1226
  style: {
1015
- transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`
1227
+ zIndex: 1,
1228
+ transform: `translate(-50%, -50%) translate(${clusterX}px, ${clusterY}px)`
1016
1229
  },
1017
1230
  children: [
1231
+ problems && problemSeverity && /* @__PURE__ */ jsxs3(
1232
+ "span",
1233
+ {
1234
+ "data-testid": "wf-edge-problem",
1235
+ "data-severity": problemSeverity,
1236
+ className: `${EDGE_CHIP_CLASS} max-w-40 truncate ${stealsPointer ? "" : "pointer-events-auto"}`,
1237
+ style: problemSurface(problemSeverity),
1238
+ children: [
1239
+ /* @__PURE__ */ jsx3("span", { "aria-hidden": true, title: problemTitle(problems), children: problems.length === 1 ? problems[0].message : `${problems.length} problems` }),
1240
+ /* @__PURE__ */ jsx3(ProblemMessages, { problems })
1241
+ ]
1242
+ }
1243
+ ),
1018
1244
  whenLabel && /* @__PURE__ */ jsx3(
1019
1245
  "span",
1020
1246
  {
@@ -1030,6 +1256,21 @@ function WfEdgeRenderer({
1030
1256
  className: `${EDGE_CHIP_CLASS} shrink-0 text-foreground`,
1031
1257
  children: visits !== void 0 ? `\u21BA \u2264${visits}` : "\u21BA"
1032
1258
  }
1259
+ ),
1260
+ offersInsert && /* @__PURE__ */ jsx3(
1261
+ "button",
1262
+ {
1263
+ type: "button",
1264
+ "data-testid": "wf-edge-insert",
1265
+ title: "Insert a step here",
1266
+ "aria-label": "Insert a step here",
1267
+ className: `flex h-5 w-5 items-center justify-center rounded-full border border-border bg-card text-muted-foreground shadow-sm backdrop-blur transition hover:border-primary hover:text-primary ${stealsPointer ? "" : "pointer-events-auto"}`,
1268
+ onClick: (event) => {
1269
+ event.stopPropagation();
1270
+ insertStep(source, target);
1271
+ },
1272
+ children: /* @__PURE__ */ jsx3(Plus, { size: 12, "aria-hidden": true })
1273
+ }
1033
1274
  )
1034
1275
  ]
1035
1276
  }
@@ -1066,6 +1307,29 @@ function buildStyledEdges(base, nodeState, maxNodeVisits) {
1066
1307
  };
1067
1308
  });
1068
1309
  }
1310
+ function decorateAuthoringEdges(edges, byEdge, insertable, hasRunOverlay) {
1311
+ if (byEdge.size === 0 && !edges.some(insertable)) return edges;
1312
+ return edges.map((e) => {
1313
+ const problems = byEdge.get(e.id);
1314
+ const severity = worstSeverity(problems);
1315
+ const offersInsert = insertable(e);
1316
+ if (!severity && !offersInsert) return e;
1317
+ const stroke = severity && !hasRunOverlay ? problemSurface(severity).color : void 0;
1318
+ return {
1319
+ ...e,
1320
+ type: WF_EDGE_TYPE,
1321
+ data: {
1322
+ ...e.data ?? { kind: "spine" },
1323
+ ...problems ? { problems } : {},
1324
+ ...offersInsert ? { insertable: true } : {}
1325
+ },
1326
+ ...stroke ? {
1327
+ style: { ...e.style, stroke },
1328
+ markerEnd: e.markerEnd && typeof e.markerEnd === "object" ? { ...e.markerEnd, color: stroke } : e.markerEnd
1329
+ } : {}
1330
+ };
1331
+ });
1332
+ }
1069
1333
  function WorkflowGraph({
1070
1334
  yaml,
1071
1335
  variant = "full",
@@ -1080,9 +1344,19 @@ function WorkflowGraph({
1080
1344
  onNodeClick,
1081
1345
  onEdgeConnect,
1082
1346
  onEdgeDelete,
1083
- onEdgeClick
1347
+ onEdgeClick,
1348
+ onEdgeInsert,
1349
+ onNodeInsert,
1350
+ onTriggerAdd,
1351
+ onTriggerDelete,
1352
+ problems
1084
1353
  }) {
1085
1354
  const editable = onEdgeConnect !== void 0;
1355
+ const insertOnEdge = editable ? onEdgeInsert : void 0;
1356
+ const insertArmed = insertOnEdge !== void 0;
1357
+ const insertAtNode = editable ? onNodeInsert : void 0;
1358
+ const addTrigger = editable ? onTriggerAdd : void 0;
1359
+ const deleteTrigger = editable ? onTriggerDelete : void 0;
1086
1360
  const colorMode = useColorMode();
1087
1361
  const isPreview = variant === "preview";
1088
1362
  const [userCompact, setUserCompact] = useState(
@@ -1103,17 +1377,54 @@ function WorkflowGraph({
1103
1377
  direction,
1104
1378
  compact,
1105
1379
  wrap,
1380
+ // The insert control lives IN the gap between two layers, so the gap is
1381
+ // reserved here — where the layout is decided — rather than measured
1382
+ // afterwards. It is the one editing prop that reaches the layout, which
1383
+ // is why arming it re-lays the graph out.
1384
+ reserveEdgeInsert: insertArmed,
1106
1385
  ...declaredEdges ? { edges: declaredEdges } : {}
1107
1386
  }),
1108
- [yaml, hasRunOverlay, direction, compact, wrap, declaredEdges]
1387
+ [
1388
+ yaml,
1389
+ hasRunOverlay,
1390
+ direction,
1391
+ compact,
1392
+ wrap,
1393
+ insertArmed,
1394
+ declaredEdges
1395
+ ]
1109
1396
  );
1397
+ const nodeBoxes = useMemo(
1398
+ () => structural.nodes.map((node) => ({
1399
+ x: node.position.x,
1400
+ y: node.position.y,
1401
+ width: node.width ?? 0,
1402
+ height: node.height ?? 0
1403
+ })),
1404
+ [structural]
1405
+ );
1406
+ const problemIndex = useMemo(() => indexProblems(problems), [problems]);
1110
1407
  const styledEdges = useMemo(() => {
1111
- const styled = buildStyledEdges(structural.edges, nodeState, maxNodeVisits);
1408
+ const run = buildStyledEdges(structural.edges, nodeState, maxNodeVisits);
1409
+ const styled = decorateAuthoringEdges(
1410
+ run,
1411
+ problemIndex.byEdge,
1412
+ (edge) => insertArmed && isEditableEdge(edge),
1413
+ hasRunOverlay
1414
+ );
1112
1415
  return styled.map((e) => ({
1113
1416
  ...e,
1114
1417
  deletable: editable ? isEditableEdge(e) : false
1115
1418
  }));
1116
- }, [structural.edges, nodeState, maxNodeVisits, editable]);
1419
+ }, [
1420
+ structural.edges,
1421
+ nodeState,
1422
+ maxNodeVisits,
1423
+ editable,
1424
+ problemIndex,
1425
+ insertArmed,
1426
+ hasRunOverlay
1427
+ ]);
1117
1428
  const [nodes, setNodes, onNodesChange] = useNodesState(structural.nodes);
1118
1429
  const [edges, setEdges, onEdgesChange] = useEdgesState(styledEdges);
1119
1430
  const nodesRef = useRef(nodes);
@@ -1242,6 +1553,19 @@ function WorkflowGraph({
1242
1553
  },
1243
1554
  [onNodeClick]
1244
1555
  );
1556
+ const handleConnectEnd = useCallback(
1557
+ (event, state) => {
1558
+ if (!insertAtNode) return;
1559
+ if (state.toNode || state.toHandle) return;
1560
+ const from = state.fromNode;
1561
+ const fromHandle = state.fromHandle;
1562
+ if (!from || !fromHandle) return;
1563
+ const at = releasePoint(event);
1564
+ if (!at || !releasedOnCanvas(at, wrapperRef.current)) return;
1565
+ insertAtNode(from.id, fromHandle.type === "target" ? "before" : "after");
1566
+ },
1567
+ [insertAtNode]
1568
+ );
1245
1569
  const handleNodeKeyDown = useCallback(
1246
1570
  (event) => {
1247
1571
  if (event.key !== "Enter" && event.key !== " ") return;
@@ -1265,7 +1589,7 @@ function WorkflowGraph({
1265
1589
  }
1266
1590
  );
1267
1591
  }
1268
- return /* @__PURE__ */ jsx3(DirectionContext.Provider, { value: direction, children: /* @__PURE__ */ jsx3(DensityContext.Provider, { value: displayCompact, children: /* @__PURE__ */ jsx3(RunModeContext.Provider, { value: hasRunOverlay, children: /* @__PURE__ */ jsx3(SelectedNodeContext.Provider, { value: selectedNodeId, children: /* @__PURE__ */ jsx3(ConnectableContext.Provider, { value: editable, children: /* @__PURE__ */ jsx3("div", { ref: wrapperRef, className: `wf-graph ${className ?? ""}`, children: /* @__PURE__ */ jsxs3(
1592
+ return /* @__PURE__ */ jsx3(DirectionContext.Provider, { value: direction, children: /* @__PURE__ */ jsx3(DensityContext.Provider, { value: displayCompact, children: /* @__PURE__ */ jsx3(RunModeContext.Provider, { value: hasRunOverlay, children: /* @__PURE__ */ jsx3(SelectedNodeContext.Provider, { value: selectedNodeId, children: /* @__PURE__ */ jsx3(ConnectableContext.Provider, { value: editable, children: /* @__PURE__ */ jsx3(NodeBoxesContext.Provider, { value: nodeBoxes, children: /* @__PURE__ */ jsx3(NodeProblemsContext.Provider, { value: problemIndex.byNode, children: /* @__PURE__ */ jsx3(TriggerDeleteContext.Provider, { value: deleteTrigger ?? null, children: /* @__PURE__ */ jsx3(EdgeInsertContext.Provider, { value: insertOnEdge ?? null, children: /* @__PURE__ */ jsx3("div", { ref: wrapperRef, className: `wf-graph ${className ?? ""}`, children: /* @__PURE__ */ jsxs3(
1269
1593
  ReactFlow,
1270
1594
  {
1271
1595
  nodes,
@@ -1306,6 +1630,7 @@ function WorkflowGraph({
1306
1630
  }
1307
1631
  }
1308
1632
  } : void 0,
1633
+ onConnectEnd: insertAtNode ? handleConnectEnd : void 0,
1309
1634
  onEdgeClick: editable ? (_event, edge) => {
1310
1635
  if (isEditableEdge(edge)) {
1311
1636
  onEdgeClick?.(edge.source, edge.target);
@@ -1322,36 +1647,59 @@ function WorkflowGraph({
1322
1647
  /* @__PURE__ */ jsx3(Background, { gap: 18 }),
1323
1648
  !isPreview && /* @__PURE__ */ jsxs3(Fragment, { children: [
1324
1649
  /* @__PURE__ */ jsx3(Controls, { showInteractive: false, position: "bottom-right" }),
1325
- /* @__PURE__ */ jsx3(Panel, { position: "top-left", children: /* @__PURE__ */ jsxs3(
1326
- "button",
1327
- {
1328
- type: "button",
1329
- onClick: () => setUserCompact((c) => !c),
1330
- "aria-pressed": userCompact,
1331
- title: userCompact ? "Expand nodes" : "Collapse nodes",
1332
- className: "flex items-center gap-1 rounded-md border border-border bg-card/80 px-2 py-1 text-[11px] text-muted-foreground shadow-sm backdrop-blur transition hover:text-foreground",
1333
- children: [
1334
- userCompact ? /* @__PURE__ */ jsx3(Maximize2, { size: 12 }) : /* @__PURE__ */ jsx3(Minimize2, { size: 12 }),
1335
- userCompact ? "Expand" : "Compact"
1336
- ]
1337
- }
1338
- ) })
1650
+ /* @__PURE__ */ jsxs3(Panel, { position: "top-left", className: "flex items-center gap-1", children: [
1651
+ addTrigger && /* @__PURE__ */ jsxs3(
1652
+ "button",
1653
+ {
1654
+ type: "button",
1655
+ "data-testid": "wf-trigger-add",
1656
+ onClick: addTrigger,
1657
+ title: "Add a trigger",
1658
+ "aria-label": "Add a trigger",
1659
+ className: "flex items-center gap-1 rounded-md border border-border bg-card/80 px-2 py-1 text-[11px] text-muted-foreground shadow-sm backdrop-blur transition hover:text-foreground",
1660
+ children: [
1661
+ /* @__PURE__ */ jsx3(Plus, { size: 12, "aria-hidden": true }),
1662
+ "Trigger"
1663
+ ]
1664
+ }
1665
+ ),
1666
+ /* @__PURE__ */ jsxs3(
1667
+ "button",
1668
+ {
1669
+ type: "button",
1670
+ onClick: () => setUserCompact((c) => !c),
1671
+ "aria-pressed": userCompact,
1672
+ title: userCompact ? "Expand nodes" : "Collapse nodes",
1673
+ className: "flex items-center gap-1 rounded-md border border-border bg-card/80 px-2 py-1 text-[11px] text-muted-foreground shadow-sm backdrop-blur transition hover:text-foreground",
1674
+ children: [
1675
+ userCompact ? /* @__PURE__ */ jsx3(Maximize2, { size: 12 }) : /* @__PURE__ */ jsx3(Minimize2, { size: 12 }),
1676
+ userCompact ? "Expand" : "Compact"
1677
+ ]
1678
+ }
1679
+ )
1680
+ ] })
1339
1681
  ] })
1340
1682
  ]
1341
1683
  }
1342
- ) }) }) }) }) }) });
1684
+ ) }) }) }) }) }) }) }) }) }) });
1343
1685
  }
1344
1686
  export {
1345
1687
  ConnectableContext,
1346
1688
  DensityContext,
1347
1689
  DirectionContext,
1690
+ EdgeInsertContext,
1348
1691
  FIT_VIEW,
1349
1692
  LAYOUT_TRANSITION_MS,
1693
+ NodeBoxesContext,
1694
+ NodeProblemsContext,
1350
1695
  RunModeContext,
1351
1696
  SelectedNodeContext,
1697
+ TriggerDeleteContext,
1698
+ WfEdgeRenderer,
1352
1699
  WorkflowGraph,
1353
1700
  WorkflowNode,
1354
1701
  buildStyledEdges,
1702
+ decorateAuthoringEdges,
1355
1703
  fitZoomCeiling,
1356
1704
  fitZoomFloor,
1357
1705
  framingViewport,
@@ -230,10 +230,18 @@ function actionNodeId(index) {
230
230
  function branchNodeId(actionIndex, branchIndex) {
231
231
  return `${actionNodeId(actionIndex)}-b${branchIndex}`;
232
232
  }
233
+ function wfEdgeId(source, target) {
234
+ return `${source}->${target}`;
235
+ }
236
+ function worstSeverity(problems) {
237
+ if (!problems || problems.length === 0) return null;
238
+ return problems.every((p) => p.severity === "warning") ? "warning" : "error";
239
+ }
233
240
  var NODE_W = 292;
234
241
  var RANK_SEP = 72;
235
242
  var CROSS_SEP = 24;
236
243
  var EDGE_LABEL_LANE = 180;
244
+ var EDGE_INSERT_LANE = 44;
237
245
  var CARD_CHROME = 20;
238
246
  var HEADER_ROW = 34;
239
247
  var DESCRIPTION_ROW = 40;
@@ -814,7 +822,7 @@ function buildWorkflowGraph(yaml, options) {
814
822
  };
815
823
  const edgeIds = /* @__PURE__ */ new Set();
816
824
  const addEdge = (source, target, kind, whenLabel) => {
817
- const id = `${source}->${target}`;
825
+ const id = wfEdgeId(source, target);
818
826
  if (edgeIds.has(id)) return;
819
827
  edgeIds.add(id);
820
828
  edges.push({
@@ -902,7 +910,7 @@ function buildWorkflowGraph(yaml, options) {
902
910
  const labelled = edges.some(
903
911
  (e) => e.whenLabel !== void 0 || e.backEdge === true
904
912
  );
905
- const rankSep = labelled ? Math.max(geo.rankSep, EDGE_LABEL_LANE) : geo.rankSep;
913
+ const rankSep = labelled ? Math.max(geo.rankSep, EDGE_LABEL_LANE) : options?.reserveEdgeInsert ? Math.max(geo.rankSep, EDGE_INSERT_LANE) : geo.rankSep;
906
914
  const folded = options?.wrap && direction === "LR" && !labelled ? layoutSerpentine(logical, edges, rankSep, geo.crossSep) : null;
907
915
  return {
908
916
  nodes: folded ?? layoutLayers(logical, direction, rankSep, geo.crossSep),
@@ -923,6 +931,8 @@ export {
923
931
  triggerNodeIndex,
924
932
  actionNodeId,
925
933
  branchNodeId,
934
+ wfEdgeId,
935
+ worstSeverity,
926
936
  AGENT_BODY_ROWS,
927
937
  ACTION_OUTPUT_ROWS,
928
938
  STRUCTURAL_OUTPUT_ROWS,
package/dist/globals.css CHANGED
@@ -407,6 +407,9 @@
407
407
  .bottom-\[-0\.75rem\] {
408
408
  bottom: -0.75rem;
409
409
  }
410
+ .-left-1\.5 {
411
+ left: calc(var(--spacing) * -1.5);
412
+ }
410
413
  .left-0 {
411
414
  left: 0px;
412
415
  }
@@ -485,6 +488,9 @@
485
488
  max-width: 96rem;
486
489
  }
487
490
  }
491
+ .-m-1 {
492
+ margin: calc(var(--spacing) * -1);
493
+ }
488
494
  .m-4 {
489
495
  margin: calc(var(--spacing) * 4);
490
496
  }
@@ -3884,6 +3890,9 @@
3884
3890
  border-color: color-mix(in oklab, var(--color-foreground) 20%, transparent);
3885
3891
  }
3886
3892
  }
3893
+ .hover\:border-primary:hover {
3894
+ border-color: var(--color-primary);
3895
+ }
3887
3896
  .hover\:border-primary\/20:hover {
3888
3897
  border-color: color-mix(in srgb, hsl(var(--primary)) 20%, transparent);
3889
3898
  }
package/dist/styles.css CHANGED
@@ -407,6 +407,9 @@
407
407
  .bottom-\[-0\.75rem\] {
408
408
  bottom: -0.75rem;
409
409
  }
410
+ .-left-1\.5 {
411
+ left: calc(var(--spacing) * -1.5);
412
+ }
410
413
  .left-0 {
411
414
  left: 0px;
412
415
  }
@@ -485,6 +488,9 @@
485
488
  max-width: 96rem;
486
489
  }
487
490
  }
491
+ .-m-1 {
492
+ margin: calc(var(--spacing) * -1);
493
+ }
488
494
  .m-4 {
489
495
  margin: calc(var(--spacing) * 4);
490
496
  }
@@ -3884,6 +3890,9 @@
3884
3890
  border-color: color-mix(in oklab, var(--color-foreground) 20%, transparent);
3885
3891
  }
3886
3892
  }
3893
+ .hover\:border-primary:hover {
3894
+ border-color: var(--color-primary);
3895
+ }
3887
3896
  .hover\:border-primary\/20:hover {
3888
3897
  border-color: color-mix(in srgb, hsl(var(--primary)) 20%, transparent);
3889
3898
  }
@@ -206,6 +206,48 @@ interface WfEdgeSpec {
206
206
  to: string;
207
207
  whenLabel?: string;
208
208
  }
209
+ /** How serious an authoring problem is. An `error` stops the definition from
210
+ * compiling; a `warning` compiles but is worth saying out loud. */
211
+ type WfProblemSeverity = "error" | "warning";
212
+ /**
213
+ * One AUTHORING problem, anchored to the node or the edge that carries it.
214
+ *
215
+ * This is the DEFINITION-time counterpart of {@link WfNodeState}, and it is a
216
+ * separate channel on purpose: run state is what a node DID (status, cost,
217
+ * duration), while a problem is what is wrong with what it SAYS. Folding one
218
+ * into the other would make an unsaved compile error render as a failed run.
219
+ *
220
+ * The host supplies the message already written for a reader — this library
221
+ * renders graphs and never interprets a compiler's output. Anchors are node ids
222
+ * from THIS graph, so name them with the exported id helpers (`actionNodeId`,
223
+ * `branchNodeId`, `TRIGGER_NODE_ID`); an edge is named by its endpoints, the
224
+ * same way {@link WfEdgeSpec} names one.
225
+ *
226
+ * An anchor the graph has no slot for is simply not drawn — an authoring surface
227
+ * reports against a draft that is still moving, and a stale anchor must never
228
+ * blank the canvas the way a stale declared edge does. That is the ONLY safety
229
+ * this gives, and it is worth being exact about what it leaves out: these ids are
230
+ * POSITIONAL, so an insert, delete or reorder does not retire `a1` — it makes
231
+ * `a1` a different step. A problem computed against an earlier draft therefore
232
+ * survives the check and lands on whichever step now holds its position. Nothing
233
+ * here can tell the two apart, which is why the pairing contract on
234
+ * `WorkflowGraphProps.problems` is a contract and not a suggestion: a list must
235
+ * be handed over with the definition it was computed from.
236
+ */
237
+ type WfProblem = {
238
+ anchor: "node";
239
+ /** The node the problem sits on. */
240
+ node: string;
241
+ severity: WfProblemSeverity;
242
+ message: string;
243
+ } | {
244
+ anchor: "edge";
245
+ /** The edge's endpoints, as {@link WfEdgeSpec} names them. */
246
+ from: string;
247
+ to: string;
248
+ severity: WfProblemSeverity;
249
+ message: string;
250
+ };
209
251
  interface WfGraph {
210
252
  nodes: WfNode[];
211
253
  edges: WfEdge[];
@@ -252,6 +294,16 @@ interface BuildWorkflowGraphOptions {
252
294
  * worse than one that says it cannot be drawn.
253
295
  */
254
296
  edges?: readonly WfEdgeSpec[];
297
+ /**
298
+ * Widen the gap between layers so an edge has room for an in-line control at
299
+ * its midpoint — the insert affordance an EDITING canvas draws there. The
300
+ * compact densities pitch their layers at 20-34px, which is the whole button,
301
+ * so a canvas that renders one without asking for the lane puts it on top of
302
+ * the nodes either side. Reserved by the layout for the same reason run rows
303
+ * are: what a node or an edge renders is spaced for before it is drawn, never
304
+ * measured afterwards. Defaults to `false`.
305
+ */
306
+ reserveEdgeInsert?: boolean;
255
307
  }
256
308
  /** Build a positioned graph from a workflow YAML string. Never throws —
257
309
  * malformed YAML or an empty definition returns an `error` the UI can fall
@@ -323,9 +375,15 @@ interface WorkflowGraphProps {
323
375
  * Editing gestures. Supplying `onEdgeConnect` turns the canvas from a diagram
324
376
  * into an EDITOR: node handles become visible and draggable, an edge can be
325
377
  * selected and removed with Delete/Backspace, and clicking one asks to edit
326
- * its guard. Omit all three — the default — and the graph stays the read-only
378
+ * its guard. Omit it — the default — and the graph stays the read-only
327
379
  * visualisation it has always been.
328
380
  *
381
+ * `onEdgeConnect` is the ONE prop that decides it. Every other editing
382
+ * callback below (`onEdgeDelete`, `onEdgeClick`, `onEdgeInsert`,
383
+ * `onNodeInsert`, `onTriggerAdd`, `onTriggerDelete`) refines an editor and is
384
+ * inert without it — a canvas that cannot accept a new connection has no
385
+ * business drawing an add control either.
386
+ *
329
387
  * Every callback speaks node ids (`actionNodeId`, `branchNodeId`), never
330
388
  * positions: this component reports the gesture, and turning it into a
331
389
  * definition edit is the host's job — it owns the YAML, and only it knows
@@ -338,8 +396,83 @@ interface WorkflowGraphProps {
338
396
  onEdgeConnect?: (sourceId: string, targetId: string) => void;
339
397
  onEdgeDelete?: (sourceId: string, targetId: string) => void;
340
398
  onEdgeClick?: (sourceId: string, targetId: string) => void;
399
+ /**
400
+ * Add a step ON an edge: the edge draws a "+" at its midpoint, and pressing it
401
+ * reports the pair it sits between. The layout widens the corridor between
402
+ * layers to hold the control, so arming this RELAYS OUT the graph — a compact
403
+ * canvas pitches its layers at 20px, which is the whole button.
404
+ *
405
+ * Offered on exactly the edges the other three gestures are ({@link
406
+ * isEditableEdge}): inserting on a fan-out or trigger edge would name a pair
407
+ * that exists in no definition. "Add a step at the very start" is the
408
+ * {@link onNodeInsert} drop instead — dragged from the first step's inbound
409
+ * handle onto empty canvas, which names one node rather than a synthesized
410
+ * edge. (A definition with no `on:` at all has no inbound handle on its first
411
+ * step, because nothing points at it; there the step list is the only way in.)
412
+ */
413
+ onEdgeInsert?: (sourceId: string, targetId: string) => void;
414
+ /**
415
+ * Add a step at the loose end of a connection dragged from a node's handle and
416
+ * released over empty canvas — the canvas's answer to "and then what?".
417
+ * `side` is which handle it left from: `"after"` for the outbound one (the new
418
+ * step follows `nodeId`), `"before"` for the inbound one (it precedes it).
419
+ *
420
+ * Reported for ANY node, including a trigger and a fan-out branch leaf: unlike
421
+ * an edge gesture, this names a node that certainly exists, and whether a step
422
+ * may go beside it is a question about the definition's schema — which this
423
+ * library does not model and the host already answers.
424
+ */
425
+ onNodeInsert?: (nodeId: string, side: "before" | "after") => void;
426
+ /**
427
+ * The `on:` list gains an entry. Drawn as a canvas control rather than a
428
+ * per-node one, because the workflow that most needs it is the one with NO
429
+ * trigger — which has no trigger node to hang a "+" on.
430
+ *
431
+ * Offered whenever it is supplied, including on a definition that ALREADY has
432
+ * a trigger: `on:` is a list with OR semantics, and a second subscription is a
433
+ * normal thing to want. Whether THIS workflow may take another one is a
434
+ * question about the definition's schema and the provider behind it, which
435
+ * this library does not model — a host that must cap the list withholds the
436
+ * callback.
437
+ */
438
+ onTriggerAdd?: () => void;
439
+ /** Remove the `on:` entry a trigger node stands for — the one node deletion
440
+ * that IS a canvas gesture (every other node is a `do` entry, reordered and
441
+ * removed in the list the host owns). Read the entry's position back with
442
+ * `triggerNodeIndex`. */
443
+ onTriggerDelete?: (nodeId: string) => void;
444
+ /**
445
+ * AUTHORING problems anchored to the nodes and edges that carry them, so a
446
+ * compile error is visible on the canvas rather than only in a list beside it.
447
+ * See {@link WfProblem} — and note it is a separate channel from `nodeState`,
448
+ * which is what a node DID rather than what is wrong with what it says.
449
+ *
450
+ * A problem tints the node's border in either density, and states itself on a
451
+ * mark in the card's identity row (a corner of the compact tile). A graph
452
+ * showing a RUN is not an authoring surface — its cards and edges state the
453
+ * run instead, which takes precedence over a problem's colour on both, and the
454
+ * problem's mark and chip still render beside it.
455
+ *
456
+ * What the canvas gives is a LOCATOR: which step or dependency is at fault,
457
+ * with the messages on the mark's `title` and in the accessibility tree. It is
458
+ * deliberately not a reader for them — at the zoom a whole pipeline is viewed
459
+ * at, a panel of message text on the canvas is unreadable, and the host
460
+ * already owns the problem list that the messages are read and acted on in.
461
+ *
462
+ * Pairing contract: anchors are positional node ids, so a problem list must be
463
+ * derived from the SAME definition passed as `yaml`. Hand over a list computed
464
+ * against an older draft and an id that still exists may now name a different
465
+ * step, which puts the diagnostic on an innocent one — the graph cannot tell
466
+ * the two apart, any more than it can for a run's `nodeState`. A host that
467
+ * validates asynchronously therefore holds each result against the text it was
468
+ * computed from, and passes neither until they agree.
469
+ *
470
+ * Immutability contract, as for `edges`: the index is memoized on this array's
471
+ * reference, so pass a stable one.
472
+ */
473
+ problems?: readonly WfProblem[];
341
474
  }
342
475
 
343
476
  declare function WorkflowGraph(props: WorkflowGraphProps): React.JSX.Element;
344
477
 
345
- export { type BuildWorkflowGraphOptions, TRIGGER_NODE_ID, type WfEdge, type WfEdgeKind, type WfEdgeSpec, type WfGraph, type WfNode, type WfNodeData, type WfNodeState, type WfNodeStatus, type WfNodeTone, WorkflowGraph as WorkflowGraphLazy, type WorkflowGraphProps, actionNodeId, branchNodeId, buildWorkflowGraph, triggerNodeId, triggerNodeIndex };
478
+ export { type BuildWorkflowGraphOptions, TRIGGER_NODE_ID, type WfEdge, type WfEdgeKind, type WfEdgeSpec, type WfGraph, type WfNode, type WfNodeData, type WfNodeState, type WfNodeStatus, type WfNodeTone, type WfProblem, type WfProblemSeverity, WorkflowGraph as WorkflowGraphLazy, type WorkflowGraphProps, actionNodeId, branchNodeId, buildWorkflowGraph, triggerNodeId, triggerNodeIndex };
package/dist/workflows.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  buildWorkflowGraph,
6
6
  triggerNodeId,
7
7
  triggerNodeIndex
8
- } from "./chunk-LHUBJPZR.js";
8
+ } from "./chunk-AI25G5MX.js";
9
9
  import {
10
10
  ModelBrandStack,
11
11
  modelBrandFor
@@ -36,7 +36,7 @@ function retryImport(factory, retries = 2, delayMs = 350) {
36
36
  function lazyWorkflowGraph() {
37
37
  return lazy(
38
38
  () => retryImport(
39
- () => import("./WorkflowGraph-BJ5ZSDZB.js").then((m) => ({ default: m.WorkflowGraph }))
39
+ () => import("./WorkflowGraph-7AFEKG7E.js").then((m) => ({ default: m.WorkflowGraph }))
40
40
  )
41
41
  );
42
42
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/sandbox-ui",
3
- "version": "0.109.0",
3
+ "version": "0.110.0",
4
4
  "description": "Unified UI component library for Tangle Sandbox — primitives, chat, dashboard, terminal, editor, and workspace components",
5
5
  "repository": {
6
6
  "type": "git",