@hlang-org/editor-ui 0.3.0 → 0.5.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.
package/lib/index.js CHANGED
@@ -25,30 +25,51 @@ __export(index_exports, {
25
25
  CanvasChrome: () => CanvasChrome,
26
26
  CanvasDock: () => CanvasDock,
27
27
  Cheatsheet: () => Cheatsheet,
28
+ ClearIcon: () => ClearIcon,
29
+ ConnectionInspector: () => ConnectionInspector,
30
+ DEFAULT_EDGE_CAPACITY: () => DEFAULT_EDGE_CAPACITY,
31
+ DEFAULT_MIN_CANVAS_HEIGHT: () => DEFAULT_MIN_CANVAS_HEIGHT,
28
32
  DiagnosticBadge: () => DiagnosticBadge,
29
33
  EditorShell: () => EditorShell,
30
34
  EditorUIThemeProvider: () => EditorUIThemeProvider,
31
35
  EmptyState: () => EmptyState,
36
+ FOLLOW_SLACK_PX: () => FOLLOW_SLACK_PX,
37
+ FOOTER_KEY_STEP: () => FOOTER_KEY_STEP,
38
+ FilterIcon: () => FilterIcon,
32
39
  FlowEdge: () => FlowEdge,
40
+ FollowIcon: () => FollowIcon,
41
+ FooterResizer: () => FooterResizer,
33
42
  HlangNode: () => HlangNode,
34
43
  InspectorPanel: () => InspectorPanel,
35
44
  KIND_ROLE: () => KIND_ROLE,
36
45
  KIND_TOKEN: () => KIND_TOKEN,
46
+ LivePulse: () => LivePulse,
37
47
  LiveSignalProvider: () => LiveSignalProvider,
38
48
  LiveSignalStore: () => LiveSignalStore,
39
49
  NodeBodySlot: () => NodeBodySlot,
40
50
  NodeChrome: () => NodeChrome,
41
51
  NodeContextMenu: () => NodeContextMenu,
42
52
  NodeInteractionProvider: () => NodeInteractionProvider,
53
+ NodeRunStats: () => NodeRunStats,
43
54
  NodeSearch: () => NodeSearch,
44
55
  OutlineTree: () => OutlineTree,
56
+ PREVIEW_LENGTH: () => PREVIEW_LENGTH,
57
+ PanelAction: () => PanelAction,
58
+ PanelHead: () => PanelHead,
45
59
  ParamForm: () => ParamForm,
46
60
  PillNodeChrome: () => PillNodeChrome,
47
61
  PortRow: () => PortRow,
48
62
  PositionPanel: () => PositionPanel,
49
63
  PreviewPanel: () => PreviewPanel,
64
+ RUN_LOG_CAPACITY: () => RUN_LOG_CAPACITY,
50
65
  RailTabs: () => RailTabs,
66
+ ReadOnlyBadge: () => ReadOnlyBadge,
51
67
  RunButton: () => RunButton,
68
+ RunLog: () => RunLog,
69
+ RunTrace: () => RunTrace,
70
+ RunTraceProvider: () => RunTraceProvider,
71
+ SELF_LOOP_CLEARANCE: () => SELF_LOOP_CLEARANCE,
72
+ SELF_LOOP_RADIUS: () => SELF_LOOP_RADIUS,
52
73
  SignalBar: () => SignalBar,
53
74
  TOKEN_NAMES: () => TOKEN_NAMES,
54
75
  ToastProvider: () => ToastProvider,
@@ -57,26 +78,47 @@ __export(index_exports, {
57
78
  ToolbarGhostButton: () => ToolbarGhostButton,
58
79
  ToolbarStat: () => ToolbarStat,
59
80
  UndoRedo: () => UndoRedo,
81
+ boundsFields: () => boundsFields,
82
+ capacityLabel: () => capacityLabel,
83
+ clampFooter: () => clampFooter,
60
84
  connectableCandidates: () => connectableCandidates,
61
85
  createHlangFlowTypes: () => createHlangFlowTypes,
62
86
  createTheme: () => createTheme,
87
+ defaultLiveDeps: () => defaultLiveDeps,
63
88
  deriveParamSchema: () => deriveParamSchema,
64
89
  descriptorCandidates: () => descriptorCandidates,
90
+ footerBounds: () => footerBounds,
91
+ formatDuration: () => formatDuration,
92
+ formatElapsed: () => formatElapsed,
65
93
  fuzzyRank: () => fuzzyRank,
66
94
  fuzzyScore: () => fuzzyScore,
95
+ involves: () => involves,
67
96
  isEditableTarget: () => isEditableTarget,
97
+ nextBounds: () => nextBounds,
98
+ previewValue: () => previewValue,
99
+ resizeByKey: () => resizeByKey,
68
100
  searchNodes: () => searchNodes,
101
+ selfLoopPath: () => selfLoopPath,
102
+ toggleCollapsed: () => toggleCollapsed,
69
103
  tokenVar: () => tokenVar,
104
+ useEdgeBounds: () => useEdgeBounds,
70
105
  useEdgeFlowing: () => useEdgeFlowing,
106
+ useEdgePulse: () => useEdgePulse,
71
107
  useEditorHotkeys: () => useEditorHotkeys,
108
+ useFollowScroll: () => useFollowScroll,
109
+ useFooterResize: () => useFooterResize,
72
110
  useLiveSignalStore: () => useLiveSignalStore,
73
111
  useLiveSignals: () => useLiveSignals,
74
112
  useNodeContextMenu: () => useNodeContextMenu,
75
113
  useNodeInteraction: () => useNodeInteraction,
76
114
  useNodeLiveState: () => useNodeLiveState,
77
115
  useNodeParams: () => useNodeParams,
116
+ useNodePulse: () => useNodePulse,
78
117
  useNodeSearch: () => useNodeSearch,
79
118
  useNodeSearchTriggers: () => useNodeSearchTriggers,
119
+ useRunTrace: () => useRunTrace,
120
+ useRunTraceStore: () => useRunTraceStore,
121
+ useRunTraceVersion: () => useRunTraceVersion,
80
122
  useToast: () => useToast
81
123
  });
82
124
  module.exports = __toCommonJS(index_exports);
@@ -149,12 +191,12 @@ function createTheme(overrides) {
149
191
  var import_jsx_runtime = require("react/jsx-runtime");
150
192
  function EditorUIThemeProvider({
151
193
  theme,
152
- className,
194
+ className: className2,
153
195
  style,
154
196
  children
155
197
  }) {
156
198
  const themeStyle = theme ? createTheme(theme) : void 0;
157
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className, style: { ...themeStyle, ...style }, children });
199
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: className2, style: { ...themeStyle, ...style }, children });
158
200
  }
159
201
 
160
202
  // src/nodes/SignalBar.tsx
@@ -204,7 +246,10 @@ var ACTIVE_WINDOW_MS = 700;
204
246
  function portKey(nodeId, portId) {
205
247
  return `${nodeId}@${portId}`;
206
248
  }
207
- var defaultDeps = {
249
+ function connectionKey(sourceNodeId, sourcePortId, targetNodeId, targetPortId) {
250
+ return `${portKey(sourceNodeId, sourcePortId)}->${portKey(targetNodeId, targetPortId)}`;
251
+ }
252
+ var defaultLiveDeps = {
208
253
  now: () => typeof performance !== "undefined" ? performance.now() : Date.now(),
209
254
  schedule: (cb) => typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame(cb) : (
210
255
  // `window.setTimeout` returns a numeric handle (the DOM overload),
@@ -215,11 +260,13 @@ var defaultDeps = {
215
260
  cancel: (h) => typeof cancelAnimationFrame !== "undefined" ? cancelAnimationFrame(h) : window.clearTimeout(h)
216
261
  };
217
262
  var LiveSignalStore = class {
218
- constructor(deps = defaultDeps) {
263
+ constructor(deps = defaultLiveDeps) {
219
264
  /** nodeId -> last-active timestamp. */
220
265
  this.nodeActiveAt = /* @__PURE__ */ new Map();
221
266
  /** nodeId set that errored during this run. */
222
267
  this.erroredNodes = /* @__PURE__ */ new Set();
268
+ /** nodeId set that finished during this run (RFC 0001). */
269
+ this.finishedNodes = /* @__PURE__ */ new Set();
223
270
  /** edgeId -> last-flowing timestamp. */
224
271
  this.edgeFlowAt = /* @__PURE__ */ new Map();
225
272
  /** Per-id listeners. */
@@ -230,12 +277,21 @@ var LiveSignalStore = class {
230
277
  this.lastEdgeFlowing = /* @__PURE__ */ new Map();
231
278
  /** Edges sourced by a given (nodeId@portId), for pulse -> edge mapping. */
232
279
  this.edgesBySourcePort = /* @__PURE__ */ new Map();
280
+ /** Edges by the connection they carry (source port -> target port). */
281
+ this.edgesByConnection = /* @__PURE__ */ new Map();
282
+ /** How many messages each node has handled or sent, and each edge carried, this run. */
283
+ this.nodePulse = /* @__PURE__ */ new Map();
284
+ this.edgePulse = /* @__PURE__ */ new Map();
285
+ /** The pulse counts last notified, so a new message notifies even while the state stays active. */
286
+ this.lastNodePulse = /* @__PURE__ */ new Map();
287
+ this.lastEdgePulse = /* @__PURE__ */ new Map();
233
288
  this.rafHandle = null;
234
289
  this.running = false;
235
290
  this.deps = deps;
236
291
  }
237
292
  /** Provide the current edge index (call whenever edges change). */
238
293
  setEdgeIndex(edges) {
294
+ var _a;
239
295
  const next = /* @__PURE__ */ new Map();
240
296
  for (const edge of edges) {
241
297
  const key = portKey(edge.sourceNodeId, edge.sourcePortId);
@@ -247,6 +303,44 @@ var LiveSignalStore = class {
247
303
  }
248
304
  }
249
305
  this.edgesBySourcePort = next;
306
+ const byConnection = /* @__PURE__ */ new Map();
307
+ for (const edge of edges) {
308
+ if (edge.targetNodeId !== void 0 && edge.targetPortId !== void 0) {
309
+ const key = connectionKey(edge.sourceNodeId, edge.sourcePortId, edge.targetNodeId, edge.targetPortId);
310
+ byConnection.set(key, [...(_a = byConnection.get(key)) != null ? _a : [], edge]);
311
+ }
312
+ }
313
+ this.edgesByConnection = byConnection;
314
+ }
315
+ /**
316
+ * Record a message delivered on a connection, when it happens: its source
317
+ * and its target each pulse once (each ran), and so does the edge that
318
+ * carried it, and only that edge. Subscribers are notified on the next
319
+ * frame, once however many messages arrived in it.
320
+ */
321
+ markDelivery(signal) {
322
+ var _a;
323
+ const t = this.deps.now();
324
+ for (const nodeId of [signal.sourceNodeId, signal.targetNodeId]) {
325
+ this.nodeActiveAt.set(nodeId, t);
326
+ this.nodePulse.set(nodeId, this.getNodePulse(nodeId) + 1);
327
+ }
328
+ const key = connectionKey(signal.sourceNodeId, signal.sourcePortId, signal.targetNodeId, signal.targetPortId);
329
+ for (const edge of (_a = this.edgesByConnection.get(key)) != null ? _a : []) {
330
+ this.edgeFlowAt.set(edge.id, t);
331
+ this.edgePulse.set(edge.id, this.getEdgePulse(edge.id) + 1);
332
+ }
333
+ this.ensureLoop();
334
+ }
335
+ /** How many messages a node has handled or sent this run; it changes each time the node runs. */
336
+ getNodePulse(nodeId) {
337
+ var _a;
338
+ return (_a = this.nodePulse.get(nodeId)) != null ? _a : 0;
339
+ }
340
+ /** How many messages an edge has carried this run. */
341
+ getEdgePulse(edgeId) {
342
+ var _a;
343
+ return (_a = this.edgePulse.get(edgeId)) != null ? _a : 0;
250
344
  }
251
345
  /** Record a node emitting on an output port (drives node + edge activity). */
252
346
  markActivity(nodeId, portId) {
@@ -260,6 +354,11 @@ var LiveSignalStore = class {
260
354
  }
261
355
  this.ensureLoop();
262
356
  }
357
+ /** Record a node finishing (RFC 0001): shown once its last flash has faded. */
358
+ markFinished(nodeId) {
359
+ this.finishedNodes.add(nodeId);
360
+ this.ensureLoop();
361
+ }
263
362
  /** Record a node erroring (sticky for the run). */
264
363
  markError(nodeId) {
265
364
  this.erroredNodes.add(nodeId);
@@ -269,7 +368,10 @@ var LiveSignalStore = class {
269
368
  reset() {
270
369
  this.nodeActiveAt.clear();
271
370
  this.erroredNodes.clear();
371
+ this.finishedNodes.clear();
272
372
  this.edgeFlowAt.clear();
373
+ this.nodePulse.clear();
374
+ this.edgePulse.clear();
273
375
  this.stopLoop();
274
376
  this.flushNotifications();
275
377
  }
@@ -290,7 +392,7 @@ var LiveSignalStore = class {
290
392
  if (at !== void 0 && this.deps.now() - at <= ACTIVE_WINDOW_MS) {
291
393
  return "active";
292
394
  }
293
- return "idle";
395
+ return this.finishedNodes.has(nodeId) ? "finished" : "idle";
294
396
  }
295
397
  /** Whether an edge is currently flowing. */
296
398
  isEdgeFlowing(edgeId) {
@@ -350,8 +452,10 @@ var LiveSignalStore = class {
350
452
  if (state !== "idle") {
351
453
  anyLive = anyLive || state === "active";
352
454
  }
353
- if (this.lastNodeState.get(nodeId) !== state) {
455
+ const pulse = this.getNodePulse(nodeId);
456
+ if (this.lastNodeState.get(nodeId) !== state || this.lastNodePulse.get(nodeId) !== pulse) {
354
457
  this.lastNodeState.set(nodeId, state);
458
+ this.lastNodePulse.set(nodeId, pulse);
355
459
  for (const listener of listeners) {
356
460
  listener();
357
461
  }
@@ -360,8 +464,10 @@ var LiveSignalStore = class {
360
464
  for (const [edgeId, listeners] of this.edgeListeners) {
361
465
  const flowing = this.isEdgeFlowing(edgeId);
362
466
  anyLive = anyLive || flowing;
363
- if (this.lastEdgeFlowing.get(edgeId) !== flowing) {
467
+ const pulse = this.getEdgePulse(edgeId);
468
+ if (this.lastEdgeFlowing.get(edgeId) !== flowing || this.lastEdgePulse.get(edgeId) !== pulse) {
364
469
  this.lastEdgeFlowing.set(edgeId, flowing);
470
+ this.lastEdgePulse.set(edgeId, pulse);
365
471
  for (const listener of listeners) {
366
472
  listener();
367
473
  }
@@ -389,6 +495,22 @@ function useNodeLiveState(nodeId) {
389
495
  () => "idle"
390
496
  );
391
497
  }
498
+ function useNodePulse(nodeId) {
499
+ const store = (0, import_react2.useContext)(LiveSignalContext);
500
+ return (0, import_react2.useSyncExternalStore)(
501
+ (onChange) => store === null ? () => void 0 : store.subscribeNode(nodeId, onChange),
502
+ () => store === null ? 0 : store.getNodePulse(nodeId),
503
+ () => 0
504
+ );
505
+ }
506
+ function useEdgePulse(edgeId) {
507
+ const store = (0, import_react2.useContext)(LiveSignalContext);
508
+ return (0, import_react2.useSyncExternalStore)(
509
+ (onChange) => store === null ? () => void 0 : store.subscribeEdge(edgeId, onChange),
510
+ () => store === null ? 0 : store.getEdgePulse(edgeId),
511
+ () => 0
512
+ );
513
+ }
392
514
  function useEdgeFlowing(edgeId) {
393
515
  const store = (0, import_react2.useContext)(LiveSignalContext);
394
516
  return (0, import_react2.useSyncExternalStore)(
@@ -398,8 +520,15 @@ function useEdgeFlowing(edgeId) {
398
520
  );
399
521
  }
400
522
 
401
- // src/nodes/AssetNode.tsx
523
+ // src/live/LivePulse.tsx
402
524
  var import_jsx_runtime6 = require("react/jsx-runtime");
525
+ function LivePulse({ nodeId }) {
526
+ const pulse = useNodePulse(nodeId);
527
+ return pulse === 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "hl-live-pulse", "aria-hidden": "true" }, pulse);
528
+ }
529
+
530
+ // src/nodes/AssetNode.tsx
531
+ var import_jsx_runtime7 = require("react/jsx-runtime");
403
532
  function stringParam(node, key) {
404
533
  const v = node.params[key];
405
534
  return typeof v === "string" ? v : void 0;
@@ -411,22 +540,24 @@ function AssetNodeChrome({ node, selected }) {
411
540
  const role = stringParam(node, "role");
412
541
  const live = useNodeLiveState(node.id);
413
542
  const outputs = node.ports.filter((p) => p.direction === "O" && !p.isError);
414
- const className = [
543
+ const className2 = [
415
544
  "hl-asset",
416
545
  selected ? "is-selected" : "",
417
546
  live === "active" ? "is-active" : "",
418
- live === "error" ? "is-live-error" : ""
547
+ live === "error" ? "is-live-error" : "",
548
+ live === "finished" ? "is-finished" : ""
419
549
  ].filter(Boolean).join(" ");
420
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className, "data-kind": node.kind, children: [
421
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "hl-asset-frame", children: [
422
- imageUrl.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("img", { className: "hl-asset-img", src: imageUrl, alt: caption, draggable: false }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "hl-asset-placeholder", "aria-hidden": true }),
423
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "hl-asset-scrim" }),
424
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "hl-asset-caption", children: [
425
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "hl-asset-name", children: caption }),
426
- role !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "hl-asset-role hl-mono", children: role })
550
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: className2, "data-kind": node.kind, children: [
551
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(LivePulse, { nodeId: node.id }),
552
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "hl-asset-frame", children: [
553
+ imageUrl.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("img", { className: "hl-asset-img", src: imageUrl, alt: caption, draggable: false }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "hl-asset-placeholder", "aria-hidden": true }),
554
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "hl-asset-scrim" }),
555
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "hl-asset-caption", children: [
556
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "hl-asset-name", children: caption }),
557
+ role !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "hl-asset-role hl-mono", children: role })
427
558
  ] })
428
559
  ] }),
429
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("ul", { className: "hl-asset-ports", children: outputs.map((port) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PortRow, { port }, port.id)) })
560
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("ul", { className: "hl-asset-ports", children: outputs.map((port) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(PortRow, { port }, port.id)) })
430
561
  ] });
431
562
  }
432
563
 
@@ -434,9 +565,9 @@ function AssetNodeChrome({ node, selected }) {
434
565
  var import_react4 = require("react");
435
566
 
436
567
  // src/icons/node-icons.tsx
437
- var import_jsx_runtime7 = require("react/jsx-runtime");
568
+ var import_jsx_runtime8 = require("react/jsx-runtime");
438
569
  function svg(size, children) {
439
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
570
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
440
571
  "svg",
441
572
  {
442
573
  width: size,
@@ -454,49 +585,49 @@ function svg(size, children) {
454
585
  }
455
586
  var BY_CODE = {
456
587
  "image-source": (s) => svg(s, [
457
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("rect", { x: "3", y: "4", width: "18", height: "16", rx: "2.5" }, "r"),
458
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("circle", { cx: "8.5", cy: "9", r: "1.6" }, "c"),
459
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M4 17l5-5 4 4 3-2 4 4" }, "p")
588
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("rect", { x: "3", y: "4", width: "18", height: "16", rx: "2.5" }, "r"),
589
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("circle", { cx: "8.5", cy: "9", r: "1.6" }, "c"),
590
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M4 17l5-5 4 4 3-2 4 4" }, "p")
460
591
  ]),
461
- "text-source": (s) => svg(s, [/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M5 6h14M12 6v12M9 18h6" }, "p")]),
592
+ "text-source": (s) => svg(s, [/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M5 6h14M12 6v12M9 18h6" }, "p")]),
462
593
  "interval-source": (s) => svg(s, [
463
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("circle", { cx: "12", cy: "12", r: "8" }, "c"),
464
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M12 8v4l3 2" }, "h")
594
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("circle", { cx: "12", cy: "12", r: "8" }, "c"),
595
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M12 8v4l3 2" }, "h")
465
596
  ]),
466
597
  "range-source": (s) => svg(s, [
467
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M4 12h16" }, "l"),
468
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("circle", { cx: "6", cy: "12", r: "2" }, "a"),
469
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("circle", { cx: "18", cy: "12", r: "2" }, "b")
598
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M4 12h16" }, "l"),
599
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("circle", { cx: "6", cy: "12", r: "2" }, "a"),
600
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("circle", { cx: "18", cy: "12", r: "2" }, "b")
470
601
  ]),
471
- "constant-source": (s) => svg(s, [/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M6 12h12M9 8l-3 4 3 4M15 8l3 4-3 4" }, "p")]),
602
+ "constant-source": (s) => svg(s, [/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M6 12h12M9 8l-3 4 3 4M15 8l3 4-3 4" }, "p")]),
472
603
  "map-scale": (s) => svg(s, [
473
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M4 20V8l8-4" }, "a"),
474
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M4 8l8 4 8-4M12 12v8" }, "b")
604
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M4 20V8l8-4" }, "a"),
605
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M4 8l8 4 8-4M12 12v8" }, "b")
475
606
  ]),
476
- uppercase: (s) => svg(s, [/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M5 18L9 6l4 12M6.5 14h5M15 10v8M15 10c3 0 3 4 0 4" }, "p")]),
477
- "filter-threshold": (s) => svg(s, [/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M4 5h16l-6 7v6l-4-2v-4z" }, "p")]),
478
- "scan-sum": (s) => svg(s, [/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M17 6H7l6 6-6 6h10" }, "p")]),
607
+ uppercase: (s) => svg(s, [/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M5 18L9 6l4 12M6.5 14h5M15 10v8M15 10c3 0 3 4 0 4" }, "p")]),
608
+ "filter-threshold": (s) => svg(s, [/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M4 5h16l-6 7v6l-4-2v-4z" }, "p")]),
609
+ "scan-sum": (s) => svg(s, [/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M17 6H7l6 6-6 6h10" }, "p")]),
479
610
  delay: (s) => svg(s, [
480
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("circle", { cx: "12", cy: "13", r: "7" }, "c"),
481
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M12 10v3l2 2M9 3h6" }, "h")
611
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("circle", { cx: "12", cy: "13", r: "7" }, "c"),
612
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M12 10v3l2 2M9 3h6" }, "h")
482
613
  ]),
483
- merge: (s) => svg(s, [/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M6 5v4c0 3 3 3 6 3s6 0 6-3V5M12 12v7" }, "p")]),
614
+ merge: (s) => svg(s, [/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M6 5v4c0 3 3 3 6 3s6 0 6-3V5M12 12v7" }, "p")]),
484
615
  "console-sink": (s) => svg(s, [
485
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("rect", { x: "4", y: "5", width: "16", height: "12", rx: "2" }, "r"),
486
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M8 10l2.5 2L8 14M13 14h3" }, "p")
616
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("rect", { x: "4", y: "5", width: "16", height: "12", rx: "2" }, "r"),
617
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M8 10l2.5 2L8 14M13 14h3" }, "p")
487
618
  ]),
488
619
  "collect-sink": (s) => svg(s, [
489
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M4 7h16M4 12h16M4 17h10" }, "p")
620
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M4 7h16M4 12h16M4 17h10" }, "p")
490
621
  ])
491
622
  };
492
623
  var BY_KIND = {
493
- R: (s) => svg(s, [/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M8 5l10 7-10 7z" }, "p")]),
624
+ R: (s) => svg(s, [/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M8 5l10 7-10 7z" }, "p")]),
494
625
  // source ▷ (play)
495
- T: (s) => svg(s, [/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M4 8h6l4 8h6M14 8h6M4 16h6" }, "p")]),
626
+ T: (s) => svg(s, [/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M4 8h6l4 8h6M14 8h6M4 16h6" }, "p")]),
496
627
  // transform
497
628
  W: (s) => svg(s, [
498
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M5 4h11l3 3v13H5z" }, "p"),
499
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M9 4v5h6" }, "q")
629
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M5 4h11l3 3v13H5z" }, "p"),
630
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M9 4v5h6" }, "q")
500
631
  ])
501
632
  // sink (save)
502
633
  };
@@ -507,78 +638,86 @@ function NodeIcon({ code, kind, size = 15 }) {
507
638
  }
508
639
  function EyeIcon({ size = 14 }) {
509
640
  return svg(size, [
510
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M2 12s3.5-6.5 10-6.5S22 12 22 12s-3.5 6.5-10 6.5S2 12 2 12z" }, "p"),
511
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("circle", { cx: "12", cy: "12", r: "2.6" }, "c")
641
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M2 12s3.5-6.5 10-6.5S22 12 22 12s-3.5 6.5-10 6.5S2 12 2 12z" }, "p"),
642
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("circle", { cx: "12", cy: "12", r: "2.6" }, "c")
512
643
  ]);
513
644
  }
514
645
  function EyeOffIcon({ size = 14 }) {
515
646
  return svg(size, [
516
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M2 12s3.5-6.5 10-6.5c1.7 0 3.2.45 4.5 1.1M22 12s-3.5 6.5-10 6.5c-1.7 0-3.2-.45-4.5-1.1" }, "p"),
517
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M9.9 9.9a2.6 2.6 0 003.7 3.7" }, "e"),
518
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M3 3l18 18" }, "s")
647
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M2 12s3.5-6.5 10-6.5c1.7 0 3.2.45 4.5 1.1M22 12s-3.5 6.5-10 6.5c-1.7 0-3.2-.45-4.5-1.1" }, "p"),
648
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M9.9 9.9a2.6 2.6 0 003.7 3.7" }, "e"),
649
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M3 3l18 18" }, "s")
519
650
  ]);
520
651
  }
521
652
  function UnlockIcon({ size = 14 }) {
522
653
  return svg(size, [
523
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("rect", { x: "5", y: "11", width: "14", height: "9", rx: "2" }, "r"),
524
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M8 11V7a4 4 0 017.9-1" }, "s")
654
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("rect", { x: "5", y: "11", width: "14", height: "9", rx: "2" }, "r"),
655
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M8 11V7a4 4 0 017.9-1" }, "s")
525
656
  ]);
526
657
  }
527
658
  function LockIcon({ size = 14 }) {
528
659
  return svg(size, [
529
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("rect", { x: "5", y: "11", width: "14", height: "9", rx: "2" }, "r"),
530
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M8 11V7a4 4 0 018 0v4" }, "s")
660
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("rect", { x: "5", y: "11", width: "14", height: "9", rx: "2" }, "r"),
661
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M8 11V7a4 4 0 018 0v4" }, "s")
531
662
  ]);
532
663
  }
533
664
  function WarningIcon({ size = 13 }) {
534
665
  return svg(size, [
535
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M12 4l9 15H3z" }, "t"),
536
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M12 10v4" }, "b"),
537
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M12 17v.5" }, "d")
666
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M12 4l9 15H3z" }, "t"),
667
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M12 10v4" }, "b"),
668
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M12 17v.5" }, "d")
538
669
  ]);
539
670
  }
540
671
  function ErrorIcon({ size = 13 }) {
541
672
  return svg(size, [
542
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("circle", { cx: "12", cy: "12", r: "9" }, "c"),
543
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M12 7v6" }, "b"),
544
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M12 16v.5" }, "d")
673
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("circle", { cx: "12", cy: "12", r: "9" }, "c"),
674
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M12 7v6" }, "b"),
675
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M12 16v.5" }, "d")
545
676
  ]);
546
677
  }
678
+ function FilterIcon({ size = 14 }) {
679
+ return svg(size, [/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M4 5h16l-6 7.5V19l-4-2v-4.5z" }, "f")]);
680
+ }
681
+ function FollowIcon({ size = 14 }) {
682
+ return svg(size, [/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M12 4v11M7 10.5l5 5 5-5" }, "a"), /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M5 20h14" }, "l")]);
683
+ }
684
+ function ClearIcon({ size = 14 }) {
685
+ return svg(size, [/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("circle", { cx: "12", cy: "12", r: "8" }, "c"), /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M6.5 17.5l11-11" }, "s")]);
686
+ }
547
687
 
548
688
  // src/panels/DiagnosticBadge.tsx
549
- var import_jsx_runtime8 = require("react/jsx-runtime");
689
+ var import_jsx_runtime9 = require("react/jsx-runtime");
550
690
  function DiagnosticBadge({ diagnostic, variant, size = 12 }) {
551
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
691
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
552
692
  "span",
553
693
  {
554
694
  className: `${variant} hl-diag-${diagnostic.severity}`,
555
695
  title: diagnostic.message,
556
696
  "aria-label": `${diagnostic.severity}: ${diagnostic.message}`,
557
- children: diagnostic.severity === "error" ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ErrorIcon, { size }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(WarningIcon, { size })
697
+ children: diagnostic.severity === "error" ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ErrorIcon, { size }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(WarningIcon, { size })
558
698
  }
559
699
  );
560
700
  }
561
701
 
562
702
  // src/nodes/NodeInteractionContext.tsx
563
703
  var import_react3 = require("react");
564
- var import_jsx_runtime9 = require("react/jsx-runtime");
704
+ var import_jsx_runtime10 = require("react/jsx-runtime");
565
705
  var NoopInteraction = {
566
- diagnosticFor: () => void 0,
567
- rename: () => void 0
706
+ diagnosticFor: () => void 0
568
707
  };
569
708
  var NodeInteractionCtx = (0, import_react3.createContext)(NoopInteraction);
570
709
  function NodeInteractionProvider({
571
710
  value,
572
711
  children
573
712
  }) {
574
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(NodeInteractionCtx.Provider, { value, children });
713
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(NodeInteractionCtx.Provider, { value, children });
575
714
  }
576
715
  function useNodeInteraction() {
577
716
  return (0, import_react3.useContext)(NodeInteractionCtx);
578
717
  }
579
718
 
580
719
  // src/nodes/PillNode.tsx
581
- var import_jsx_runtime10 = require("react/jsx-runtime");
720
+ var import_jsx_runtime11 = require("react/jsx-runtime");
582
721
  var DOUBLE_CLICK_MS = 320;
583
722
  function PillNodeChrome({ node, selected }) {
584
723
  const live = useNodeLiveState(node.id);
@@ -596,10 +735,11 @@ function PillNodeChrome({ node, selected }) {
596
735
  });
597
736
  }
598
737
  }, [editing, node.name]);
738
+ const { rename } = interaction;
599
739
  const commit = (0, import_react4.useCallback)(() => {
600
740
  setEditing(false);
601
- interaction.rename(node.id, draft);
602
- }, [interaction, node.id, draft]);
741
+ rename == null ? void 0 : rename(node.id, draft);
742
+ }, [rename, node.id, draft]);
603
743
  const onKeyDown = (0, import_react4.useCallback)(
604
744
  (e) => {
605
745
  if (e.key === "Enter") {
@@ -614,6 +754,9 @@ function PillNodeChrome({ node, selected }) {
614
754
  );
615
755
  const lastClick = (0, import_react4.useRef)(0);
616
756
  const onNameClick = (0, import_react4.useCallback)(() => {
757
+ if (rename === void 0) {
758
+ return;
759
+ }
617
760
  const now = performance.now();
618
761
  if (now - lastClick.current < DOUBLE_CLICK_MS) {
619
762
  setEditing(true);
@@ -621,21 +764,23 @@ function PillNodeChrome({ node, selected }) {
621
764
  } else {
622
765
  lastClick.current = now;
623
766
  }
624
- }, []);
767
+ }, [rename]);
625
768
  const inputs = node.ports.filter((p) => p.direction === "I" && !p.isError);
626
769
  const outputs = node.ports.filter((p) => p.direction === "O" && !p.isError);
627
- const className = [
770
+ const className2 = [
628
771
  "hl-pill",
629
772
  `hl-pill-${node.kind.toLowerCase()}`,
630
773
  selected ? "is-selected" : "",
631
774
  live === "active" ? "is-active" : "",
632
775
  live === "error" ? "is-live-error" : "",
776
+ live === "finished" ? "is-finished" : "",
633
777
  (diagnostic == null ? void 0 : diagnostic.severity) === "error" ? "has-diag-error" : "",
634
778
  (diagnostic == null ? void 0 : diagnostic.severity) === "warning" ? "has-diag-warning" : ""
635
779
  ].filter(Boolean).join(" ");
636
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className, "data-kind": node.kind, children: [
637
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "hl-pill-dot", "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(NodeIcon, { code: node.code, kind: node.kind, size: 13 }) }),
638
- editing ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
780
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: className2, "data-kind": node.kind, children: [
781
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(LivePulse, { nodeId: node.id }),
782
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "hl-pill-dot", "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(NodeIcon, { code: node.code, kind: node.kind, size: 13 }) }),
783
+ editing ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
639
784
  "input",
640
785
  {
641
786
  ref: inputRef,
@@ -647,71 +792,178 @@ function PillNodeChrome({ node, selected }) {
647
792
  spellCheck: false,
648
793
  "aria-label": "Rename node"
649
794
  }
650
- ) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
795
+ ) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
651
796
  "span",
652
797
  {
653
798
  className: "hl-pill-name",
654
799
  onClick: () => {
655
800
  onNameClick();
656
801
  },
657
- title: "Double-click to rename",
802
+ title: rename === void 0 ? void 0 : "Double-click to rename",
658
803
  children: node.name
659
804
  }
660
805
  ),
661
- diagnostic !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(DiagnosticBadge, { diagnostic, variant: "hl-pill-diag", size: 12 }),
662
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "hl-pill-status", "aria-hidden": true }),
663
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("ul", { className: "hl-pill-ports hl-in", children: inputs.map((port) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(PortRow, { port }, port.id)) }),
664
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("ul", { className: "hl-pill-ports hl-out", children: outputs.map((port) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(PortRow, { port }, port.id)) })
806
+ diagnostic !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(DiagnosticBadge, { diagnostic, variant: "hl-pill-diag", size: 12 }),
807
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "hl-pill-status", "aria-hidden": true }),
808
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("ul", { className: "hl-pill-ports hl-in", children: inputs.map((port) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(PortRow, { port }, port.id)) }),
809
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("ul", { className: "hl-pill-ports hl-out", children: outputs.map((port) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(PortRow, { port }, port.id)) })
665
810
  ] });
666
811
  }
667
812
 
668
813
  // src/nodes/HlangNode.tsx
669
- var import_jsx_runtime11 = require("react/jsx-runtime");
814
+ var import_jsx_runtime12 = require("react/jsx-runtime");
670
815
  function NodeChrome({ node, selected, body }) {
671
816
  const inputs = node.ports.filter((p) => p.direction === "I");
672
817
  const outputs = node.ports.filter((p) => p.direction === "O");
673
818
  const live = useNodeLiveState(node.id);
674
- const className = [
819
+ const className2 = [
675
820
  "hl-node",
676
821
  `hl-${node.kind.toLowerCase()}`,
677
822
  selected ? "is-selected" : "",
678
823
  live === "active" ? "is-active" : "",
679
- live === "error" ? "is-live-error" : ""
824
+ live === "error" ? "is-live-error" : "",
825
+ live === "finished" ? "is-finished" : ""
680
826
  ].filter(Boolean).join(" ");
681
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className, "data-kind": node.kind, children: [
682
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(SignalBar, {}),
683
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("header", { className: "hl-head", children: [
684
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "hl-kind", children: node.kind }),
685
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "hl-name", children: node.name }),
686
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "hl-role", children: KIND_ROLE[node.kind] })
827
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: className2, "data-kind": node.kind, children: [
828
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(LivePulse, { nodeId: node.id }),
829
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(SignalBar, {}),
830
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("header", { className: "hl-head", children: [
831
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "hl-kind", children: node.kind }),
832
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "hl-name", children: node.name }),
833
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "hl-role", children: KIND_ROLE[node.kind] })
687
834
  ] }),
688
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(NodeBodySlot, { node, children: body }),
689
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "hl-ports", children: [
690
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("ul", { className: "hl-col hl-in", children: inputs.map((port) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(PortRow, { port }, port.id)) }),
691
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("ul", { className: "hl-col hl-out", children: outputs.map((port) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(PortRow, { port }, port.id)) })
835
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(NodeBodySlot, { node, children: body }),
836
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "hl-ports", children: [
837
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("ul", { className: "hl-col hl-in", children: inputs.map((port) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(PortRow, { port }, port.id)) }),
838
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("ul", { className: "hl-col hl-out", children: outputs.map((port) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(PortRow, { port }, port.id)) })
692
839
  ] })
693
840
  ] });
694
841
  }
695
842
  function HlangNode({ data, selected }) {
696
843
  const node = data.editorNode;
697
844
  if (typeof node.params.imageUrl === "string" && node.params.imageUrl.length > 0) {
698
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(AssetNodeChrome, { node, selected });
845
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(AssetNodeChrome, { node, selected });
699
846
  }
700
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(PillNodeChrome, { node, selected });
847
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(PillNodeChrome, { node, selected });
701
848
  }
702
849
 
703
850
  // src/edges/FlowEdge.tsx
704
851
  var import_react5 = require("@xyflow/react");
705
- var import_jsx_runtime12 = require("react/jsx-runtime");
706
- function useSourceRole(sourceId) {
707
- const node = (0, import_react5.useInternalNode)(sourceId);
852
+
853
+ // src/edges/self-loop.ts
854
+ var SELF_LOOP_CLEARANCE = 28;
855
+ var SELF_LOOP_RADIUS = 16;
856
+ function format(value) {
857
+ return String(Math.round(value * 100) / 100);
858
+ }
859
+ function distance(a, b) {
860
+ return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
861
+ }
862
+ function toward(from, to, length) {
863
+ const span = distance(from, to);
864
+ const share = span === 0 ? 0 : length / span;
865
+ return { x: from.x + (to.x - from.x) * share, y: from.y + (to.y - from.y) * share };
866
+ }
867
+ function selfLoopPath(input) {
868
+ var _a, _b, _c, _d, _e;
869
+ const clearance = (_a = input.clearance) != null ? _a : SELF_LOOP_CLEARANCE;
870
+ const radius = (_b = input.radius) != null ? _b : SELF_LOOP_RADIUS;
871
+ const { source, target, node } = input;
872
+ const right = Math.max(source.x, node.x + node.width) + clearance;
873
+ const left = Math.min(target.x, node.x) - clearance;
874
+ const below = node.y + node.height + clearance;
875
+ const points = [
876
+ source,
877
+ { x: right, y: source.y },
878
+ { x: right, y: below },
879
+ { x: left, y: below },
880
+ { x: left, y: target.y },
881
+ target
882
+ ];
883
+ const parts = [`M ${format(source.x)} ${format(source.y)}`];
884
+ for (let i = 1; i < points.length - 1; i += 1) {
885
+ const before = (_c = points[i - 1]) != null ? _c : source;
886
+ const corner = (_d = points[i]) != null ? _d : source;
887
+ const after = (_e = points[i + 1]) != null ? _e : target;
888
+ const r = Math.min(radius, distance(before, corner) / 2, distance(corner, after) / 2);
889
+ if (r <= 0) {
890
+ parts.push(`L ${format(corner.x)} ${format(corner.y)}`);
891
+ continue;
892
+ }
893
+ const enter = toward(corner, before, r);
894
+ const leave = toward(corner, after, r);
895
+ parts.push(
896
+ `L ${format(enter.x)} ${format(enter.y)} Q ${format(corner.x)} ${format(corner.y)} ${format(leave.x)} ${format(leave.y)}`
897
+ );
898
+ }
899
+ parts.push(`L ${format(target.x)} ${format(target.y)}`);
900
+ return { path: parts.join(" "), labelX: (right + left) / 2, labelY: below };
901
+ }
902
+
903
+ // src/edges/edge-bounds.ts
904
+ var DEFAULT_EDGE_CAPACITY = 10;
905
+ function boundsFields(bounds) {
906
+ var _a;
907
+ const bounded = bounds.capacity !== void 0;
908
+ return [
909
+ { key: "bounded", label: "Bounded", type: "toggle", value: bounded },
910
+ { key: "capacity", label: "Capacity", type: "number", value: (_a = bounds.capacity) != null ? _a : DEFAULT_EDGE_CAPACITY, disabled: !bounded },
911
+ {
912
+ key: "dropOldest",
913
+ label: "Drop oldest when full",
914
+ type: "toggle",
915
+ value: bounds.overflow === "drop-oldest",
916
+ disabled: !bounded
917
+ }
918
+ ];
919
+ }
920
+ function nextBounds(bounds, key, value) {
921
+ const { capacity, overflow } = bounds;
922
+ if (key === "bounded") {
923
+ return value === true ? { capacity: capacity != null ? capacity : DEFAULT_EDGE_CAPACITY } : {};
924
+ }
925
+ if (capacity === void 0) {
926
+ return {};
927
+ }
928
+ const keep = overflow === void 0 ? {} : { overflow };
929
+ if (key === "capacity") {
930
+ if (typeof value !== "number" || Number.isNaN(value)) {
931
+ return bounds;
932
+ }
933
+ return { capacity: Math.max(1, Math.round(value)), ...keep };
934
+ }
935
+ if (key === "dropOldest") {
936
+ return value === true ? { capacity, overflow: "drop-oldest" } : { capacity };
937
+ }
938
+ return bounds;
939
+ }
940
+ function capacityLabel(bounds) {
941
+ if (bounds.capacity === void 0) {
942
+ return void 0;
943
+ }
944
+ const label = `[${String(bounds.capacity)}]`;
945
+ return bounds.overflow === "drop-oldest" ? `${label} drop oldest` : label;
946
+ }
947
+
948
+ // src/edges/FlowEdge.tsx
949
+ var import_jsx_runtime13 = require("react/jsx-runtime");
950
+ function roleOf(node) {
708
951
  const role = node == null ? void 0 : node.data.editorNode.params.role;
709
952
  return typeof role === "string" && role.length > 0 ? role : void 0;
710
953
  }
711
954
  function FlowEdge(props) {
712
- var _a;
713
- const { id, source, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, data } = props;
714
- const [path] = (0, import_react5.getBezierPath)({
955
+ var _a, _b, _c, _d, _e, _f;
956
+ const { id, source, target, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, data, selected } = props;
957
+ const sourceNode = (0, import_react5.useInternalNode)(source);
958
+ const role = roleOf(sourceNode);
959
+ const width = sourceNode == null ? void 0 : sourceNode.measured.width;
960
+ const height = sourceNode == null ? void 0 : sourceNode.measured.height;
961
+ const loop = source === target && sourceNode !== void 0 && width !== void 0 && height !== void 0 ? selfLoopPath({
962
+ source: { x: sourceX, y: sourceY },
963
+ target: { x: targetX, y: targetY },
964
+ node: { ...sourceNode.internals.positionAbsolute, width, height }
965
+ }) : void 0;
966
+ const [curve] = (0, import_react5.getBezierPath)({
715
967
  sourceX,
716
968
  sourceY,
717
969
  sourcePosition,
@@ -719,25 +971,39 @@ function FlowEdge(props) {
719
971
  targetY,
720
972
  targetPosition
721
973
  });
974
+ const path = (_a = loop == null ? void 0 : loop.path) != null ? _a : curve;
722
975
  const t = 0.62;
723
- const labelX = sourceX + (targetX - sourceX) * t;
724
- const labelY = sourceY + (targetY - sourceY) * t;
725
- const isError = (_a = data == null ? void 0 : data.isError) != null ? _a : false;
976
+ const labelX = (_b = loop == null ? void 0 : loop.labelX) != null ? _b : sourceX + (targetX - sourceX) * t;
977
+ const labelY = (_c = loop == null ? void 0 : loop.labelY) != null ? _c : sourceY + (targetY - sourceY) * t;
978
+ const capacity = capacityLabel(data != null ? data : {});
979
+ const capacityX = (_d = loop == null ? void 0 : loop.labelX) != null ? _d : sourceX + (targetX - sourceX) * 0.38;
980
+ const capacityY = (_e = loop == null ? void 0 : loop.labelY) != null ? _e : sourceY + (targetY - sourceY) * 0.38;
981
+ const isError = (_f = data == null ? void 0 : data.isError) != null ? _f : false;
726
982
  const isValue = (data == null ? void 0 : data.stream) === "value";
727
983
  const flowing = useEdgeFlowing(id);
728
- const role = useSourceRole(source);
729
- const className = [
984
+ const pulse = useEdgePulse(id);
985
+ const className2 = [
730
986
  "hl-edge",
731
987
  isError ? "is-error" : "",
732
988
  isValue ? "is-value" : "is-live",
733
- flowing ? "is-flowing" : ""
989
+ flowing ? "is-flowing" : "",
990
+ selected === true ? "is-selected" : ""
734
991
  ].filter(Boolean).join(" ");
735
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_jsx_runtime12.Fragment, { children: [
736
- /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("g", { className, children: [
737
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_react5.BaseEdge, { id, path, className: "hl-edge-base" }),
738
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("path", { d: path, className: "hl-edge-flow" })
992
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_jsx_runtime13.Fragment, { children: [
993
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("g", { className: className2, children: [
994
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_react5.BaseEdge, { id, path, className: "hl-edge-base" }),
995
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("path", { d: path, className: "hl-edge-flow" }),
996
+ pulse > 0 && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("path", { d: path, className: "hl-edge-pulse" }, pulse)
739
997
  ] }),
740
- role !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_react5.EdgeLabelRenderer, { children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
998
+ capacity !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_react5.EdgeLabelRenderer, { children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
999
+ "div",
1000
+ {
1001
+ className: `hl-edge-capacity hl-mono${(data == null ? void 0 : data.overflow) === "drop-oldest" ? " is-lossy" : ""}`,
1002
+ style: { transform: `translate(-50%, -50%) translate(${String(capacityX)}px, ${String(capacityY)}px)` },
1003
+ children: capacity
1004
+ }
1005
+ ) }),
1006
+ role !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_react5.EdgeLabelRenderer, { children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
741
1007
  "div",
742
1008
  {
743
1009
  className: "hl-edge-label hl-mono",
@@ -759,49 +1025,257 @@ function createHlangFlowTypes() {
759
1025
  }
760
1026
 
761
1027
  // src/shell/EditorShell.tsx
762
- var import_jsx_runtime13 = require("react/jsx-runtime");
1028
+ var import_react7 = require("react");
1029
+
1030
+ // src/shell/FooterResizer.tsx
1031
+ var import_jsx_runtime14 = require("react/jsx-runtime");
1032
+ function FooterResizer({ resize, controls, label }) {
1033
+ const onPointerDown = (event) => {
1034
+ if (event.pointerType === "mouse" && event.button !== 0) {
1035
+ return;
1036
+ }
1037
+ event.preventDefault();
1038
+ event.currentTarget.setPointerCapture(event.pointerId);
1039
+ resize.startDrag(event.clientY);
1040
+ };
1041
+ const onPointerMove = (event) => {
1042
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
1043
+ resize.drag(event.clientY);
1044
+ }
1045
+ };
1046
+ const onPointerEnd = (event) => {
1047
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
1048
+ event.currentTarget.releasePointerCapture(event.pointerId);
1049
+ }
1050
+ resize.endDrag();
1051
+ };
1052
+ const onKeyDown = (event) => {
1053
+ if (resize.keyDown(event.key, event.shiftKey)) {
1054
+ event.preventDefault();
1055
+ }
1056
+ };
1057
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1058
+ "div",
1059
+ {
1060
+ role: "separator",
1061
+ "aria-orientation": "horizontal",
1062
+ "aria-label": label,
1063
+ "aria-controls": controls,
1064
+ "aria-valuemin": resize.bounds.min,
1065
+ "aria-valuemax": resize.bounds.max,
1066
+ "aria-valuenow": resize.height,
1067
+ tabIndex: 0,
1068
+ className: `hl-footer-resizer${resize.dragging ? " is-dragging" : ""}`,
1069
+ onPointerDown,
1070
+ onPointerMove,
1071
+ onPointerUp: onPointerEnd,
1072
+ onPointerCancel: onPointerEnd,
1073
+ onLostPointerCapture: () => {
1074
+ resize.endDrag();
1075
+ },
1076
+ onDoubleClick: () => {
1077
+ resize.toggle();
1078
+ },
1079
+ onKeyDown
1080
+ }
1081
+ );
1082
+ }
1083
+
1084
+ // src/shell/useFooterResize.ts
1085
+ var import_react6 = require("react");
1086
+
1087
+ // src/shell/footer-size.ts
1088
+ var FOOTER_KEY_STEP = 16;
1089
+ function footerBounds(options) {
1090
+ const { available, minHeight, minCanvasHeight } = options;
1091
+ return { min: minHeight, max: Math.max(minHeight, available - minCanvasHeight) };
1092
+ }
1093
+ function clampFooter(height, bounds) {
1094
+ return Math.round(Math.min(bounds.max, Math.max(bounds.min, height)));
1095
+ }
1096
+ function resizeByKey(height, key, shift, bounds) {
1097
+ const step = FOOTER_KEY_STEP * (shift ? 4 : 1);
1098
+ switch (key) {
1099
+ case "ArrowUp":
1100
+ return clampFooter(height + step, bounds);
1101
+ case "ArrowDown":
1102
+ return clampFooter(height - step, bounds);
1103
+ case "Home":
1104
+ return bounds.min;
1105
+ case "End":
1106
+ return bounds.max;
1107
+ default:
1108
+ return void 0;
1109
+ }
1110
+ }
1111
+ function toggleCollapsed(size, bounds) {
1112
+ if (size.height > bounds.min) {
1113
+ return { height: bounds.min, restore: size.height };
1114
+ }
1115
+ return { height: clampFooter(size.restore, bounds), restore: size.restore };
1116
+ }
1117
+
1118
+ // src/shell/useFooterResize.ts
1119
+ var DEFAULT_MIN_CANVAS_HEIGHT = 160;
1120
+ function useFooterResize(options) {
1121
+ var _a;
1122
+ const { defaultHeight, minHeight } = options;
1123
+ const minCanvasHeight = (_a = options.minCanvasHeight) != null ? _a : DEFAULT_MIN_CANVAS_HEIGHT;
1124
+ const [available, setAvailable] = (0, import_react6.useState)(null);
1125
+ const [preferred, setPreferred] = (0, import_react6.useState)(defaultHeight);
1126
+ const [restore, setRestore] = (0, import_react6.useState)(defaultHeight);
1127
+ const [dragging, setDragging] = (0, import_react6.useState)(false);
1128
+ const dragStart = (0, import_react6.useRef)(null);
1129
+ const bounds = (0, import_react6.useMemo)(
1130
+ () => available === null ? { min: minHeight, max: Math.max(minHeight, defaultHeight) } : footerBounds({ available, minHeight, minCanvasHeight }),
1131
+ [available, minHeight, minCanvasHeight, defaultHeight]
1132
+ );
1133
+ const height = clampFooter(preferred, bounds);
1134
+ const choose = (0, import_react6.useCallback)(
1135
+ (next) => {
1136
+ setPreferred(next);
1137
+ if (next > bounds.min) {
1138
+ setRestore(next);
1139
+ }
1140
+ },
1141
+ [bounds.min]
1142
+ );
1143
+ const measure = (0, import_react6.useCallback)((next) => {
1144
+ setAvailable(next);
1145
+ }, []);
1146
+ const startDrag = (0, import_react6.useCallback)(
1147
+ (clientY) => {
1148
+ dragStart.current = { clientY, height };
1149
+ setDragging(true);
1150
+ },
1151
+ [height]
1152
+ );
1153
+ const drag = (0, import_react6.useCallback)(
1154
+ (clientY) => {
1155
+ const start = dragStart.current;
1156
+ if (start === null) {
1157
+ return;
1158
+ }
1159
+ choose(clampFooter(start.height + (start.clientY - clientY), bounds));
1160
+ },
1161
+ [bounds, choose]
1162
+ );
1163
+ const endDrag = (0, import_react6.useCallback)(() => {
1164
+ dragStart.current = null;
1165
+ setDragging(false);
1166
+ }, []);
1167
+ const toggle = (0, import_react6.useCallback)(() => {
1168
+ const next = toggleCollapsed({ height, restore }, bounds);
1169
+ setPreferred(next.height);
1170
+ setRestore(next.restore);
1171
+ }, [height, restore, bounds]);
1172
+ const keyDown = (0, import_react6.useCallback)(
1173
+ (key, shift) => {
1174
+ if (key === "Enter") {
1175
+ toggle();
1176
+ return true;
1177
+ }
1178
+ const next = resizeByKey(height, key, shift, bounds);
1179
+ if (next === void 0) {
1180
+ return false;
1181
+ }
1182
+ choose(next);
1183
+ return true;
1184
+ },
1185
+ [height, bounds, choose, toggle]
1186
+ );
1187
+ return { height, bounds, dragging, measure, startDrag, drag, endDrag, keyDown, toggle };
1188
+ }
1189
+
1190
+ // src/shell/EditorShell.tsx
1191
+ var import_jsx_runtime15 = require("react/jsx-runtime");
763
1192
  function EditorShell({
764
1193
  toolbar,
765
1194
  left,
766
1195
  right,
767
1196
  canvas,
768
1197
  footer,
1198
+ footerResize,
769
1199
  leftCollapsed = false,
770
1200
  rightCollapsed = false
771
1201
  }) {
772
- const className = [
1202
+ const shellRef = (0, import_react7.useRef)(null);
1203
+ const toolbarRef = (0, import_react7.useRef)(null);
1204
+ const className2 = [
773
1205
  "hl-shell",
774
1206
  leftCollapsed ? "hl-left-collapsed" : "",
775
1207
  rightCollapsed ? "hl-right-collapsed" : ""
776
1208
  ].filter(Boolean).join(" ");
777
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className, children: [
778
- toolbar !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "hl-region-toolbar", children: toolbar }),
779
- /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "hl-region-body", children: [
780
- left !== void 0 && !leftCollapsed && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("aside", { className: "hl-region-left", children: left }),
781
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("main", { className: "hl-region-canvas", children: canvas }),
782
- right !== void 0 && !rightCollapsed && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("aside", { className: "hl-region-right", children: right })
1209
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: className2, ref: shellRef, children: [
1210
+ toolbar !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "hl-region-toolbar", ref: toolbarRef, children: toolbar }),
1211
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "hl-region-body", children: [
1212
+ left !== void 0 && !leftCollapsed && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("aside", { className: "hl-region-left", children: left }),
1213
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("main", { className: "hl-region-canvas", children: canvas }),
1214
+ right !== void 0 && !rightCollapsed && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("aside", { className: "hl-region-right", children: right })
783
1215
  ] }),
784
- footer !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "hl-region-footer", children: footer })
1216
+ footer !== void 0 && (footerResize === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "hl-region-footer", children: footer }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(ResizableFooter, { options: footerResize, shellRef, toolbarRef, children: footer }))
785
1217
  ] });
786
1218
  }
1219
+ function ResizableFooter({
1220
+ options,
1221
+ shellRef,
1222
+ toolbarRef,
1223
+ children
1224
+ }) {
1225
+ var _a;
1226
+ const resize = useFooterResize(options);
1227
+ const id = (0, import_react7.useId)();
1228
+ const { measure } = resize;
1229
+ (0, import_react7.useEffect)(() => {
1230
+ const shell = shellRef.current;
1231
+ if (shell === null) {
1232
+ return;
1233
+ }
1234
+ const update = () => {
1235
+ var _a2, _b;
1236
+ measure(shell.clientHeight - ((_b = (_a2 = toolbarRef.current) == null ? void 0 : _a2.offsetHeight) != null ? _b : 0));
1237
+ };
1238
+ update();
1239
+ const observer = new ResizeObserver(update);
1240
+ observer.observe(shell);
1241
+ if (toolbarRef.current !== null) {
1242
+ observer.observe(toolbarRef.current);
1243
+ }
1244
+ return () => {
1245
+ observer.disconnect();
1246
+ };
1247
+ }, [measure, shellRef, toolbarRef]);
1248
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
1249
+ "div",
1250
+ {
1251
+ id,
1252
+ className: `hl-region-footer is-resizable${resize.dragging ? " is-resizing" : ""}`,
1253
+ style: { height: resize.height },
1254
+ children: [
1255
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(FooterResizer, { resize, controls: id, label: (_a = options.label) != null ? _a : "Resize the panel" }),
1256
+ children
1257
+ ]
1258
+ }
1259
+ );
1260
+ }
787
1261
 
788
1262
  // src/toolbar/Toolbar.tsx
789
1263
  var import_editor_react = require("@hlang-org/editor-react");
790
- var import_jsx_runtime14 = require("react/jsx-runtime");
1264
+ var import_jsx_runtime16 = require("react/jsx-runtime");
791
1265
  function Toolbar({ title, center, actions }) {
792
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("header", { className: "hl-toolbar", children: [
793
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "hl-toolbar-brand", children: title != null ? title : /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
794
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "hl-toolbar-glyph", "aria-hidden": "true" }),
795
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "hl-toolbar-wordmark", children: "hlang" })
1266
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("header", { className: "hl-toolbar", children: [
1267
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "hl-toolbar-brand", children: title != null ? title : /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
1268
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "hl-toolbar-glyph", "aria-hidden": "true" }),
1269
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "hl-toolbar-wordmark", children: "hlang" })
796
1270
  ] }) }),
797
- center !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "hl-toolbar-center", children: center }),
798
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "hl-toolbar-actions", children: actions })
1271
+ center !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "hl-toolbar-center", children: center }),
1272
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "hl-toolbar-actions", children: actions })
799
1273
  ] });
800
1274
  }
801
1275
  function UndoRedo({ core }) {
802
1276
  const history = (0, import_editor_react.useHistory)(core);
803
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
804
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1277
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
1278
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
805
1279
  "button",
806
1280
  {
807
1281
  type: "button",
@@ -812,7 +1286,7 @@ function UndoRedo({ core }) {
812
1286
  children: "Undo"
813
1287
  }
814
1288
  ),
815
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1289
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
816
1290
  "button",
817
1291
  {
818
1292
  type: "button",
@@ -831,7 +1305,7 @@ function RunButton({
831
1305
  runLabel = "Run",
832
1306
  stopLabel = "Stop"
833
1307
  }) {
834
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
1308
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
835
1309
  "button",
836
1310
  {
837
1311
  type: "button",
@@ -839,7 +1313,7 @@ function RunButton({
839
1313
  onClick: onRun,
840
1314
  "aria-pressed": running,
841
1315
  children: [
842
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1316
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
843
1317
  "span",
844
1318
  {
845
1319
  className: `hl-toolbar-run-icon${running ? " is-stop" : ""}`,
@@ -852,22 +1326,31 @@ function RunButton({
852
1326
  );
853
1327
  }
854
1328
  function ToolbarStat({ label, value }) {
855
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "hl-toolbar-stat hl-mono", title: label, children: value });
1329
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "hl-toolbar-stat hl-mono", title: label, children: value });
856
1330
  }
857
1331
  function ToolbarGhostButton({
858
1332
  children,
859
1333
  onClick,
860
1334
  title
861
1335
  }) {
862
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("button", { type: "button", className: "hl-toolbar-btn", onClick, title, children });
1336
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("button", { type: "button", className: "hl-toolbar-btn", onClick, title, children });
1337
+ }
1338
+
1339
+ // src/toolbar/ReadOnlyBadge.tsx
1340
+ var import_jsx_runtime17 = require("react/jsx-runtime");
1341
+ function ReadOnlyBadge({ label = "View only", title }) {
1342
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("span", { className: "hl-readonly-badge", title: title != null ? title : label, children: [
1343
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(LockIcon, { size: 12 }),
1344
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "hl-readonly-badge-label", children: label })
1345
+ ] });
863
1346
  }
864
1347
 
865
1348
  // src/toolbar/ToolCluster.tsx
866
- var import_jsx_runtime15 = require("react/jsx-runtime");
1349
+ var import_jsx_runtime18 = require("react/jsx-runtime");
867
1350
  function ToolCluster({ items }) {
868
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "hl-toolcluster", role: "toolbar", "aria-label": "Canvas tools", children: items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("span", { className: "hl-toolcluster-slot", children: [
869
- item.separatorBefore === true && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "hl-toolcluster-sep", "aria-hidden": true }),
870
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1351
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "hl-toolcluster", role: "toolbar", "aria-label": "Canvas tools", children: items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { className: "hl-toolcluster-slot", children: [
1352
+ item.separatorBefore === true && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "hl-toolcluster-sep", "aria-hidden": true }),
1353
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
871
1354
  "button",
872
1355
  {
873
1356
  type: "button",
@@ -884,31 +1367,31 @@ function ToolCluster({ items }) {
884
1367
  }
885
1368
 
886
1369
  // src/canvas/CanvasChrome.tsx
887
- var import_react6 = require("@xyflow/react");
888
- var import_jsx_runtime16 = require("react/jsx-runtime");
1370
+ var import_react8 = require("@xyflow/react");
1371
+ var import_jsx_runtime19 = require("react/jsx-runtime");
889
1372
  function CanvasChrome({ background = true }) {
890
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_jsx_runtime16.Fragment, { children: background && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react6.Background, { variant: import_react6.BackgroundVariant.Dots, gap: 26, size: 1, color: "#242830" }) });
1373
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_jsx_runtime19.Fragment, { children: background && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_react8.Background, { variant: import_react8.BackgroundVariant.Dots, gap: 26, size: 1, color: "#242830" }) });
891
1374
  }
892
1375
 
893
1376
  // src/canvas/CanvasDock.tsx
894
- var import_jsx_runtime17 = require("react/jsx-runtime");
1377
+ var import_jsx_runtime20 = require("react/jsx-runtime");
895
1378
  function CursorIcon() {
896
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("svg", { width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("path", { d: "M5 3l6 16 2-6 6-2z" }) });
1379
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("svg", { width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("path", { d: "M5 3l6 16 2-6 6-2z" }) });
897
1380
  }
898
1381
  function HandIcon() {
899
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("svg", { width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("path", { d: "M8 12V6a1.5 1.5 0 013 0v5m0-1V5a1.5 1.5 0 013 0v6m0-1V6.5a1.5 1.5 0 013 0V13c0 3.5-2 6-5.5 6H12c-2 0-3-.8-4.2-2.5L5 12.5c-.7-1 .6-2.3 1.6-1.6z" }) });
1382
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("svg", { width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("path", { d: "M8 12V6a1.5 1.5 0 013 0v5m0-1V5a1.5 1.5 0 013 0v6m0-1V6.5a1.5 1.5 0 013 0V13c0 3.5-2 6-5.5 6H12c-2 0-3-.8-4.2-2.5L5 12.5c-.7-1 .6-2.3 1.6-1.6z" }) });
900
1383
  }
901
1384
  function PlusIcon() {
902
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("svg", { width: 17, height: 17, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.7, strokeLinecap: "round", "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("path", { d: "M12 5v14M5 12h14" }) });
1385
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("svg", { width: 17, height: 17, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.7, strokeLinecap: "round", "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("path", { d: "M12 5v14M5 12h14" }) });
903
1386
  }
904
1387
  function FitIcon() {
905
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("svg", { width: 15, height: 15, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("path", { d: "M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5" }) });
1388
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("svg", { width: 15, height: 15, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("path", { d: "M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5" }) });
906
1389
  }
907
1390
  function UndoIcon() {
908
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("svg", { width: 15, height: 15, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("path", { d: "M9 7L4 12l5 5M4 12h11a5 5 0 010 10h-1" }) });
1391
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("svg", { width: 15, height: 15, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("path", { d: "M9 7L4 12l5 5M4 12h11a5 5 0 010 10h-1" }) });
909
1392
  }
910
1393
  function RedoIcon() {
911
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("svg", { width: 15, height: 15, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("path", { d: "M15 7l5 5-5 5M20 12H9a5 5 0 000 10h1" }) });
1394
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("svg", { width: 15, height: 15, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("path", { d: "M15 7l5 5-5 5M20 12H9a5 5 0 000 10h1" }) });
912
1395
  }
913
1396
  function CanvasDock({
914
1397
  zoom,
@@ -921,76 +1404,79 @@ function CanvasDock({
921
1404
  onUndo,
922
1405
  onRedo,
923
1406
  canUndo,
924
- canRedo
1407
+ canRedo,
1408
+ readOnly = false
925
1409
  }) {
926
1410
  const pct = `${String(Math.round(zoom * 100))}%`;
927
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "hl-dock", role: "toolbar", "aria-label": "Canvas tools", children: [
928
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
929
- "button",
930
- {
931
- className: "hl-dock-btn",
932
- onClick: onUndo,
933
- disabled: !canUndo,
934
- title: "Undo (\u2318Z)",
935
- "aria-label": "Undo",
936
- children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(UndoIcon, {})
937
- }
938
- ),
939
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
940
- "button",
941
- {
942
- className: "hl-dock-btn",
943
- onClick: onRedo,
944
- disabled: !canRedo,
945
- title: "Redo (\u2318\u21E7Z)",
946
- "aria-label": "Redo",
947
- children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(RedoIcon, {})
948
- }
949
- ),
950
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "hl-dock-sep", "aria-hidden": true }),
951
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
952
- "button",
953
- {
954
- className: "hl-dock-btn" + (mode === "select" ? " is-active" : ""),
955
- onClick: () => onModeChange("select"),
956
- title: "Select (V)",
957
- "aria-pressed": mode === "select",
958
- children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(CursorIcon, {})
959
- }
960
- ),
961
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
962
- "button",
963
- {
964
- className: "hl-dock-btn" + (mode === "pan" ? " is-active" : ""),
965
- onClick: () => onModeChange("pan"),
966
- title: "Pan (Space)",
967
- "aria-pressed": mode === "pan",
968
- children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(HandIcon, {})
969
- }
970
- ),
971
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { className: "hl-dock-btn hl-dock-add", onClick: onAdd, title: "Add node", children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(PlusIcon, {}) }),
972
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "hl-dock-sep", "aria-hidden": true }),
973
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { className: "hl-dock-btn", onClick: onZoomOut, title: "Zoom out", "aria-label": "Zoom out", children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "hl-dock-glyph", children: "\u2212" }) }),
974
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { className: "hl-dock-zoom hl-mono", onClick: onFitView, title: "Fit view", children: pct }),
975
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { className: "hl-dock-btn", onClick: onZoomIn, title: "Zoom in", "aria-label": "Zoom in", children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "hl-dock-glyph", children: "+" }) }),
976
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { className: "hl-dock-btn hl-dock-fit", onClick: onFitView, title: "Fit view", "aria-label": "Fit view", children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(FitIcon, {}) })
1411
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "hl-dock", role: "toolbar", "aria-label": "Canvas tools", children: [
1412
+ readOnly ? null : /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_jsx_runtime20.Fragment, { children: [
1413
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
1414
+ "button",
1415
+ {
1416
+ className: "hl-dock-btn",
1417
+ onClick: onUndo,
1418
+ disabled: !canUndo,
1419
+ title: "Undo (\u2318Z)",
1420
+ "aria-label": "Undo",
1421
+ children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(UndoIcon, {})
1422
+ }
1423
+ ),
1424
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
1425
+ "button",
1426
+ {
1427
+ className: "hl-dock-btn",
1428
+ onClick: onRedo,
1429
+ disabled: !canRedo,
1430
+ title: "Redo (\u2318\u21E7Z)",
1431
+ "aria-label": "Redo",
1432
+ children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(RedoIcon, {})
1433
+ }
1434
+ ),
1435
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "hl-dock-sep", "aria-hidden": true }),
1436
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
1437
+ "button",
1438
+ {
1439
+ className: "hl-dock-btn" + (mode === "select" ? " is-active" : ""),
1440
+ onClick: () => onModeChange("select"),
1441
+ title: "Select (V)",
1442
+ "aria-pressed": mode === "select",
1443
+ children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(CursorIcon, {})
1444
+ }
1445
+ ),
1446
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
1447
+ "button",
1448
+ {
1449
+ className: "hl-dock-btn" + (mode === "pan" ? " is-active" : ""),
1450
+ onClick: () => onModeChange("pan"),
1451
+ title: "Pan (Space)",
1452
+ "aria-pressed": mode === "pan",
1453
+ children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(HandIcon, {})
1454
+ }
1455
+ ),
1456
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("button", { className: "hl-dock-btn hl-dock-add", onClick: onAdd, title: "Add node", children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(PlusIcon, {}) }),
1457
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "hl-dock-sep", "aria-hidden": true })
1458
+ ] }),
1459
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("button", { className: "hl-dock-btn", onClick: onZoomOut, title: "Zoom out", "aria-label": "Zoom out", children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "hl-dock-glyph", children: "\u2212" }) }),
1460
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("button", { className: "hl-dock-zoom hl-mono", onClick: onFitView, title: "Fit view", children: pct }),
1461
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("button", { className: "hl-dock-btn", onClick: onZoomIn, title: "Zoom in", "aria-label": "Zoom in", children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "hl-dock-glyph", children: "+" }) }),
1462
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("button", { className: "hl-dock-btn hl-dock-fit", onClick: onFitView, title: "Fit view", "aria-label": "Fit view", children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(FitIcon, {}) })
977
1463
  ] });
978
1464
  }
979
1465
 
980
1466
  // src/canvas/EmptyState.tsx
981
- var import_jsx_runtime18 = require("react/jsx-runtime");
1467
+ var import_jsx_runtime21 = require("react/jsx-runtime");
982
1468
  function EmptyState({
983
1469
  title = "An empty canvas.",
984
1470
  hint = "add a source \xB7 wire it to a sink \xB7 run"
985
1471
  }) {
986
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "hl-empty", "aria-hidden": "true", children: [
987
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { className: "hl-empty-title", children: title }),
988
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { className: "hl-empty-hint", children: hint })
1472
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "hl-empty", "aria-hidden": "true", children: [
1473
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("p", { className: "hl-empty-title", children: title }),
1474
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("p", { className: "hl-empty-hint", children: hint })
989
1475
  ] });
990
1476
  }
991
1477
 
992
1478
  // src/panels/InspectorPanel.tsx
993
- var import_editor_react2 = require("@hlang-org/editor-react");
1479
+ var import_editor_react3 = require("@hlang-org/editor-react");
994
1480
 
995
1481
  // src/schema/param-field.ts
996
1482
  function humanizeKey(key) {
@@ -1021,11 +1507,11 @@ function deriveParamSchema(descriptor, node) {
1021
1507
  }
1022
1508
 
1023
1509
  // src/hooks/useNodeParams.ts
1024
- var import_react7 = require("react");
1510
+ var import_react9 = require("react");
1025
1511
  var import_editor_core = require("@hlang-org/editor-core");
1026
1512
  function useNodeParams(core, nodeId) {
1027
- const versionRef = (0, import_react7.useRef)(0);
1028
- const subscribe = (0, import_react7.useCallback)(
1513
+ const versionRef = (0, import_react9.useRef)(0);
1514
+ const subscribe = (0, import_react9.useCallback)(
1029
1515
  (onStoreChange) => {
1030
1516
  const disposable = core.events.on("nodeChanged", (event) => {
1031
1517
  if (nodeId !== void 0 && event.node.id === nodeId) {
@@ -1039,9 +1525,9 @@ function useNodeParams(core, nodeId) {
1039
1525
  },
1040
1526
  [core, nodeId]
1041
1527
  );
1042
- const getSnapshot = (0, import_react7.useCallback)(() => versionRef.current, []);
1043
- const version = (0, import_react7.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
1044
- const setParams = (0, import_react7.useCallback)(
1528
+ const getSnapshot = (0, import_react9.useCallback)(() => versionRef.current, []);
1529
+ const version = (0, import_react9.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
1530
+ const setParams = (0, import_react9.useCallback)(
1045
1531
  (next) => {
1046
1532
  if (nodeId === void 0) {
1047
1533
  return;
@@ -1050,7 +1536,7 @@ function useNodeParams(core, nodeId) {
1050
1536
  },
1051
1537
  [core, nodeId]
1052
1538
  );
1053
- const setParam = (0, import_react7.useCallback)(
1539
+ const setParam = (0, import_react9.useCallback)(
1054
1540
  (key, value) => {
1055
1541
  if (nodeId === void 0) {
1056
1542
  return;
@@ -1063,7 +1549,7 @@ function useNodeParams(core, nodeId) {
1063
1549
  },
1064
1550
  [core, nodeId, setParams]
1065
1551
  );
1066
- return (0, import_react7.useMemo)(
1552
+ return (0, import_react9.useMemo)(
1067
1553
  () => ({
1068
1554
  node: nodeId === void 0 ? void 0 : core.model.getNode(nodeId),
1069
1555
  setParam,
@@ -1075,19 +1561,61 @@ function useNodeParams(core, nodeId) {
1075
1561
  );
1076
1562
  }
1077
1563
 
1564
+ // src/panels/ConnectionInspector.tsx
1565
+ var import_editor_react2 = require("@hlang-org/editor-react");
1566
+
1567
+ // src/hooks/useEdgeBounds.ts
1568
+ var import_react10 = require("react");
1569
+ var import_editor_core2 = require("@hlang-org/editor-core");
1570
+ function useEdgeBounds(core, edgeId) {
1571
+ const subscribe = (0, import_react10.useCallback)(
1572
+ (onStoreChange) => {
1573
+ const changed = core.events.on("edgeChanged", (event) => {
1574
+ if (event.edge.id === edgeId) {
1575
+ onStoreChange();
1576
+ }
1577
+ });
1578
+ const removed = core.events.on("edgeRemoved", (event) => {
1579
+ if (event.edgeId === edgeId) {
1580
+ onStoreChange();
1581
+ }
1582
+ });
1583
+ return () => {
1584
+ changed.dispose();
1585
+ removed.dispose();
1586
+ };
1587
+ },
1588
+ [core, edgeId]
1589
+ );
1590
+ const getSnapshot = (0, import_react10.useCallback)(
1591
+ () => edgeId === void 0 ? void 0 : core.model.getEdge(edgeId),
1592
+ [core, edgeId]
1593
+ );
1594
+ const edge = (0, import_react10.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
1595
+ const setBounds = (0, import_react10.useCallback)(
1596
+ (bounds) => {
1597
+ if (edgeId !== void 0) {
1598
+ core.commands.execute(new import_editor_core2.SetEdgeBoundsCommand(edgeId, bounds));
1599
+ }
1600
+ },
1601
+ [core, edgeId]
1602
+ );
1603
+ return { edge, setBounds };
1604
+ }
1605
+
1078
1606
  // src/panels/ParamForm.tsx
1079
- var import_react8 = require("react");
1080
- var import_jsx_runtime19 = require("react/jsx-runtime");
1081
- function ParamForm({ fields, onCommit }) {
1082
- return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "hl-form", children: fields.map((field) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(FieldRow, { field, onCommit }, field.key)) });
1607
+ var import_react11 = require("react");
1608
+ var import_jsx_runtime22 = require("react/jsx-runtime");
1609
+ function ParamForm({ fields, onCommit, disabled = false }) {
1610
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("fieldset", { className: "hl-form", disabled, children: fields.map((field) => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(FieldRow, { field, onCommit }, field.key)) });
1083
1611
  }
1084
1612
  function FieldRow({
1085
1613
  field,
1086
1614
  onCommit
1087
1615
  }) {
1088
- return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("label", { className: "hl-field", children: [
1089
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "hl-field-label", children: field.label }),
1090
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(FieldControl, { field, onCommit })
1616
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("label", { className: "hl-field", children: [
1617
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "hl-field-label", children: field.label }),
1618
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(FieldControl, { field, onCommit })
1091
1619
  ] });
1092
1620
  }
1093
1621
  function FieldControl({
@@ -1096,7 +1624,7 @@ function FieldControl({
1096
1624
  }) {
1097
1625
  switch (field.type) {
1098
1626
  case "toggle":
1099
- return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
1627
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
1100
1628
  "button",
1101
1629
  {
1102
1630
  type: "button",
@@ -1105,17 +1633,17 @@ function FieldControl({
1105
1633
  className: `hl-toggle${field.value ? " is-on" : ""}`,
1106
1634
  disabled: field.disabled,
1107
1635
  onClick: () => onCommit(field.key, !field.value),
1108
- children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "hl-toggle-knob" })
1636
+ children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "hl-toggle-knob" })
1109
1637
  }
1110
1638
  );
1111
1639
  case "number":
1112
- return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(NumberControl, { field, onCommit });
1640
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(NumberControl, { field, onCommit });
1113
1641
  case "textarea":
1114
- return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(TextControl, { field, onCommit, multiline: true });
1642
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(TextControl, { field, onCommit, multiline: true });
1115
1643
  case "text":
1116
- return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(TextControl, { field, onCommit });
1644
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(TextControl, { field, onCommit });
1117
1645
  case "custom":
1118
- return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "hl-field-custom hl-mono", title: field.fieldType, children: describeCustom(field.value) });
1646
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "hl-field-custom hl-mono", title: field.fieldType, children: describeCustom(field.value) });
1119
1647
  }
1120
1648
  }
1121
1649
  function TextControl({
@@ -1123,7 +1651,7 @@ function TextControl({
1123
1651
  onCommit,
1124
1652
  multiline = false
1125
1653
  }) {
1126
- const [draft, setDraft] = (0, import_react8.useState)(field.value);
1654
+ const [draft, setDraft] = (0, import_react11.useState)(field.value);
1127
1655
  const commit = () => {
1128
1656
  if (draft !== field.value) {
1129
1657
  onCommit(field.key, draft);
@@ -1137,7 +1665,7 @@ function TextControl({
1137
1665
  onBlur: commit
1138
1666
  };
1139
1667
  if (multiline) {
1140
- return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
1668
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
1141
1669
  "textarea",
1142
1670
  {
1143
1671
  ...shared,
@@ -1146,7 +1674,7 @@ function TextControl({
1146
1674
  }
1147
1675
  );
1148
1676
  }
1149
- return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
1677
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
1150
1678
  "input",
1151
1679
  {
1152
1680
  ...shared,
@@ -1164,14 +1692,14 @@ function NumberControl({
1164
1692
  field,
1165
1693
  onCommit
1166
1694
  }) {
1167
- const [draft, setDraft] = (0, import_react8.useState)(String(field.value));
1695
+ const [draft, setDraft] = (0, import_react11.useState)(String(field.value));
1168
1696
  const commit = () => {
1169
1697
  const parsed = Number(draft);
1170
1698
  if (!Number.isNaN(parsed) && parsed !== field.value) {
1171
1699
  onCommit(field.key, parsed);
1172
1700
  }
1173
1701
  };
1174
- return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
1702
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
1175
1703
  "input",
1176
1704
  {
1177
1705
  className: "hl-input hl-mono",
@@ -1202,57 +1730,289 @@ function describeCustom(value) {
1202
1730
  return String(value);
1203
1731
  }
1204
1732
 
1733
+ // src/panels/ConnectionInspector.tsx
1734
+ var import_jsx_runtime23 = require("react/jsx-runtime");
1735
+ function ConnectionInspector({ core, edgeId }) {
1736
+ const { edge, setBounds } = useEdgeBounds(core, edgeId);
1737
+ const readOnly = (0, import_editor_react2.useReadOnly)(core);
1738
+ if (edge === void 0) {
1739
+ return null;
1740
+ }
1741
+ const end = (endpoint) => {
1742
+ var _a, _b;
1743
+ return `${(_b = (_a = core.model.getNode(endpoint.nodeId)) == null ? void 0 : _a.name) != null ? _b : endpoint.nodeId}.${endpoint.portId}`;
1744
+ };
1745
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: "hl-inspector", children: [
1746
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("header", { className: "hl-inspector-head", children: [
1747
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "hl-inspector-kind hl-connection", children: "\u2192" }),
1748
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: "hl-inspector-titles", children: [
1749
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "hl-inspector-name", children: "Connection" }),
1750
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("span", { className: "hl-inspector-role hl-mono", children: [
1751
+ end(edge.source),
1752
+ " \u2192 ",
1753
+ end(edge.target)
1754
+ ] })
1755
+ ] })
1756
+ ] }),
1757
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("section", { className: "hl-inspector-section", children: [
1758
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("h3", { className: "hl-inspector-section-title", children: "Back-pressure" }),
1759
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("p", { className: "hl-inspector-hint hl-inspector-note", children: "A bounded connection holds at most its capacity, counting the message being worked on. A sender to a full connection waits, unless it drops its oldest waiting message." }),
1760
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
1761
+ ParamForm,
1762
+ {
1763
+ fields: boundsFields(edge),
1764
+ disabled: readOnly,
1765
+ onCommit: (key, value) => {
1766
+ setBounds(nextBounds(edge, key, value));
1767
+ }
1768
+ }
1769
+ )
1770
+ ] })
1771
+ ] });
1772
+ }
1773
+
1774
+ // src/live/format.ts
1775
+ function formatDuration(ms) {
1776
+ if (ms === 0) {
1777
+ return "0 ms";
1778
+ }
1779
+ if (ms < 0.01) {
1780
+ return "<0.01 ms";
1781
+ }
1782
+ if (ms < 1) {
1783
+ return `${ms.toFixed(2)} ms`;
1784
+ }
1785
+ if (ms < 10) {
1786
+ return `${ms.toFixed(1)} ms`;
1787
+ }
1788
+ if (ms < 1e3) {
1789
+ return `${Math.round(ms).toString()} ms`;
1790
+ }
1791
+ return `${(ms / 1e3).toFixed(2)} s`;
1792
+ }
1793
+ function formatElapsed(ms) {
1794
+ return `${(ms / 1e3).toFixed(3)}s`;
1795
+ }
1796
+
1797
+ // src/live/preview.ts
1798
+ var PREVIEW_LENGTH = 120;
1799
+ var CIRCULAR = "[Circular]";
1800
+ function functionPreview(value) {
1801
+ return `\u0192 ${value.name === "" ? "anonymous" : value.name}`;
1802
+ }
1803
+ function bigintPreview(value) {
1804
+ return `${value.toString()}n`;
1805
+ }
1806
+ function className(value) {
1807
+ const prototype = Object.getPrototypeOf(value);
1808
+ if (prototype === null || prototype === Object.prototype) {
1809
+ return "Object";
1810
+ }
1811
+ const name = typeof prototype === "object" && "constructor" in prototype ? prototype.constructor : void 0;
1812
+ return typeof name === "function" && name.name !== "" ? name.name : "Object";
1813
+ }
1814
+ function nestedPreview(value) {
1815
+ if (typeof value === "bigint") {
1816
+ return bigintPreview(value);
1817
+ }
1818
+ if (typeof value === "function") {
1819
+ return functionPreview(value);
1820
+ }
1821
+ if (typeof value === "symbol") {
1822
+ return value.toString();
1823
+ }
1824
+ if (value instanceof Error) {
1825
+ return `${value.name}: ${value.message}`;
1826
+ }
1827
+ if (value instanceof Map || value instanceof Set) {
1828
+ return `${value instanceof Map ? "Map" : "Set"}(${String(value.size)})`;
1829
+ }
1830
+ return value;
1831
+ }
1832
+ function json(value) {
1833
+ const ancestors = [];
1834
+ return JSON.stringify(value, function replace(_key, nested) {
1835
+ while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this) {
1836
+ ancestors.pop();
1837
+ }
1838
+ if (typeof nested === "object" && nested !== null) {
1839
+ if (ancestors.includes(nested)) {
1840
+ return CIRCULAR;
1841
+ }
1842
+ ancestors.push(nested);
1843
+ }
1844
+ return nestedPreview(nested);
1845
+ });
1846
+ }
1847
+ function fullPreview(value) {
1848
+ switch (typeof value) {
1849
+ case "string":
1850
+ return JSON.stringify(value);
1851
+ case "bigint":
1852
+ case "function":
1853
+ case "symbol": {
1854
+ const preview = nestedPreview(value);
1855
+ return typeof preview === "string" ? preview : String(preview);
1856
+ }
1857
+ case "object":
1858
+ break;
1859
+ default:
1860
+ return String(value);
1861
+ }
1862
+ if (value === null) {
1863
+ return "null";
1864
+ }
1865
+ if (value instanceof Error || value instanceof Map || value instanceof Set) {
1866
+ return String(nestedPreview(value));
1867
+ }
1868
+ if (value instanceof Date) {
1869
+ return Number.isNaN(value.getTime()) ? "Invalid Date" : value.toISOString();
1870
+ }
1871
+ const name = Array.isArray(value) ? "Array" : className(value);
1872
+ let body;
1873
+ try {
1874
+ body = json(value);
1875
+ } catch {
1876
+ return name;
1877
+ }
1878
+ if (body === void 0) {
1879
+ return name;
1880
+ }
1881
+ return name === "Object" || name === "Array" ? body : `${name} ${body}`;
1882
+ }
1883
+ function previewValue(value, maxLength = PREVIEW_LENGTH) {
1884
+ const preview = fullPreview(value);
1885
+ return preview.length <= maxLength ? preview : `${preview.slice(0, maxLength - 1)}\u2026`;
1886
+ }
1887
+
1888
+ // src/live/RunTraceContext.tsx
1889
+ var import_react12 = require("react");
1890
+ var import_jsx_runtime24 = require("react/jsx-runtime");
1891
+ var RunTraceContext = (0, import_react12.createContext)(null);
1892
+ function RunTraceProvider({ trace, children }) {
1893
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(RunTraceContext.Provider, { value: trace, children });
1894
+ }
1895
+ function useRunTraceStore() {
1896
+ return (0, import_react12.useContext)(RunTraceContext);
1897
+ }
1898
+ function useRunTraceVersion(trace) {
1899
+ return (0, import_react12.useSyncExternalStore)(
1900
+ (onChange) => trace === null ? () => void 0 : trace.subscribe(onChange),
1901
+ () => trace === null ? 0 : trace.getVersion(),
1902
+ () => 0
1903
+ );
1904
+ }
1905
+
1906
+ // src/panels/NodeRunStats.tsx
1907
+ var import_jsx_runtime25 = require("react/jsx-runtime");
1908
+ function NodeRunStats({ nodeId }) {
1909
+ const trace = useRunTraceStore();
1910
+ useRunTraceVersion(trace);
1911
+ if (trace === null) {
1912
+ return null;
1913
+ }
1914
+ const stats = trace.statsOf(nodeId);
1915
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("section", { className: "hl-inspector-section", children: [
1916
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("h3", { className: "hl-inspector-section-title", children: "Run" }),
1917
+ stats === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("p", { className: "hl-inspector-hint hl-runstats-hint", children: "No activity in this run." }) : /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("dl", { className: "hl-runstats", children: [
1918
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "hl-runstats-cell", children: [
1919
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("dt", { children: "In" }),
1920
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("dd", { className: "hl-mono", children: stats.received })
1921
+ ] }),
1922
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "hl-runstats-cell", children: [
1923
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("dt", { children: "Out" }),
1924
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("dd", { className: "hl-mono", children: stats.sent })
1925
+ ] }),
1926
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: `hl-runstats-cell${stats.errors > 0 ? " is-error" : ""}`, children: [
1927
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("dt", { children: "Errors" }),
1928
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("dd", { className: "hl-mono", children: stats.errors })
1929
+ ] }),
1930
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "hl-runstats-cell", children: [
1931
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("dt", { children: "Avg" }),
1932
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("dd", { className: "hl-mono", children: stats.received === 0 ? "\u2014" : formatDuration(stats.totalMs / stats.received) })
1933
+ ] }),
1934
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "hl-runstats-cell", children: [
1935
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("dt", { children: "Max" }),
1936
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("dd", { className: "hl-mono", children: stats.received === 0 ? "\u2014" : formatDuration(stats.maxMs) })
1937
+ ] }),
1938
+ stats.dropped > 0 && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "hl-runstats-cell is-warning", children: [
1939
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("dt", { children: "Dropped" }),
1940
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("dd", { className: "hl-mono", children: stats.dropped })
1941
+ ] }),
1942
+ stats.finished && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "hl-runstats-cell is-done", children: [
1943
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("dt", { children: "State" }),
1944
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("dd", { children: "Finished" })
1945
+ ] }),
1946
+ stats.received > 0 && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "hl-runstats-cell is-wide", children: [
1947
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("dt", { children: "Last in" }),
1948
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("dd", { className: "hl-mono", children: previewValue(stats.lastReceived) })
1949
+ ] }),
1950
+ stats.sent > 0 && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "hl-runstats-cell is-wide", children: [
1951
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("dt", { children: "Last out" }),
1952
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("dd", { className: "hl-mono", children: previewValue(stats.lastSent) })
1953
+ ] })
1954
+ ] })
1955
+ ] });
1956
+ }
1957
+
1205
1958
  // src/panels/InspectorPanel.tsx
1206
- var import_jsx_runtime20 = require("react/jsx-runtime");
1959
+ var import_jsx_runtime26 = require("react/jsx-runtime");
1207
1960
  function InspectorPanel({ core, emptyState }) {
1208
- const selection = (0, import_editor_react2.useSelection)(core);
1961
+ const selection = (0, import_editor_react3.useSelection)(core);
1962
+ const readOnly = (0, import_editor_react3.useReadOnly)(core);
1209
1963
  const selectedNodes2 = selection.selectedNodes;
1210
1964
  const singleId = selectedNodes2.length === 1 ? selectedNodes2[0] : void 0;
1211
1965
  const params = useNodeParams(core, singleId);
1212
1966
  const node = params.node;
1967
+ const singleEdgeId = selectedNodes2.length === 0 && selection.selectedEdges.length === 1 ? selection.selectedEdges[0] : void 0;
1968
+ if (node === void 0 && singleEdgeId !== void 0) {
1969
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(ConnectionInspector, { core, edgeId: singleEdgeId });
1970
+ }
1213
1971
  if (node === void 0) {
1214
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "hl-inspector hl-inspector-empty", children: emptyState != null ? emptyState : /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "hl-inspector-blank", children: [
1215
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "hl-inspector-blank-badge", "aria-hidden": true }),
1216
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("p", { className: "hl-inspector-blank-title", children: selectedNodes2.length > 1 ? `${String(selectedNodes2.length)} nodes selected` : "Nothing selected" }),
1217
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("p", { className: "hl-inspector-hint", children: selectedNodes2.length > 1 ? "Select a single node to edit its parameters." : "Pick a node on the canvas or in the outline to inspect and tune it." })
1972
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: "hl-inspector hl-inspector-empty", children: emptyState != null ? emptyState : /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "hl-inspector-blank", children: [
1973
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { className: "hl-inspector-blank-badge", "aria-hidden": true }),
1974
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("p", { className: "hl-inspector-blank-title", children: selectedNodes2.length > 1 ? `${String(selectedNodes2.length)} nodes selected` : "Nothing selected" }),
1975
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("p", { className: "hl-inspector-hint", children: selectedNodes2.length > 1 ? "Select a single node to edit its parameters." : "Pick a node or a connection on the canvas to inspect and tune it." })
1218
1976
  ] }) });
1219
1977
  }
1220
1978
  const descriptor = core.registry.get(node.code);
1221
1979
  const fields = descriptor === void 0 ? [] : deriveParamSchema(descriptor, node);
1222
1980
  const imageUrl = typeof node.params.imageUrl === "string" && node.params.imageUrl.length > 0 ? node.params.imageUrl : void 0;
1223
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "hl-inspector", children: [
1224
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: `hl-inspector-preview hl-${node.kind.toLowerCase()}`, children: imageUrl !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("img", { className: "hl-inspector-preview-img", src: imageUrl, alt: node.name }) : /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "hl-inspector-preview-badge hl-mono", children: node.kind }) }),
1225
- /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("header", { className: "hl-inspector-head", children: [
1226
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: `hl-inspector-kind hl-${node.kind.toLowerCase()}`, children: node.kind }),
1227
- /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "hl-inspector-titles", children: [
1228
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "hl-inspector-name", children: node.name }),
1229
- /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("span", { className: "hl-inspector-role hl-mono", children: [
1981
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "hl-inspector", children: [
1982
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: `hl-inspector-preview hl-${node.kind.toLowerCase()}`, children: imageUrl !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("img", { className: "hl-inspector-preview-img", src: imageUrl, alt: node.name }) : /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { className: "hl-inspector-preview-badge hl-mono", children: node.kind }) }),
1983
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("header", { className: "hl-inspector-head", children: [
1984
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { className: `hl-inspector-kind hl-${node.kind.toLowerCase()}`, children: node.kind }),
1985
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "hl-inspector-titles", children: [
1986
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { className: "hl-inspector-name", children: node.name }),
1987
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("span", { className: "hl-inspector-role hl-mono", children: [
1230
1988
  node.code,
1231
1989
  " \xB7 ",
1232
1990
  KIND_ROLE[node.kind]
1233
1991
  ] })
1234
1992
  ] })
1235
1993
  ] }),
1236
- /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("section", { className: "hl-inspector-section", children: [
1237
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("h3", { className: "hl-inspector-section-title", children: "Parameters" }),
1238
- fields.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("p", { className: "hl-inspector-hint", children: "This node has no parameters." }) : /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
1994
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("section", { className: "hl-inspector-section", children: [
1995
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("h3", { className: "hl-inspector-section-title", children: "Parameters" }),
1996
+ fields.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("p", { className: "hl-inspector-hint", children: "This node has no parameters." }) : /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
1239
1997
  ParamForm,
1240
1998
  {
1241
1999
  fields,
2000
+ disabled: readOnly,
1242
2001
  onCommit: (key, value) => params.setParam(key, value)
1243
2002
  }
1244
2003
  )
1245
2004
  ] }),
1246
- /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("section", { className: "hl-inspector-section", children: [
1247
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("h3", { className: "hl-inspector-section-title", children: "Ports" }),
1248
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("ul", { className: "hl-inspector-ports", children: node.ports.map((port) => /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
2005
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(NodeRunStats, { nodeId: node.id }),
2006
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("section", { className: "hl-inspector-section", children: [
2007
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("h3", { className: "hl-inspector-section-title", children: "Ports" }),
2008
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("ul", { className: "hl-inspector-ports", children: node.ports.map((port) => /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
1249
2009
  "li",
1250
2010
  {
1251
2011
  className: `hl-inspector-port${port.isError ? " is-error" : ""}`,
1252
2012
  children: [
1253
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: `hl-port-dot hl-dir-${port.direction.toLowerCase()}` }),
1254
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "hl-mono", children: port.name }),
1255
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "hl-inspector-port-dir hl-mono", children: port.direction })
2013
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { className: `hl-port-dot hl-dir-${port.direction.toLowerCase()}` }),
2014
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { className: "hl-mono", children: port.name }),
2015
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { className: "hl-inspector-port-dir hl-mono", children: port.direction })
1256
2016
  ]
1257
2017
  },
1258
2018
  port.id
@@ -1262,14 +2022,14 @@ function InspectorPanel({ core, emptyState }) {
1262
2022
  }
1263
2023
 
1264
2024
  // src/panels/OutlineTree.tsx
1265
- var import_react9 = require("react");
1266
- var import_editor_core2 = require("@hlang-org/editor-core");
1267
- var import_editor_react3 = require("@hlang-org/editor-react");
1268
- var import_jsx_runtime21 = require("react/jsx-runtime");
2025
+ var import_react13 = require("react");
2026
+ var import_editor_core3 = require("@hlang-org/editor-core");
2027
+ var import_editor_react4 = require("@hlang-org/editor-react");
2028
+ var import_jsx_runtime27 = require("react/jsx-runtime");
1269
2029
  function useTopologyComponents(core) {
1270
- const [tick, setTick] = (0, import_react9.useState)(0);
1271
- const bump = (0, import_react9.useCallback)(() => setTick((t) => t + 1), []);
1272
- (0, import_react9.useEffect)(() => {
2030
+ const [tick, setTick] = (0, import_react13.useState)(0);
2031
+ const bump = (0, import_react13.useCallback)(() => setTick((t) => t + 1), []);
2032
+ (0, import_react13.useEffect)(() => {
1273
2033
  const kinds = ["nodeAdded", "nodeRemoved", "nodeChanged", "edgeAdded", "edgeRemoved"];
1274
2034
  const disposers = kinds.map((k) => core.events.on(k, bump));
1275
2035
  bump();
@@ -1279,27 +2039,27 @@ function useTopologyComponents(core) {
1279
2039
  }
1280
2040
  };
1281
2041
  }, [core, bump]);
1282
- return (0, import_react9.useMemo)(() => {
2042
+ return (0, import_react13.useMemo)(() => {
1283
2043
  void tick;
1284
- const topo = new import_editor_core2.GraphTopology();
2044
+ const topo = new import_editor_core3.GraphTopology();
1285
2045
  topo.reset(core.model.getNodes(), core.model.getEdges());
1286
2046
  return topo.components();
1287
2047
  }, [core, tick]);
1288
2048
  }
1289
2049
  function OutlineTree({ core, onFocus }) {
1290
2050
  const components = useTopologyComponents(core);
1291
- const selection = (0, import_editor_react3.useSelection)(core);
1292
- const viewState = (0, import_editor_react3.useViewState)(core);
1293
- const diagnostics = (0, import_editor_react3.useDiagnostics)(core);
1294
- const [collapsed, setCollapsed] = (0, import_react9.useState)(/* @__PURE__ */ new Set());
1295
- const nodeById = (0, import_react9.useMemo)(() => {
2051
+ const selection = (0, import_editor_react4.useSelection)(core);
2052
+ const viewState = (0, import_editor_react4.useViewState)(core);
2053
+ const diagnostics = (0, import_editor_react4.useDiagnostics)(core);
2054
+ const [collapsed, setCollapsed] = (0, import_react13.useState)(/* @__PURE__ */ new Set());
2055
+ const nodeById = (0, import_react13.useMemo)(() => {
1296
2056
  const m = /* @__PURE__ */ new Map();
1297
2057
  for (const n of core.model.getNodes()) {
1298
2058
  m.set(n.id, n);
1299
2059
  }
1300
2060
  return m;
1301
2061
  }, [core, components]);
1302
- const toggleGroup = (0, import_react9.useCallback)((id) => {
2062
+ const toggleGroup = (0, import_react13.useCallback)((id) => {
1303
2063
  setCollapsed((prev) => {
1304
2064
  const next = new Set(prev);
1305
2065
  if (next.has(id)) {
@@ -1310,7 +2070,7 @@ function OutlineTree({ core, onFocus }) {
1310
2070
  return next;
1311
2071
  });
1312
2072
  }, []);
1313
- const select = (0, import_react9.useCallback)(
2073
+ const select = (0, import_react13.useCallback)(
1314
2074
  (id) => {
1315
2075
  selection.selectNodes([id], "replace");
1316
2076
  onFocus == null ? void 0 : onFocus(id);
@@ -1318,80 +2078,80 @@ function OutlineTree({ core, onFocus }) {
1318
2078
  [selection, onFocus]
1319
2079
  );
1320
2080
  if (components.length === 0) {
1321
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("p", { className: "hl-outline-empty hl-mono", children: "no layers yet" });
2081
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("p", { className: "hl-outline-empty hl-mono", children: "no layers yet" });
1322
2082
  }
1323
2083
  const groups = [...components].sort((a, b) => b.nodeIds.size - a.nodeIds.size);
1324
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "hl-outline", role: "tree", "aria-label": "Outline", children: groups.map((group, gi) => {
2084
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { className: "hl-outline", role: "tree", "aria-label": "Outline", children: groups.map((group, gi) => {
1325
2085
  const members = [...group.nodeIds].map((id) => nodeById.get(id)).filter((n) => n !== void 0);
1326
2086
  const open = !collapsed.has(group.id);
1327
2087
  const groupName = `Flow ${String(gi + 1)}`;
1328
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "hl-outline-group", role: "treeitem", "aria-expanded": open, children: [
1329
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
2088
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("div", { className: "hl-outline-group", role: "treeitem", "aria-expanded": open, children: [
2089
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
1330
2090
  "button",
1331
2091
  {
1332
2092
  className: "hl-outline-grouphead",
1333
2093
  onClick: () => toggleGroup(group.id),
1334
2094
  children: [
1335
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "hl-outline-caret" + (open ? " is-open" : ""), "aria-hidden": true, children: "\u25B8" }),
1336
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "hl-outline-groupname", children: groupName }),
1337
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "hl-outline-count hl-mono", children: group.nodeIds.size })
2095
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "hl-outline-caret" + (open ? " is-open" : ""), "aria-hidden": true, children: "\u25B8" }),
2096
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "hl-outline-groupname", children: groupName }),
2097
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "hl-outline-count hl-mono", children: group.nodeIds.size })
1338
2098
  ]
1339
2099
  }
1340
2100
  ),
1341
- open && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("ul", { className: "hl-outline-members", children: members.map((node) => {
2101
+ open && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("ul", { className: "hl-outline-members", children: members.map((node) => {
1342
2102
  const active = selection.isSelected(node.id);
1343
2103
  const isHidden = viewState.isHidden(node.id);
1344
2104
  const isLocked = viewState.isLocked(node.id);
1345
2105
  const { imageUrl } = node.params;
1346
2106
  const thumbnail = typeof imageUrl === "string" && imageUrl.length > 0 ? imageUrl : void 0;
1347
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
2107
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
1348
2108
  "div",
1349
2109
  {
1350
2110
  className: "hl-outline-row" + (active ? " is-active" : "") + (isHidden ? " is-hidden" : "") + (isLocked ? " is-locked" : ""),
1351
2111
  children: [
1352
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("button", { className: "hl-outline-main", onClick: () => select(node.id), children: [
1353
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
2112
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("button", { className: "hl-outline-main", onClick: () => select(node.id), children: [
2113
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
1354
2114
  "span",
1355
2115
  {
1356
2116
  className: `hl-outline-icon hl-kind-${node.kind.toLowerCase()}`,
1357
2117
  "aria-hidden": true,
1358
- children: thumbnail !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
2118
+ children: thumbnail !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
1359
2119
  "img",
1360
2120
  {
1361
2121
  className: "hl-outline-thumb",
1362
2122
  src: thumbnail,
1363
2123
  alt: ""
1364
2124
  }
1365
- ) : /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(NodeIcon, { code: node.code, kind: node.kind, size: 13 })
2125
+ ) : /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(NodeIcon, { code: node.code, kind: node.kind, size: 13 })
1366
2126
  }
1367
2127
  ),
1368
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "hl-outline-name", children: node.name }),
2128
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "hl-outline-name", children: node.name }),
1369
2129
  (() => {
1370
2130
  const diag = diagnostics.worstOf(node.id);
1371
- return diag === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(DiagnosticBadge, { diagnostic: diag, variant: "hl-outline-diag", size: 11 });
2131
+ return diag === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(DiagnosticBadge, { diagnostic: diag, variant: "hl-outline-diag", size: 11 });
1372
2132
  })(),
1373
- isLocked && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "hl-outline-badge", "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(LockIcon, { size: 11 }) }),
1374
- isHidden && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "hl-outline-badge", "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(EyeOffIcon, { size: 11 }) })
2133
+ isLocked && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "hl-outline-badge", "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(LockIcon, { size: 11 }) }),
2134
+ isHidden && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "hl-outline-badge", "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(EyeOffIcon, { size: 11 }) })
1375
2135
  ] }),
1376
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("span", { className: "hl-outline-controls", children: [
1377
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
2136
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("span", { className: "hl-outline-controls", children: [
2137
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
1378
2138
  "button",
1379
2139
  {
1380
2140
  className: "hl-outline-ctl" + (isHidden ? " is-active-ctl" : ""),
1381
2141
  onClick: () => viewState.toggleHidden(node.id),
1382
2142
  "aria-label": isHidden ? "Show layer" : "Hide layer",
1383
2143
  title: isHidden ? "Show" : "Hide",
1384
- children: isHidden ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(EyeOffIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(EyeIcon, {})
2144
+ children: isHidden ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(EyeOffIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(EyeIcon, {})
1385
2145
  }
1386
2146
  ),
1387
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
2147
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
1388
2148
  "button",
1389
2149
  {
1390
2150
  className: "hl-outline-ctl" + (isLocked ? " is-active-ctl" : ""),
1391
2151
  onClick: () => viewState.toggleLocked(node.id),
1392
2152
  "aria-label": isLocked ? "Unlock layer" : "Lock layer",
1393
2153
  title: isLocked ? "Unlock" : "Lock",
1394
- children: isLocked ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(LockIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(UnlockIcon, {})
2154
+ children: isLocked ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(LockIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(UnlockIcon, {})
1395
2155
  }
1396
2156
  )
1397
2157
  ] })
@@ -1404,21 +2164,48 @@ function OutlineTree({ core, onFocus }) {
1404
2164
  }
1405
2165
 
1406
2166
  // src/panels/PreviewPanel.tsx
1407
- var import_jsx_runtime22 = require("react/jsx-runtime");
2167
+ var import_jsx_runtime28 = require("react/jsx-runtime");
1408
2168
  function PreviewPanel({ imageUrl, title = "Preview", status }) {
1409
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("section", { className: "hl-preview", "aria-label": "Preview", children: [
1410
- /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "hl-preview-head", children: [
1411
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "hl-preview-title", children: title }),
1412
- status !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "hl-preview-status hl-mono", children: status })
2169
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("section", { className: "hl-preview", "aria-label": "Preview", children: [
2170
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("div", { className: "hl-preview-head", children: [
2171
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: "hl-preview-title", children: title }),
2172
+ status !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: "hl-preview-status hl-mono", children: status })
1413
2173
  ] }),
1414
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { className: "hl-preview-frame", children: imageUrl !== void 0 && imageUrl.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("img", { className: "hl-preview-img", src: imageUrl, alt: title, draggable: false }) : /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { className: "hl-preview-placeholder hl-mono", children: "no output yet" }) })
2174
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { className: "hl-preview-frame", children: imageUrl !== void 0 && imageUrl.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("img", { className: "hl-preview-img", src: imageUrl, alt: title, draggable: false }) : /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { className: "hl-preview-placeholder hl-mono", children: "no output yet" }) })
1415
2175
  ] });
1416
2176
  }
1417
2177
 
2178
+ // src/panels/PanelHead.tsx
2179
+ var import_jsx_runtime29 = require("react/jsx-runtime");
2180
+ function PanelHead({ leading, meta, children }) {
2181
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("header", { className: "hl-panel-head", children: [
2182
+ leading,
2183
+ meta === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { className: "hl-panel-meta hl-mono", children: meta }),
2184
+ children === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { className: "hl-panel-actions", children })
2185
+ ] });
2186
+ }
2187
+ function PanelAction({ icon, label, onClick, pressed, disabled = false, title }) {
2188
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
2189
+ "button",
2190
+ {
2191
+ type: "button",
2192
+ className: `hl-panel-action${pressed === true ? " is-on" : ""}`,
2193
+ "aria-pressed": pressed,
2194
+ disabled,
2195
+ title: title != null ? title : label,
2196
+ onClick,
2197
+ children: [
2198
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { className: "hl-panel-action-icon", "aria-hidden": true, children: icon }),
2199
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { className: "hl-panel-action-label", children: label })
2200
+ ]
2201
+ }
2202
+ );
2203
+ }
2204
+
1418
2205
  // src/panels/RailTabs.tsx
1419
- var import_jsx_runtime23 = require("react/jsx-runtime");
2206
+ var import_jsx_runtime30 = require("react/jsx-runtime");
1420
2207
  function RailTabs({ tabs, active, onChange }) {
1421
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { className: "hl-railtabs", role: "tablist", children: tabs.map((tab) => /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
2208
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { className: "hl-railtabs", role: "tablist", children: tabs.map((tab) => /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
1422
2209
  "button",
1423
2210
  {
1424
2211
  role: "tab",
@@ -1432,20 +2219,20 @@ function RailTabs({ tabs, active, onChange }) {
1432
2219
  }
1433
2220
 
1434
2221
  // src/panels/AssetGrid.tsx
1435
- var import_jsx_runtime24 = require("react/jsx-runtime");
2222
+ var import_jsx_runtime31 = require("react/jsx-runtime");
1436
2223
  function AssetGrid({ items, onSelect, emptyHint = "No assets yet" }) {
1437
2224
  if (items.length === 0) {
1438
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("p", { className: "hl-assetgrid-empty hl-mono", children: emptyHint });
2225
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("p", { className: "hl-assetgrid-empty hl-mono", children: emptyHint });
1439
2226
  }
1440
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { className: "hl-assetgrid", children: items.map((item) => {
2227
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("div", { className: "hl-assetgrid", children: items.map((item) => {
1441
2228
  var _a;
1442
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
2229
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
1443
2230
  "button",
1444
2231
  {
1445
2232
  className: "hl-assetgrid-cell",
1446
2233
  onClick: () => onSelect == null ? void 0 : onSelect(item.id),
1447
2234
  title: item.label,
1448
- children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("img", { className: "hl-assetgrid-img", src: item.url, alt: (_a = item.label) != null ? _a : "", loading: "lazy" })
2235
+ children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("img", { className: "hl-assetgrid-img", src: item.url, alt: (_a = item.label) != null ? _a : "", loading: "lazy" })
1449
2236
  },
1450
2237
  item.id
1451
2238
  );
@@ -1453,10 +2240,10 @@ function AssetGrid({ items, onSelect, emptyHint = "No assets yet" }) {
1453
2240
  }
1454
2241
 
1455
2242
  // src/panels/PositionPanel.tsx
1456
- var import_react10 = require("react");
1457
- var import_editor_core3 = require("@hlang-org/editor-core");
1458
- var import_editor_react4 = require("@hlang-org/editor-react");
1459
- var import_jsx_runtime25 = require("react/jsx-runtime");
2243
+ var import_react14 = require("react");
2244
+ var import_editor_core4 = require("@hlang-org/editor-core");
2245
+ var import_editor_react5 = require("@hlang-org/editor-react");
2246
+ var import_jsx_runtime32 = require("react/jsx-runtime");
1460
2247
  function selectedNodes(core) {
1461
2248
  const ids = core.selection.getSelectedNodes();
1462
2249
  const out = [];
@@ -1470,9 +2257,10 @@ function selectedNodes(core) {
1470
2257
  }
1471
2258
  function PositionPanel({ core, livePosition }) {
1472
2259
  var _a;
1473
- (0, import_editor_react4.useSelection)(core);
1474
- const [, bump] = (0, import_react10.useReducer)((n) => n + 1, 0);
1475
- (0, import_react10.useEffect)(() => {
2260
+ (0, import_editor_react5.useSelection)(core);
2261
+ const readOnly = (0, import_editor_react5.useReadOnly)(core);
2262
+ const [, bump] = (0, import_react14.useReducer)((n) => n + 1, 0);
2263
+ (0, import_react14.useEffect)(() => {
1476
2264
  const kinds = ["nodeChanged", "nodeAdded", "nodeRemoved"];
1477
2265
  const disposers = kinds.map((k) => core.events.on(k, bump));
1478
2266
  return () => {
@@ -1484,7 +2272,7 @@ function PositionPanel({ core, livePosition }) {
1484
2272
  const nodes = selectedNodes(core);
1485
2273
  const single = nodes.length === 1 ? nodes[0] : void 0;
1486
2274
  const shownPos = single !== void 0 ? (_a = livePosition == null ? void 0 : livePosition(single.id)) != null ? _a : single.position : void 0;
1487
- const moveAll = (0, import_react10.useCallback)(
2275
+ const moveAll = (0, import_react14.useCallback)(
1488
2276
  (fn) => {
1489
2277
  const targets = selectedNodes(core);
1490
2278
  if (targets.length === 0) {
@@ -1492,13 +2280,13 @@ function PositionPanel({ core, livePosition }) {
1492
2280
  }
1493
2281
  core.history.transact("Align", () => {
1494
2282
  for (const n of targets) {
1495
- core.commands.execute(new import_editor_core3.MoveNodeCommand(n.id, fn(n)));
2283
+ core.commands.execute(new import_editor_core4.MoveNodeCommand(n.id, fn(n)));
1496
2284
  }
1497
2285
  });
1498
2286
  },
1499
2287
  [core]
1500
2288
  );
1501
- const align = (0, import_react10.useCallback)(
2289
+ const align = (0, import_react14.useCallback)(
1502
2290
  (kind) => {
1503
2291
  const targets = selectedNodes(core);
1504
2292
  if (targets.length === 0) {
@@ -1531,17 +2319,17 @@ function PositionPanel({ core, livePosition }) {
1531
2319
  },
1532
2320
  [core, moveAll]
1533
2321
  );
1534
- const setCoord = (0, import_react10.useCallback)(
2322
+ const setCoord = (0, import_react14.useCallback)(
1535
2323
  (axis, value) => {
1536
2324
  if (single === void 0 || Number.isNaN(value)) {
1537
2325
  return;
1538
2326
  }
1539
2327
  const next = axis === "x" ? { x: value, y: single.position.y } : { x: single.position.x, y: value };
1540
- core.commands.execute(new import_editor_core3.MoveNodeCommand(single.id, next));
2328
+ core.commands.execute(new import_editor_core4.MoveNodeCommand(single.id, next));
1541
2329
  },
1542
2330
  [core, single]
1543
2331
  );
1544
- const disabled = nodes.length === 0;
2332
+ const disabled = readOnly || nodes.length === 0;
1545
2333
  const alignButtons = [
1546
2334
  { kind: "left", label: "Align left", d: "M5 4v16M9 8h8M9 16h5" },
1547
2335
  { kind: "hcenter", label: "Align centre", d: "M12 4v16M8 9h8M9 15h6" },
@@ -1550,9 +2338,9 @@ function PositionPanel({ core, livePosition }) {
1550
2338
  { kind: "vcenter", label: "Align middle", d: "M4 12h16M9 8v8M15 9v6" },
1551
2339
  { kind: "bottom", label: "Align bottom", d: "M4 19h16M8 7v8M16 12v3" }
1552
2340
  ];
1553
- return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("section", { className: "hl-position", children: [
1554
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("h3", { className: "hl-position-title hl-mono", children: "Position" }),
1555
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { className: "hl-position-align", role: "group", "aria-label": "Align", children: alignButtons.map((b) => /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
2341
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("section", { className: "hl-position", children: [
2342
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("h3", { className: "hl-position-title hl-mono", children: "Position" }),
2343
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("div", { className: "hl-position-align", role: "group", "aria-label": "Align", children: alignButtons.map((b) => /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
1556
2344
  "button",
1557
2345
  {
1558
2346
  className: "hl-position-alignbtn",
@@ -1560,55 +2348,55 @@ function PositionPanel({ core, livePosition }) {
1560
2348
  disabled,
1561
2349
  title: b.label,
1562
2350
  "aria-label": b.label,
1563
- children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("svg", { width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("path", { d: b.d }) })
2351
+ children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("svg", { width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("path", { d: b.d }) })
1564
2352
  },
1565
2353
  b.kind
1566
2354
  )) }),
1567
- /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "hl-position-coords", children: [
1568
- /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("label", { className: "hl-position-field", children: [
1569
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
2355
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("div", { className: "hl-position-coords", children: [
2356
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("label", { className: "hl-position-field", children: [
2357
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
1570
2358
  "input",
1571
2359
  {
1572
2360
  type: "number",
1573
2361
  className: "hl-position-input hl-mono",
1574
2362
  value: shownPos !== void 0 ? Math.round(shownPos.x) : "",
1575
- disabled: single === void 0,
2363
+ disabled: readOnly || single === void 0,
1576
2364
  onChange: (e) => setCoord("x", Number(e.target.value)),
1577
2365
  "aria-label": "X position"
1578
2366
  }
1579
2367
  ),
1580
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { className: "hl-position-axis hl-mono", children: "X" })
2368
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "hl-position-axis hl-mono", children: "X" })
1581
2369
  ] }),
1582
- /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("label", { className: "hl-position-field", children: [
1583
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
2370
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("label", { className: "hl-position-field", children: [
2371
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
1584
2372
  "input",
1585
2373
  {
1586
2374
  type: "number",
1587
2375
  className: "hl-position-input hl-mono",
1588
2376
  value: shownPos !== void 0 ? Math.round(shownPos.y) : "",
1589
- disabled: single === void 0,
2377
+ disabled: readOnly || single === void 0,
1590
2378
  onChange: (e) => setCoord("y", Number(e.target.value)),
1591
2379
  "aria-label": "Y position"
1592
2380
  }
1593
2381
  ),
1594
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { className: "hl-position-axis hl-mono", children: "Y" })
2382
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "hl-position-axis hl-mono", children: "Y" })
1595
2383
  ] })
1596
2384
  ] })
1597
2385
  ] });
1598
2386
  }
1599
2387
 
1600
2388
  // src/command-palette/NodeSearch.tsx
1601
- var import_react11 = require("react");
1602
- var import_jsx_runtime26 = require("react/jsx-runtime");
2389
+ var import_react15 = require("react");
2390
+ var import_jsx_runtime33 = require("react/jsx-runtime");
1603
2391
  var PANEL_WIDTH = 320;
1604
2392
  var PANEL_MAX_HEIGHT = 360;
1605
2393
  function NodeSearch({ controller }) {
1606
2394
  const { state } = controller;
1607
- const [query, setQuery] = (0, import_react11.useState)("");
1608
- const [active, setActive] = (0, import_react11.useState)(0);
1609
- const inputRef = (0, import_react11.useRef)(null);
2395
+ const [query, setQuery] = (0, import_react15.useState)("");
2396
+ const [active, setActive] = (0, import_react15.useState)(0);
2397
+ const inputRef = (0, import_react15.useRef)(null);
1610
2398
  const isOpen = state !== void 0;
1611
- (0, import_react11.useEffect)(() => {
2399
+ (0, import_react15.useEffect)(() => {
1612
2400
  var _a;
1613
2401
  if (isOpen) {
1614
2402
  setQuery("");
@@ -1616,12 +2404,12 @@ function NodeSearch({ controller }) {
1616
2404
  (_a = inputRef.current) == null ? void 0 : _a.focus();
1617
2405
  }
1618
2406
  }, [isOpen]);
1619
- const results = (0, import_react11.useMemo)(
2407
+ const results = (0, import_react15.useMemo)(
1620
2408
  () => state !== void 0 ? controller.results(query) : [],
1621
2409
  [controller, state, query]
1622
2410
  );
1623
2411
  const clampedActive = results.length === 0 ? 0 : Math.min(active, results.length - 1);
1624
- const choose = (0, import_react11.useCallback)(
2412
+ const choose = (0, import_react15.useCallback)(
1625
2413
  (view) => {
1626
2414
  if (view !== void 0) {
1627
2415
  controller.commit(view);
@@ -1629,7 +2417,7 @@ function NodeSearch({ controller }) {
1629
2417
  },
1630
2418
  [controller]
1631
2419
  );
1632
- const onKeyDown = (0, import_react11.useCallback)(
2420
+ const onKeyDown = (0, import_react15.useCallback)(
1633
2421
  (event) => {
1634
2422
  switch (event.key) {
1635
2423
  case "ArrowDown":
@@ -1659,8 +2447,8 @@ function NodeSearch({ controller }) {
1659
2447
  }
1660
2448
  const style = clampToViewport(state.screen.x, state.screen.y);
1661
2449
  const connectMode = state.pending !== void 0;
1662
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(import_jsx_runtime26.Fragment, { children: [
1663
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
2450
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(import_jsx_runtime33.Fragment, { children: [
2451
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
1664
2452
  "div",
1665
2453
  {
1666
2454
  className: "hl-node-search-scrim",
@@ -1669,10 +2457,10 @@ function NodeSearch({ controller }) {
1669
2457
  }
1670
2458
  }
1671
2459
  ),
1672
- /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "hl-node-search", style, role: "dialog", "aria-label": "Create node", children: [
1673
- /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "hl-node-search-field", children: [
1674
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { className: "hl-node-search-glyph", "aria-hidden": true, children: connectMode ? "\u2933" : "\uFF0B" }),
1675
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
2460
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("div", { className: "hl-node-search", style, role: "dialog", "aria-label": "Create node", children: [
2461
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("div", { className: "hl-node-search-field", children: [
2462
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: "hl-node-search-glyph", "aria-hidden": true, children: connectMode ? "\u2933" : "\uFF0B" }),
2463
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
1676
2464
  "input",
1677
2465
  {
1678
2466
  ref: inputRef,
@@ -1690,12 +2478,12 @@ function NodeSearch({ controller }) {
1690
2478
  }
1691
2479
  )
1692
2480
  ] }),
1693
- /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("ul", { className: "hl-node-search-list", role: "listbox", children: [
1694
- results.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("li", { className: "hl-node-search-empty hl-mono", children: "no matching nodes" }),
2481
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("ul", { className: "hl-node-search-list", role: "listbox", children: [
2482
+ results.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("li", { className: "hl-node-search-empty hl-mono", children: "no matching nodes" }),
1695
2483
  results.map((view, index) => {
1696
2484
  const descriptor = view.result.value;
1697
2485
  const kind = descriptor.kind;
1698
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
2486
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
1699
2487
  "li",
1700
2488
  {
1701
2489
  role: "option",
@@ -1707,15 +2495,15 @@ function NodeSearch({ controller }) {
1707
2495
  choose(view);
1708
2496
  },
1709
2497
  children: [
1710
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
2498
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
1711
2499
  "span",
1712
2500
  {
1713
2501
  className: `hl-node-search-kind hl-mono hl-kind-${kind.toLowerCase()}`,
1714
2502
  children: kind
1715
2503
  }
1716
2504
  ),
1717
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { className: "hl-node-search-label", children: highlight(descriptor.displayName, view.result.match) }),
1718
- view.connectPortId !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { className: "hl-node-search-port hl-mono", children: view.connectPortId })
2505
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: "hl-node-search-label", children: highlight(descriptor.displayName, view.result.match) }),
2506
+ view.connectPortId !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: "hl-node-search-port hl-mono", children: view.connectPortId })
1719
2507
  ]
1720
2508
  },
1721
2509
  descriptor.code
@@ -1729,7 +2517,7 @@ function highlight(text, match) {
1729
2517
  const hit = new Set(match.positions);
1730
2518
  const chars = Array.from(text);
1731
2519
  return chars.map(
1732
- (ch, i) => hit.has(i) ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("mark", { className: "hl-node-search-hit", children: ch }, i) : /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { children: ch }, i)
2520
+ (ch, i) => hit.has(i) ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("mark", { className: "hl-node-search-hit", children: ch }, i) : /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { children: ch }, i)
1733
2521
  );
1734
2522
  }
1735
2523
  function clampToViewport(x, y) {
@@ -1742,8 +2530,8 @@ function clampToViewport(x, y) {
1742
2530
  }
1743
2531
 
1744
2532
  // src/command-palette/useNodeSearch.ts
1745
- var import_react12 = require("react");
1746
- var import_editor_core4 = require("@hlang-org/editor-core");
2533
+ var import_react16 = require("react");
2534
+ var import_editor_core5 = require("@hlang-org/editor-core");
1747
2535
 
1748
2536
  // src/command-palette/fuzzy.ts
1749
2537
  function fuzzyScore(query, target) {
@@ -1833,21 +2621,21 @@ function firstCompletingPort(ports, pending, validator) {
1833
2621
 
1834
2622
  // src/command-palette/useNodeSearch.ts
1835
2623
  function useNodeSearch(core) {
1836
- const [state, setState] = (0, import_react12.useState)(void 0);
1837
- const openBlank = (0, import_react12.useCallback)((screen, flow) => {
2624
+ const [state, setState] = (0, import_react16.useState)(void 0);
2625
+ const openBlank = (0, import_react16.useCallback)((screen, flow) => {
1838
2626
  setState({ screen, flow });
1839
2627
  }, []);
1840
- const openFromConnection = (0, import_react12.useCallback)(
2628
+ const openFromConnection = (0, import_react16.useCallback)(
1841
2629
  (screen, flow, pending) => {
1842
2630
  setState({ screen, flow, pending });
1843
2631
  },
1844
2632
  []
1845
2633
  );
1846
- const close = (0, import_react12.useCallback)(() => {
2634
+ const close = (0, import_react16.useCallback)(() => {
1847
2635
  setState(void 0);
1848
2636
  }, []);
1849
2637
  const descriptors = core.registry.list();
1850
- const results = (0, import_react12.useCallback)(
2638
+ const results = (0, import_react16.useCallback)(
1851
2639
  (query) => {
1852
2640
  if ((state == null ? void 0 : state.pending) !== void 0) {
1853
2641
  return connectableCandidates(
@@ -1861,7 +2649,7 @@ function useNodeSearch(core) {
1861
2649
  },
1862
2650
  [state, descriptors, core]
1863
2651
  );
1864
- const commit = (0, import_react12.useCallback)(
2652
+ const commit = (0, import_react16.useCallback)(
1865
2653
  (view) => {
1866
2654
  if (state === void 0) {
1867
2655
  return;
@@ -1872,44 +2660,44 @@ function useNodeSearch(core) {
1872
2660
  const pending = state.pending;
1873
2661
  const connectPortId = view.connectPortId;
1874
2662
  core.history.transact("Create & Connect Node", () => {
1875
- core.commands.execute(new import_editor_core4.AddNodeCommand(node));
2663
+ core.commands.execute(new import_editor_core5.AddNodeCommand(node));
1876
2664
  const edgeId = core.idGenerator.nextEdgeId();
1877
2665
  const newEndpoint = { nodeId: node.id, portId: connectPortId };
1878
2666
  const [source, target] = pending.direction === "from-source" ? [pending.fromEndpoint, newEndpoint] : [newEndpoint, pending.fromEndpoint];
1879
- core.commands.execute(new import_editor_core4.ConnectCommand(edgeId, source, target));
2667
+ core.commands.execute(new import_editor_core5.ConnectCommand(edgeId, source, target));
1880
2668
  });
1881
2669
  } else {
1882
- core.commands.execute(new import_editor_core4.AddNodeCommand(node));
2670
+ core.commands.execute(new import_editor_core5.AddNodeCommand(node));
1883
2671
  }
1884
2672
  setState(void 0);
1885
2673
  },
1886
2674
  [state, core]
1887
2675
  );
1888
- return (0, import_react12.useMemo)(
2676
+ return (0, import_react16.useMemo)(
1889
2677
  () => ({ state, openBlank, openFromConnection, close, results, commit }),
1890
2678
  [state, openBlank, openFromConnection, close, results, commit]
1891
2679
  );
1892
2680
  }
1893
2681
 
1894
2682
  // src/command-palette/useNodeSearchTriggers.ts
1895
- var import_react13 = require("react");
1896
- var import_react14 = require("@xyflow/react");
2683
+ var import_react17 = require("react");
2684
+ var import_react18 = require("@xyflow/react");
1897
2685
  var DEFAULT_DOUBLE_CLICK_MS = 350;
1898
2686
  function useNodeSearchTriggers(core, search, options = {}) {
1899
2687
  var _a, _b;
1900
- const flow = (0, import_react14.useReactFlow)();
2688
+ const flow = (0, import_react18.useReactFlow)();
1901
2689
  const doubleClickMs = (_a = options.doubleClickMs) != null ? _a : DEFAULT_DOUBLE_CLICK_MS;
1902
2690
  const now = (_b = options.now) != null ? _b : defaultNow;
1903
- const startRef = (0, import_react13.useRef)(null);
1904
- const lastPaneClickRef = (0, import_react13.useRef)(Number.NEGATIVE_INFINITY);
1905
- const onConnectStart = (0, import_react13.useCallback)((_event, params) => {
2691
+ const startRef = (0, import_react17.useRef)(null);
2692
+ const lastPaneClickRef = (0, import_react17.useRef)(Number.NEGATIVE_INFINITY);
2693
+ const onConnectStart = (0, import_react17.useCallback)((_event, params) => {
1906
2694
  startRef.current = params;
1907
2695
  }, []);
1908
- const onConnectEnd = (0, import_react13.useCallback)(
2696
+ const onConnectEnd = (0, import_react17.useCallback)(
1909
2697
  (event, connectionState) => {
1910
2698
  const start = startRef.current;
1911
2699
  startRef.current = null;
1912
- if (connectionState.toNode !== null || start === null) {
2700
+ if (core.mode.readOnly || connectionState.toNode !== null || start === null) {
1913
2701
  return;
1914
2702
  }
1915
2703
  if (start.nodeId === null || start.handleId === null || start.handleType === null) {
@@ -1938,8 +2726,11 @@ function useNodeSearchTriggers(core, search, options = {}) {
1938
2726
  },
1939
2727
  [core, flow, search]
1940
2728
  );
1941
- const onPaneClick = (0, import_react13.useCallback)(
2729
+ const onPaneClick = (0, import_react17.useCallback)(
1942
2730
  (event) => {
2731
+ if (core.mode.readOnly) {
2732
+ return;
2733
+ }
1943
2734
  const t = now();
1944
2735
  const isDouble = t - lastPaneClickRef.current <= doubleClickMs;
1945
2736
  lastPaneClickRef.current = t;
@@ -1950,7 +2741,7 @@ function useNodeSearchTriggers(core, search, options = {}) {
1950
2741
  const screen = { x: event.clientX, y: event.clientY };
1951
2742
  search.openBlank(screen, flow.screenToFlowPosition(screen));
1952
2743
  },
1953
- [flow, search, doubleClickMs, now]
2744
+ [core, flow, search, doubleClickMs, now]
1954
2745
  );
1955
2746
  return { onConnectStart, onConnectEnd, onPaneClick };
1956
2747
  }
@@ -1970,8 +2761,8 @@ function pointerOf(event) {
1970
2761
  }
1971
2762
 
1972
2763
  // src/hooks/useEditorHotkeys.ts
1973
- var import_react15 = require("react");
1974
- var import_editor_react5 = require("@hlang-org/editor-react");
2764
+ var import_react19 = require("react");
2765
+ var import_editor_react6 = require("@hlang-org/editor-react");
1975
2766
  function isEditableTarget(target) {
1976
2767
  if (!(target instanceof HTMLElement)) {
1977
2768
  return false;
@@ -1987,9 +2778,9 @@ function withinScope(target, scope) {
1987
2778
  return root !== null && target instanceof Node && root.contains(target);
1988
2779
  }
1989
2780
  function useEditorHotkeys(core, options = {}) {
1990
- const clipboard = (0, import_editor_react5.useClipboard)(core);
2781
+ const clipboard = (0, import_editor_react6.useClipboard)(core);
1991
2782
  const { scope } = options;
1992
- (0, import_react15.useEffect)(() => {
2783
+ (0, import_react19.useEffect)(() => {
1993
2784
  const onKeyDown = (event) => {
1994
2785
  if (!withinScope(event.target, scope) || isEditableTarget(event.target)) {
1995
2786
  return;
@@ -2052,33 +2843,33 @@ function useEditorHotkeys(core, options = {}) {
2052
2843
  }
2053
2844
 
2054
2845
  // src/context-menu/NodeContextMenu.tsx
2055
- var import_react16 = require("react");
2056
- var import_jsx_runtime27 = require("react/jsx-runtime");
2846
+ var import_react20 = require("react");
2847
+ var import_jsx_runtime34 = require("react/jsx-runtime");
2057
2848
  var PANEL_WIDTH2 = 208;
2058
2849
  var ITEM_HEIGHT = 34;
2059
2850
  var VIEWPORT_MARGIN = 8;
2060
2851
  var MENU_VPAD = 12;
2061
2852
  function NodeContextMenu({ controller, items }) {
2062
2853
  const { state } = controller;
2063
- const [active, setActive] = (0, import_react16.useState)(0);
2064
- const menuRef = (0, import_react16.useRef)(null);
2854
+ const [active, setActive] = (0, import_react20.useState)(0);
2855
+ const menuRef = (0, import_react20.useRef)(null);
2065
2856
  const isOpen = state !== void 0;
2066
- (0, import_react16.useEffect)(() => {
2857
+ (0, import_react20.useEffect)(() => {
2067
2858
  var _a;
2068
2859
  if (isOpen) {
2069
2860
  setActive(0);
2070
2861
  (_a = menuRef.current) == null ? void 0 : _a.focus();
2071
2862
  }
2072
2863
  }, [isOpen]);
2073
- const rows = (0, import_react16.useMemo)(
2864
+ const rows = (0, import_react20.useMemo)(
2074
2865
  () => state !== void 0 ? items(state.nodeId) : [],
2075
2866
  [items, state]
2076
2867
  );
2077
- const enabledIndices = (0, import_react16.useMemo)(
2868
+ const enabledIndices = (0, import_react20.useMemo)(
2078
2869
  () => rows.map((r, i) => r.disabled ? -1 : i).filter((i) => i >= 0),
2079
2870
  [rows]
2080
2871
  );
2081
- const move = (0, import_react16.useCallback)(
2872
+ const move = (0, import_react20.useCallback)(
2082
2873
  (delta) => {
2083
2874
  if (enabledIndices.length === 0) {
2084
2875
  return;
@@ -2092,7 +2883,7 @@ function NodeContextMenu({ controller, items }) {
2092
2883
  },
2093
2884
  [enabledIndices]
2094
2885
  );
2095
- const choose = (0, import_react16.useCallback)(
2886
+ const choose = (0, import_react20.useCallback)(
2096
2887
  (item) => {
2097
2888
  if (item === void 0 || item.disabled) {
2098
2889
  return;
@@ -2102,7 +2893,7 @@ function NodeContextMenu({ controller, items }) {
2102
2893
  },
2103
2894
  [controller]
2104
2895
  );
2105
- const onKeyDown = (0, import_react16.useCallback)(
2896
+ const onKeyDown = (0, import_react20.useCallback)(
2106
2897
  (event) => {
2107
2898
  switch (event.key) {
2108
2899
  case "ArrowDown":
@@ -2131,8 +2922,8 @@ function NodeContextMenu({ controller, items }) {
2131
2922
  return null;
2132
2923
  }
2133
2924
  const style = clampToViewport2(state.screen.x, state.screen.y, rows.length);
2134
- return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(import_jsx_runtime27.Fragment, { children: [
2135
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
2925
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(import_jsx_runtime34.Fragment, { children: [
2926
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2136
2927
  "div",
2137
2928
  {
2138
2929
  className: "hl-ctx-scrim",
@@ -2145,7 +2936,7 @@ function NodeContextMenu({ controller, items }) {
2145
2936
  }
2146
2937
  }
2147
2938
  ),
2148
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
2939
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
2149
2940
  "div",
2150
2941
  {
2151
2942
  ref: menuRef,
@@ -2155,7 +2946,7 @@ function NodeContextMenu({ controller, items }) {
2155
2946
  "aria-label": "Node actions",
2156
2947
  tabIndex: -1,
2157
2948
  onKeyDown,
2158
- children: rows.map((item, index) => /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
2949
+ children: rows.map((item, index) => /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(
2159
2950
  "button",
2160
2951
  {
2161
2952
  type: "button",
@@ -2168,9 +2959,9 @@ function NodeContextMenu({ controller, items }) {
2168
2959
  choose(item);
2169
2960
  },
2170
2961
  children: [
2171
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "hl-ctx-item-icon", "aria-hidden": true, children: item.icon }),
2172
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "hl-ctx-item-label", children: item.label }),
2173
- item.shortcut !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "hl-ctx-item-key hl-mono", children: item.shortcut })
2962
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { className: "hl-ctx-item-icon", "aria-hidden": true, children: item.icon }),
2963
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { className: "hl-ctx-item-label", children: item.label }),
2964
+ item.shortcut !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { className: "hl-ctx-item-key hl-mono", children: item.shortcut })
2174
2965
  ]
2175
2966
  },
2176
2967
  item.id
@@ -2190,21 +2981,21 @@ function clampToViewport2(x, y, rowCount) {
2190
2981
  }
2191
2982
 
2192
2983
  // src/context-menu/useNodeContextMenu.ts
2193
- var import_react17 = require("react");
2984
+ var import_react21 = require("react");
2194
2985
  function useNodeContextMenu() {
2195
- const [state, setState] = (0, import_react17.useState)(void 0);
2196
- const openForNode = (0, import_react17.useCallback)((screen, nodeId) => {
2986
+ const [state, setState] = (0, import_react21.useState)(void 0);
2987
+ const openForNode = (0, import_react21.useCallback)((screen, nodeId) => {
2197
2988
  setState({ screen, nodeId });
2198
2989
  }, []);
2199
- const close = (0, import_react17.useCallback)(() => {
2990
+ const close = (0, import_react21.useCallback)(() => {
2200
2991
  setState(void 0);
2201
2992
  }, []);
2202
- return (0, import_react17.useMemo)(() => ({ state, openForNode, close }), [state, openForNode, close]);
2993
+ return (0, import_react21.useMemo)(() => ({ state, openForNode, close }), [state, openForNode, close]);
2203
2994
  }
2204
2995
 
2205
2996
  // src/overlays/Cheatsheet.tsx
2206
- var import_react18 = require("react");
2207
- var import_jsx_runtime28 = require("react/jsx-runtime");
2997
+ var import_react22 = require("react");
2998
+ var import_jsx_runtime35 = require("react/jsx-runtime");
2208
2999
  var DEFAULT_GROUPS = [
2209
3000
  {
2210
3001
  title: "Edit",
@@ -2240,8 +3031,8 @@ var DEFAULT_GROUPS = [
2240
3031
  }
2241
3032
  ];
2242
3033
  function Cheatsheet({ open, onClose, groups = DEFAULT_GROUPS }) {
2243
- const dialogRef = (0, import_react18.useRef)(null);
2244
- (0, import_react18.useEffect)(() => {
3034
+ const dialogRef = (0, import_react22.useRef)(null);
3035
+ (0, import_react22.useEffect)(() => {
2245
3036
  var _a;
2246
3037
  if (!open) {
2247
3038
  return;
@@ -2258,9 +3049,9 @@ function Cheatsheet({ open, onClose, groups = DEFAULT_GROUPS }) {
2258
3049
  if (!open) {
2259
3050
  return null;
2260
3051
  }
2261
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_jsx_runtime28.Fragment, { children: [
2262
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { className: "hl-cheat-scrim", onPointerDown: onClose }),
2263
- /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(
3052
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(import_jsx_runtime35.Fragment, { children: [
3053
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "hl-cheat-scrim", onPointerDown: onClose }),
3054
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
2264
3055
  "div",
2265
3056
  {
2266
3057
  ref: dialogRef,
@@ -2270,18 +3061,18 @@ function Cheatsheet({ open, onClose, groups = DEFAULT_GROUPS }) {
2270
3061
  "aria-label": "Keyboard shortcuts",
2271
3062
  tabIndex: -1,
2272
3063
  children: [
2273
- /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("div", { className: "hl-cheat-head", children: [
2274
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: "hl-cheat-title", children: "Shortcuts" }),
2275
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("button", { className: "hl-cheat-close", onClick: onClose, "aria-label": "Close", children: "\u2715" })
3064
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "hl-cheat-head", children: [
3065
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("span", { className: "hl-cheat-title", children: "Shortcuts" }),
3066
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("button", { className: "hl-cheat-close", onClick: onClose, "aria-label": "Close", children: "\u2715" })
2276
3067
  ] }),
2277
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { className: "hl-cheat-grid", children: groups.map((group) => /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("section", { className: "hl-cheat-group", children: [
2278
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("h3", { className: "hl-cheat-group-title hl-mono", children: group.title }),
2279
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("ul", { className: "hl-cheat-rows", children: group.rows.map((row) => (
3068
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "hl-cheat-grid", children: groups.map((group) => /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("section", { className: "hl-cheat-group", children: [
3069
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("h3", { className: "hl-cheat-group-title hl-mono", children: group.title }),
3070
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("ul", { className: "hl-cheat-rows", children: group.rows.map((row) => (
2280
3071
  // A row is a binding: one action can have several (two ways to pan),
2281
3072
  // so neither the label nor the keys alone identify it.
2282
- /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("li", { className: "hl-cheat-row", children: [
2283
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: "hl-cheat-label", children: row.label }),
2284
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("kbd", { className: "hl-cheat-keys hl-mono", children: row.keys })
3073
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("li", { className: "hl-cheat-row", children: [
3074
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("span", { className: "hl-cheat-label", children: row.label }),
3075
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("kbd", { className: "hl-cheat-keys hl-mono", children: row.keys })
2285
3076
  ] }, `${row.keys}\0${row.label}`)
2286
3077
  )) })
2287
3078
  ] }, group.title)) })
@@ -2292,13 +3083,13 @@ function Cheatsheet({ open, onClose, groups = DEFAULT_GROUPS }) {
2292
3083
  }
2293
3084
 
2294
3085
  // src/overlays/Toast.tsx
2295
- var import_react19 = require("react");
2296
- var import_jsx_runtime29 = require("react/jsx-runtime");
2297
- var ToastCtx = (0, import_react19.createContext)({ show: () => void 0 });
3086
+ var import_react23 = require("react");
3087
+ var import_jsx_runtime36 = require("react/jsx-runtime");
3088
+ var ToastCtx = (0, import_react23.createContext)({ show: () => void 0 });
2298
3089
  function ToastProvider({ children, duration = 2200 }) {
2299
- const [items, setItems] = (0, import_react19.useState)([]);
2300
- const seq = (0, import_react19.useRef)(0);
2301
- const show = (0, import_react19.useCallback)(
3090
+ const [items, setItems] = (0, import_react23.useState)([]);
3091
+ const seq = (0, import_react23.useRef)(0);
3092
+ const show = (0, import_react23.useCallback)(
2302
3093
  (message) => {
2303
3094
  seq.current += 1;
2304
3095
  const id = seq.current;
@@ -2309,22 +3100,22 @@ function ToastProvider({ children, duration = 2200 }) {
2309
3100
  },
2310
3101
  [duration]
2311
3102
  );
2312
- const api = (0, import_react19.useMemo)(() => ({ show }), [show]);
2313
- return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(ToastCtx.Provider, { value: api, children: [
3103
+ const api = (0, import_react23.useMemo)(() => ({ show }), [show]);
3104
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(ToastCtx.Provider, { value: api, children: [
2314
3105
  children,
2315
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "hl-toast-stack", "aria-live": "polite", children: items.map((t) => /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "hl-toast", children: t.message }, t.id)) })
3106
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("div", { className: "hl-toast-stack", "aria-live": "polite", children: items.map((t) => /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("div", { className: "hl-toast", children: t.message }, t.id)) })
2316
3107
  ] });
2317
3108
  }
2318
3109
  function useToast() {
2319
- return (0, import_react19.useContext)(ToastCtx);
3110
+ return (0, import_react23.useContext)(ToastCtx);
2320
3111
  }
2321
3112
 
2322
3113
  // src/hooks/useLiveSignals.ts
2323
- var import_react20 = require("react");
3114
+ var import_react24 = require("react");
2324
3115
  function useLiveSignals(core, handle, explicitStore) {
2325
3116
  const contextStore = useLiveSignalStore();
2326
3117
  const store = explicitStore != null ? explicitStore : contextStore;
2327
- (0, import_react20.useEffect)(() => {
3118
+ (0, import_react24.useEffect)(() => {
2328
3119
  if (store === null) {
2329
3120
  return;
2330
3121
  }
@@ -2333,6 +3124,8 @@ function useLiveSignals(core, handle, explicitStore) {
2333
3124
  id: edge.id,
2334
3125
  sourceNodeId: edge.source.nodeId,
2335
3126
  sourcePortId: edge.source.portId,
3127
+ targetNodeId: edge.target.nodeId,
3128
+ targetPortId: edge.target.portId,
2336
3129
  isError: edge.source.portId === "@node/error"
2337
3130
  }));
2338
3131
  store.setEdgeIndex(entries);
@@ -2345,7 +3138,8 @@ function useLiveSignals(core, handle, explicitStore) {
2345
3138
  removed.dispose();
2346
3139
  };
2347
3140
  }, [core, store]);
2348
- (0, import_react20.useEffect)(() => {
3141
+ (0, import_react24.useEffect)(() => {
3142
+ var _a, _b, _c;
2349
3143
  if (store === null) {
2350
3144
  return;
2351
3145
  }
@@ -2353,15 +3147,406 @@ function useLiveSignals(core, handle, explicitStore) {
2353
3147
  store.reset();
2354
3148
  return;
2355
3149
  }
2356
- const activitySub = handle.onActivity((e) => store.markActivity(e.nodeId, e.portId));
3150
+ const signalSub = (_b = (_a = handle.onDelivery) == null ? void 0 : _a.call(handle, (event) => {
3151
+ store.markDelivery(event);
3152
+ })) != null ? _b : handle.onActivity((e) => store.markActivity(e.nodeId, e.portId));
2357
3153
  const errorSub = handle.onError((e) => store.markError(e.nodeId));
3154
+ const finishedSub = (_c = handle.onFinished) == null ? void 0 : _c.call(handle, (e) => {
3155
+ store.markFinished(e.nodeId);
3156
+ });
2358
3157
  return () => {
2359
- activitySub.dispose();
3158
+ signalSub.dispose();
2360
3159
  errorSub.dispose();
3160
+ finishedSub == null ? void 0 : finishedSub.dispose();
2361
3161
  store.reset();
2362
3162
  };
2363
3163
  }, [store, handle]);
2364
3164
  }
3165
+
3166
+ // src/live/run-trace.ts
3167
+ var RUN_LOG_CAPACITY = 500;
3168
+ var RunTrace = class {
3169
+ constructor(options = {}) {
3170
+ /** The log as a ring: `ring[(head + i) % capacity]` is the i-th oldest entry. */
3171
+ this.ring = [];
3172
+ this.head = 0;
3173
+ this.droppedCount = 0;
3174
+ this.seq = 0;
3175
+ this.startedAt = 0;
3176
+ this.stats = /* @__PURE__ */ new Map();
3177
+ this.listeners = /* @__PURE__ */ new Set();
3178
+ /** Bumped by every change; the entries cache follows it. */
3179
+ this.revision = 0;
3180
+ /** Bumped once per notified frame; what subscribers compare. */
3181
+ this.version = 0;
3182
+ this.frame = null;
3183
+ /** The entries as an array, rebuilt only after a change. */
3184
+ this.entriesCache = [];
3185
+ this.entriesRevision = -1;
3186
+ var _a, _b;
3187
+ const capacity = (_a = options.capacity) != null ? _a : RUN_LOG_CAPACITY;
3188
+ if (!Number.isInteger(capacity) || capacity < 1) {
3189
+ throw new RangeError("RunTrace capacity must be a positive integer.");
3190
+ }
3191
+ this.capacity = capacity;
3192
+ this.deps = (_b = options.deps) != null ? _b : defaultLiveDeps;
3193
+ }
3194
+ /** Begins a run: clears everything and times entries from now. */
3195
+ start() {
3196
+ this.clear();
3197
+ this.startedAt = this.deps.now();
3198
+ }
3199
+ /** Forgets the log and the statistics. */
3200
+ clear() {
3201
+ this.ring.length = 0;
3202
+ this.head = 0;
3203
+ this.droppedCount = 0;
3204
+ this.stats.clear();
3205
+ this.changed();
3206
+ }
3207
+ recordDelivery(event) {
3208
+ this.append({
3209
+ kind: "delivery",
3210
+ seq: this.nextSeq(),
3211
+ at: this.elapsed(),
3212
+ sourceNodeId: event.sourceNodeId,
3213
+ sourcePortId: event.sourcePortId,
3214
+ targetNodeId: event.targetNodeId,
3215
+ targetPortId: event.targetPortId,
3216
+ message: event.message,
3217
+ duration: event.duration
3218
+ });
3219
+ const source = this.statsFor(event.sourceNodeId);
3220
+ source.sent += 1;
3221
+ source.lastSent = event.message;
3222
+ const target = this.statsFor(event.targetNodeId);
3223
+ target.received += 1;
3224
+ target.totalMs += event.duration;
3225
+ target.maxMs = Math.max(target.maxMs, event.duration);
3226
+ target.lastReceived = event.message;
3227
+ this.changed();
3228
+ }
3229
+ recordError(event) {
3230
+ this.append({ kind: "error", seq: this.nextSeq(), at: this.elapsed(), nodeId: event.nodeId, error: event.error });
3231
+ this.statsFor(event.nodeId).errors += 1;
3232
+ this.changed();
3233
+ }
3234
+ recordEnded(event) {
3235
+ this.append({ kind: "ended", seq: this.nextSeq(), at: this.elapsed(), nodeId: event.nodeId, portId: event.portId });
3236
+ this.statsFor(event.nodeId);
3237
+ this.changed();
3238
+ }
3239
+ recordFinished(event) {
3240
+ this.append({ kind: "finished", seq: this.nextSeq(), at: this.elapsed(), nodeId: event.nodeId });
3241
+ this.statsFor(event.nodeId).finished = true;
3242
+ this.changed();
3243
+ }
3244
+ recordDropped(event) {
3245
+ this.append({
3246
+ kind: "dropped",
3247
+ seq: this.nextSeq(),
3248
+ at: this.elapsed(),
3249
+ sourceNodeId: event.sourceNodeId,
3250
+ sourcePortId: event.sourcePortId,
3251
+ targetNodeId: event.targetNodeId,
3252
+ targetPortId: event.targetPortId,
3253
+ message: event.message
3254
+ });
3255
+ this.statsFor(event.sourceNodeId);
3256
+ this.statsFor(event.targetNodeId).dropped += 1;
3257
+ this.changed();
3258
+ }
3259
+ /** The log, oldest first. The same array is returned until something changes. */
3260
+ entries() {
3261
+ if (this.entriesRevision !== this.revision) {
3262
+ const count = this.ring.length;
3263
+ const ordered = [];
3264
+ for (let i = 0; i < count; i += 1) {
3265
+ const entry = this.ring[(this.head + i) % count];
3266
+ if (entry !== void 0) {
3267
+ ordered.push(entry);
3268
+ }
3269
+ }
3270
+ this.entriesCache = ordered;
3271
+ this.entriesRevision = this.revision;
3272
+ }
3273
+ return this.entriesCache;
3274
+ }
3275
+ /** A node's statistics, or `undefined` when it has done nothing this run. */
3276
+ statsOf(nodeId) {
3277
+ const stats = this.stats.get(nodeId);
3278
+ return stats === void 0 ? void 0 : { ...stats };
3279
+ }
3280
+ /** How many entries the log has let go of to stay within its capacity. */
3281
+ get dropped() {
3282
+ return this.droppedCount;
3283
+ }
3284
+ /** Changes once per notified frame; a snapshot for `useSyncExternalStore`. */
3285
+ getVersion() {
3286
+ return this.version;
3287
+ }
3288
+ subscribe(listener) {
3289
+ this.listeners.add(listener);
3290
+ return () => {
3291
+ this.listeners.delete(listener);
3292
+ };
3293
+ }
3294
+ append(entry) {
3295
+ if (this.ring.length < this.capacity) {
3296
+ this.ring.push(entry);
3297
+ return;
3298
+ }
3299
+ this.ring[this.head] = entry;
3300
+ this.head = (this.head + 1) % this.capacity;
3301
+ this.droppedCount += 1;
3302
+ }
3303
+ nextSeq() {
3304
+ this.seq += 1;
3305
+ return this.seq;
3306
+ }
3307
+ elapsed() {
3308
+ return this.deps.now() - this.startedAt;
3309
+ }
3310
+ statsFor(nodeId) {
3311
+ let stats = this.stats.get(nodeId);
3312
+ if (stats === void 0) {
3313
+ stats = {
3314
+ received: 0,
3315
+ sent: 0,
3316
+ errors: 0,
3317
+ totalMs: 0,
3318
+ maxMs: 0,
3319
+ lastReceived: void 0,
3320
+ lastSent: void 0,
3321
+ dropped: 0,
3322
+ finished: false
3323
+ };
3324
+ this.stats.set(nodeId, stats);
3325
+ }
3326
+ return stats;
3327
+ }
3328
+ /** Schedules one notification for the next frame, however many changes come before it. */
3329
+ changed() {
3330
+ this.revision += 1;
3331
+ if (this.frame !== null) {
3332
+ return;
3333
+ }
3334
+ this.frame = this.deps.schedule(() => {
3335
+ this.frame = null;
3336
+ this.version += 1;
3337
+ for (const listener of [...this.listeners]) {
3338
+ listener();
3339
+ }
3340
+ });
3341
+ }
3342
+ };
3343
+ function involves(entry, nodeId) {
3344
+ switch (entry.kind) {
3345
+ case "delivery":
3346
+ case "dropped":
3347
+ return entry.sourceNodeId === nodeId || entry.targetNodeId === nodeId;
3348
+ case "error":
3349
+ case "ended":
3350
+ case "finished":
3351
+ return entry.nodeId === nodeId;
3352
+ }
3353
+ }
3354
+
3355
+ // src/hooks/useRunTrace.ts
3356
+ var import_react25 = require("react");
3357
+ function useRunTrace(trace, handle) {
3358
+ (0, import_react25.useEffect)(() => {
3359
+ var _a, _b, _c, _d;
3360
+ if (handle === void 0) {
3361
+ return;
3362
+ }
3363
+ trace.start();
3364
+ const deliveries = (_a = handle.onDelivery) == null ? void 0 : _a.call(handle, (event) => {
3365
+ trace.recordDelivery(event);
3366
+ });
3367
+ const errors = handle.onError((event) => {
3368
+ trace.recordError(event);
3369
+ });
3370
+ const ended = (_b = handle.onEnded) == null ? void 0 : _b.call(handle, (event) => {
3371
+ trace.recordEnded(event);
3372
+ });
3373
+ const finished = (_c = handle.onFinished) == null ? void 0 : _c.call(handle, (event) => {
3374
+ trace.recordFinished(event);
3375
+ });
3376
+ const dropped = (_d = handle.onDropped) == null ? void 0 : _d.call(handle, (event) => {
3377
+ trace.recordDropped(event);
3378
+ });
3379
+ return () => {
3380
+ deliveries == null ? void 0 : deliveries.dispose();
3381
+ errors.dispose();
3382
+ ended == null ? void 0 : ended.dispose();
3383
+ finished == null ? void 0 : finished.dispose();
3384
+ dropped == null ? void 0 : dropped.dispose();
3385
+ };
3386
+ }, [trace, handle]);
3387
+ }
3388
+
3389
+ // src/hooks/useFollowScroll.ts
3390
+ var import_react26 = require("react");
3391
+ var FOLLOW_SLACK_PX = 24;
3392
+ function useFollowScroll() {
3393
+ const ref = (0, import_react26.useRef)(null);
3394
+ const [follow, setFollow] = (0, import_react26.useState)(true);
3395
+ (0, import_react26.useLayoutEffect)(() => {
3396
+ const element = ref.current;
3397
+ if (follow && element !== null) {
3398
+ element.scrollTop = element.scrollHeight;
3399
+ }
3400
+ });
3401
+ const onScroll = (0, import_react26.useCallback)((event) => {
3402
+ const element = event.currentTarget;
3403
+ setFollow(element.scrollHeight - element.scrollTop - element.clientHeight <= FOLLOW_SLACK_PX);
3404
+ }, []);
3405
+ return { ref, onScroll, follow, setFollow };
3406
+ }
3407
+
3408
+ // src/panels/RunLog.tsx
3409
+ var import_react27 = require("react");
3410
+ var import_editor_react7 = require("@hlang-org/editor-react");
3411
+ var import_jsx_runtime37 = require("react/jsx-runtime");
3412
+ function RunLog({ core, onSelectNode, title = "Run log", leading }) {
3413
+ const trace = useRunTraceStore();
3414
+ useRunTraceVersion(trace);
3415
+ const selection = (0, import_editor_react7.useSelection)(core);
3416
+ const [onlySelected, setOnlySelected] = (0, import_react27.useState)(false);
3417
+ const { ref: bodyRef, onScroll, follow, setFollow } = useFollowScroll();
3418
+ const all = trace === null ? [] : trace.entries();
3419
+ const selectedId = selection.selectedNodes.length === 1 ? selection.selectedNodes[0] : void 0;
3420
+ const filterId = onlySelected ? selectedId : void 0;
3421
+ const entries = filterId === void 0 ? all : all.filter((entry) => involves(entry, filterId));
3422
+ if (trace === null) {
3423
+ return null;
3424
+ }
3425
+ const label = (nodeId) => {
3426
+ var _a, _b;
3427
+ return (_b = (_a = core.model.getNode(nodeId)) == null ? void 0 : _a.name) != null ? _b : nodeId;
3428
+ };
3429
+ const choose = (nodeId) => {
3430
+ core.selection.selectNodes([nodeId], "replace");
3431
+ onSelectNode == null ? void 0 : onSelectNode(nodeId);
3432
+ };
3433
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("section", { className: "hl-runlog", "aria-label": title, children: [
3434
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
3435
+ PanelHead,
3436
+ {
3437
+ leading: leading != null ? leading : /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "hl-panel-title", children: title }),
3438
+ meta: (entries.length === all.length ? `${String(all.length)} events` : `${String(entries.length)} of ${String(all.length)} events`) + (trace.dropped > 0 ? ` \xB7 ${String(trace.dropped)} earlier not kept` : ""),
3439
+ children: [
3440
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
3441
+ PanelAction,
3442
+ {
3443
+ icon: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(FilterIcon, {}),
3444
+ label: onlySelected && selectedId !== void 0 ? `Only ${label(selectedId)}` : "Selected node",
3445
+ title: selectedId === void 0 ? "Select a node to filter by it" : `Only ${label(selectedId)}`,
3446
+ pressed: onlySelected,
3447
+ disabled: selectedId === void 0 && !onlySelected,
3448
+ onClick: () => {
3449
+ setOnlySelected((value) => !value);
3450
+ }
3451
+ }
3452
+ ),
3453
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
3454
+ PanelAction,
3455
+ {
3456
+ icon: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(FollowIcon, {}),
3457
+ label: "Follow",
3458
+ title: "Follow the newest event",
3459
+ pressed: follow,
3460
+ onClick: () => {
3461
+ setFollow(!follow);
3462
+ }
3463
+ }
3464
+ ),
3465
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
3466
+ PanelAction,
3467
+ {
3468
+ icon: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(ClearIcon, {}),
3469
+ label: "Clear",
3470
+ title: "Clear the log",
3471
+ disabled: all.length === 0,
3472
+ onClick: () => {
3473
+ trace.clear();
3474
+ }
3475
+ }
3476
+ )
3477
+ ]
3478
+ }
3479
+ ),
3480
+ entries.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("p", { className: "hl-runlog-empty", children: all.length === 0 ? "Run the graph to see every message as it moves through it." : "Nothing involving the selected node yet." }) : /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("ol", { className: "hl-runlog-body hl-mono", ref: bodyRef, onScroll, children: entries.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(Row, { entry, label, onChoose: choose }, entry.seq)) })
3481
+ ] });
3482
+ }
3483
+ function Row({
3484
+ entry,
3485
+ label,
3486
+ onChoose
3487
+ }) {
3488
+ switch (entry.kind) {
3489
+ case "error":
3490
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(Event, { className: "is-error", at: entry.at, onChoose: () => onChoose(entry.nodeId), ms: "", children: [
3491
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(NodePort, { node: label(entry.nodeId), port: "error" }),
3492
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "hl-runlog-value", children: previewValue(entry.error) })
3493
+ ] });
3494
+ case "ended":
3495
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(Event, { className: "is-end", at: entry.at, onChoose: () => onChoose(entry.nodeId), ms: "", children: [
3496
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(NodePort, { node: label(entry.nodeId), port: entry.portId }),
3497
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "hl-runlog-value", children: "ended" })
3498
+ ] });
3499
+ case "finished":
3500
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(Event, { className: "is-end", at: entry.at, onChoose: () => onChoose(entry.nodeId), ms: "", children: [
3501
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(NodePort, { node: label(entry.nodeId), port: "" }),
3502
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "hl-runlog-value", children: "finished" })
3503
+ ] });
3504
+ case "dropped":
3505
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(Event, { className: "is-dropped", at: entry.at, onChoose: () => onChoose(entry.targetNodeId), ms: "", children: [
3506
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(Route, { entry, label }),
3507
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("span", { className: "hl-runlog-value", children: [
3508
+ "dropped ",
3509
+ previewValue(entry.message)
3510
+ ] })
3511
+ ] });
3512
+ case "delivery":
3513
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(Event, { className: "", at: entry.at, onChoose: () => onChoose(entry.targetNodeId), ms: formatDuration(entry.duration), children: [
3514
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(Route, { entry, label }),
3515
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "hl-runlog-value", children: previewValue(entry.message) })
3516
+ ] });
3517
+ }
3518
+ }
3519
+ function Event({
3520
+ className: className2,
3521
+ at,
3522
+ ms,
3523
+ onChoose,
3524
+ children
3525
+ }) {
3526
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("li", { className: `hl-runlog-row ${className2}`.trim(), children: /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("button", { type: "button", className: "hl-runlog-hit", onClick: onChoose, children: [
3527
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "hl-runlog-at", children: formatElapsed(at) }),
3528
+ children,
3529
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "hl-runlog-ms", children: ms })
3530
+ ] }) });
3531
+ }
3532
+ function NodePort({ node, port }) {
3533
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("span", { className: "hl-runlog-route", children: [
3534
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "hl-runlog-node", children: node }),
3535
+ port === "" ? null : /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "hl-runlog-port", children: port })
3536
+ ] });
3537
+ }
3538
+ function Route({
3539
+ entry,
3540
+ label
3541
+ }) {
3542
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("span", { className: "hl-runlog-route", children: [
3543
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "hl-runlog-node", children: label(entry.sourceNodeId) }),
3544
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "hl-runlog-port", children: entry.sourcePortId }),
3545
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "hl-runlog-arrow", "aria-label": "to", children: "\u2192" }),
3546
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "hl-runlog-node", children: label(entry.targetNodeId) }),
3547
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "hl-runlog-port", children: entry.targetPortId })
3548
+ ] });
3549
+ }
2365
3550
  // Annotate the CommonJS export names for ESM import in node:
2366
3551
  0 && (module.exports = {
2367
3552
  AssetGrid,
@@ -2369,30 +3554,51 @@ function useLiveSignals(core, handle, explicitStore) {
2369
3554
  CanvasChrome,
2370
3555
  CanvasDock,
2371
3556
  Cheatsheet,
3557
+ ClearIcon,
3558
+ ConnectionInspector,
3559
+ DEFAULT_EDGE_CAPACITY,
3560
+ DEFAULT_MIN_CANVAS_HEIGHT,
2372
3561
  DiagnosticBadge,
2373
3562
  EditorShell,
2374
3563
  EditorUIThemeProvider,
2375
3564
  EmptyState,
3565
+ FOLLOW_SLACK_PX,
3566
+ FOOTER_KEY_STEP,
3567
+ FilterIcon,
2376
3568
  FlowEdge,
3569
+ FollowIcon,
3570
+ FooterResizer,
2377
3571
  HlangNode,
2378
3572
  InspectorPanel,
2379
3573
  KIND_ROLE,
2380
3574
  KIND_TOKEN,
3575
+ LivePulse,
2381
3576
  LiveSignalProvider,
2382
3577
  LiveSignalStore,
2383
3578
  NodeBodySlot,
2384
3579
  NodeChrome,
2385
3580
  NodeContextMenu,
2386
3581
  NodeInteractionProvider,
3582
+ NodeRunStats,
2387
3583
  NodeSearch,
2388
3584
  OutlineTree,
3585
+ PREVIEW_LENGTH,
3586
+ PanelAction,
3587
+ PanelHead,
2389
3588
  ParamForm,
2390
3589
  PillNodeChrome,
2391
3590
  PortRow,
2392
3591
  PositionPanel,
2393
3592
  PreviewPanel,
3593
+ RUN_LOG_CAPACITY,
2394
3594
  RailTabs,
3595
+ ReadOnlyBadge,
2395
3596
  RunButton,
3597
+ RunLog,
3598
+ RunTrace,
3599
+ RunTraceProvider,
3600
+ SELF_LOOP_CLEARANCE,
3601
+ SELF_LOOP_RADIUS,
2396
3602
  SignalBar,
2397
3603
  TOKEN_NAMES,
2398
3604
  ToastProvider,
@@ -2401,26 +3607,47 @@ function useLiveSignals(core, handle, explicitStore) {
2401
3607
  ToolbarGhostButton,
2402
3608
  ToolbarStat,
2403
3609
  UndoRedo,
3610
+ boundsFields,
3611
+ capacityLabel,
3612
+ clampFooter,
2404
3613
  connectableCandidates,
2405
3614
  createHlangFlowTypes,
2406
3615
  createTheme,
3616
+ defaultLiveDeps,
2407
3617
  deriveParamSchema,
2408
3618
  descriptorCandidates,
3619
+ footerBounds,
3620
+ formatDuration,
3621
+ formatElapsed,
2409
3622
  fuzzyRank,
2410
3623
  fuzzyScore,
3624
+ involves,
2411
3625
  isEditableTarget,
3626
+ nextBounds,
3627
+ previewValue,
3628
+ resizeByKey,
2412
3629
  searchNodes,
3630
+ selfLoopPath,
3631
+ toggleCollapsed,
2413
3632
  tokenVar,
3633
+ useEdgeBounds,
2414
3634
  useEdgeFlowing,
3635
+ useEdgePulse,
2415
3636
  useEditorHotkeys,
3637
+ useFollowScroll,
3638
+ useFooterResize,
2416
3639
  useLiveSignalStore,
2417
3640
  useLiveSignals,
2418
3641
  useNodeContextMenu,
2419
3642
  useNodeInteraction,
2420
3643
  useNodeLiveState,
2421
3644
  useNodeParams,
3645
+ useNodePulse,
2422
3646
  useNodeSearch,
2423
3647
  useNodeSearchTriggers,
3648
+ useRunTrace,
3649
+ useRunTraceStore,
3650
+ useRunTraceVersion,
2424
3651
  useToast
2425
3652
  });
2426
3653
  //# sourceMappingURL=index.js.map