@hlang-tech/editor-ui 0.2.2 → 0.4.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.mjs CHANGED
@@ -9,11 +9,18 @@ var TOKEN_NAMES = [
9
9
  "hl-silver",
10
10
  "hl-silver-hi",
11
11
  "hl-silver-lo",
12
+ "hl-glass",
13
+ "hl-glass-2",
14
+ "hl-glass-blur",
15
+ "hl-glass-sat",
16
+ "hl-glass-edge",
17
+ "hl-glass-edge-hi",
12
18
  "hl-spark",
13
19
  "hl-r",
14
20
  "hl-t",
15
21
  "hl-w",
16
22
  "hl-error",
23
+ "hl-warning",
17
24
  "hl-ink",
18
25
  "hl-ink-2",
19
26
  "hl-ink-3",
@@ -26,7 +33,9 @@ var TOKEN_NAMES = [
26
33
  "hl-shadow-node",
27
34
  "hl-shadow-glass",
28
35
  "hl-glow-silver",
29
- "hl-ease"
36
+ "hl-ease",
37
+ "hl-activity",
38
+ "hl-cost"
30
39
  ];
31
40
  function tokenVar(name) {
32
41
  return `var(--${name})`;
@@ -57,12 +66,12 @@ function createTheme(overrides) {
57
66
  import { jsx } from "react/jsx-runtime";
58
67
  function EditorUIThemeProvider({
59
68
  theme,
60
- className,
69
+ className: className2,
61
70
  style,
62
71
  children
63
72
  }) {
64
73
  const themeStyle = theme ? createTheme(theme) : void 0;
65
- return /* @__PURE__ */ jsx("div", { className, style: { ...themeStyle, ...style }, children });
74
+ return /* @__PURE__ */ jsx("div", { className: className2, style: { ...themeStyle, ...style }, children });
66
75
  }
67
76
 
68
77
  // src/nodes/SignalBar.tsx
@@ -112,7 +121,10 @@ var ACTIVE_WINDOW_MS = 700;
112
121
  function portKey(nodeId, portId) {
113
122
  return `${nodeId}@${portId}`;
114
123
  }
115
- var defaultDeps = {
124
+ function connectionKey(sourceNodeId, sourcePortId, targetNodeId, targetPortId) {
125
+ return `${portKey(sourceNodeId, sourcePortId)}->${portKey(targetNodeId, targetPortId)}`;
126
+ }
127
+ var defaultLiveDeps = {
116
128
  now: () => typeof performance !== "undefined" ? performance.now() : Date.now(),
117
129
  schedule: (cb) => typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame(cb) : (
118
130
  // `window.setTimeout` returns a numeric handle (the DOM overload),
@@ -123,11 +135,13 @@ var defaultDeps = {
123
135
  cancel: (h) => typeof cancelAnimationFrame !== "undefined" ? cancelAnimationFrame(h) : window.clearTimeout(h)
124
136
  };
125
137
  var LiveSignalStore = class {
126
- constructor(deps = defaultDeps) {
138
+ constructor(deps = defaultLiveDeps) {
127
139
  /** nodeId -> last-active timestamp. */
128
140
  this.nodeActiveAt = /* @__PURE__ */ new Map();
129
141
  /** nodeId set that errored during this run. */
130
142
  this.erroredNodes = /* @__PURE__ */ new Set();
143
+ /** nodeId set that finished during this run (RFC 0001). */
144
+ this.finishedNodes = /* @__PURE__ */ new Set();
131
145
  /** edgeId -> last-flowing timestamp. */
132
146
  this.edgeFlowAt = /* @__PURE__ */ new Map();
133
147
  /** Per-id listeners. */
@@ -138,12 +152,21 @@ var LiveSignalStore = class {
138
152
  this.lastEdgeFlowing = /* @__PURE__ */ new Map();
139
153
  /** Edges sourced by a given (nodeId@portId), for pulse -> edge mapping. */
140
154
  this.edgesBySourcePort = /* @__PURE__ */ new Map();
155
+ /** Edges by the connection they carry (source port -> target port). */
156
+ this.edgesByConnection = /* @__PURE__ */ new Map();
157
+ /** How many messages each node has handled or sent, and each edge carried, this run. */
158
+ this.nodePulse = /* @__PURE__ */ new Map();
159
+ this.edgePulse = /* @__PURE__ */ new Map();
160
+ /** The pulse counts last notified, so a new message notifies even while the state stays active. */
161
+ this.lastNodePulse = /* @__PURE__ */ new Map();
162
+ this.lastEdgePulse = /* @__PURE__ */ new Map();
141
163
  this.rafHandle = null;
142
164
  this.running = false;
143
165
  this.deps = deps;
144
166
  }
145
167
  /** Provide the current edge index (call whenever edges change). */
146
168
  setEdgeIndex(edges) {
169
+ var _a;
147
170
  const next = /* @__PURE__ */ new Map();
148
171
  for (const edge of edges) {
149
172
  const key = portKey(edge.sourceNodeId, edge.sourcePortId);
@@ -155,6 +178,44 @@ var LiveSignalStore = class {
155
178
  }
156
179
  }
157
180
  this.edgesBySourcePort = next;
181
+ const byConnection = /* @__PURE__ */ new Map();
182
+ for (const edge of edges) {
183
+ if (edge.targetNodeId !== void 0 && edge.targetPortId !== void 0) {
184
+ const key = connectionKey(edge.sourceNodeId, edge.sourcePortId, edge.targetNodeId, edge.targetPortId);
185
+ byConnection.set(key, [...(_a = byConnection.get(key)) != null ? _a : [], edge]);
186
+ }
187
+ }
188
+ this.edgesByConnection = byConnection;
189
+ }
190
+ /**
191
+ * Record a message delivered on a connection, when it happens: its source
192
+ * and its target each pulse once (each ran), and so does the edge that
193
+ * carried it, and only that edge. Subscribers are notified on the next
194
+ * frame, once however many messages arrived in it.
195
+ */
196
+ markDelivery(signal) {
197
+ var _a;
198
+ const t = this.deps.now();
199
+ for (const nodeId of [signal.sourceNodeId, signal.targetNodeId]) {
200
+ this.nodeActiveAt.set(nodeId, t);
201
+ this.nodePulse.set(nodeId, this.getNodePulse(nodeId) + 1);
202
+ }
203
+ const key = connectionKey(signal.sourceNodeId, signal.sourcePortId, signal.targetNodeId, signal.targetPortId);
204
+ for (const edge of (_a = this.edgesByConnection.get(key)) != null ? _a : []) {
205
+ this.edgeFlowAt.set(edge.id, t);
206
+ this.edgePulse.set(edge.id, this.getEdgePulse(edge.id) + 1);
207
+ }
208
+ this.ensureLoop();
209
+ }
210
+ /** How many messages a node has handled or sent this run; it changes each time the node runs. */
211
+ getNodePulse(nodeId) {
212
+ var _a;
213
+ return (_a = this.nodePulse.get(nodeId)) != null ? _a : 0;
214
+ }
215
+ /** How many messages an edge has carried this run. */
216
+ getEdgePulse(edgeId) {
217
+ var _a;
218
+ return (_a = this.edgePulse.get(edgeId)) != null ? _a : 0;
158
219
  }
159
220
  /** Record a node emitting on an output port (drives node + edge activity). */
160
221
  markActivity(nodeId, portId) {
@@ -168,6 +229,11 @@ var LiveSignalStore = class {
168
229
  }
169
230
  this.ensureLoop();
170
231
  }
232
+ /** Record a node finishing (RFC 0001): shown once its last flash has faded. */
233
+ markFinished(nodeId) {
234
+ this.finishedNodes.add(nodeId);
235
+ this.ensureLoop();
236
+ }
171
237
  /** Record a node erroring (sticky for the run). */
172
238
  markError(nodeId) {
173
239
  this.erroredNodes.add(nodeId);
@@ -177,7 +243,10 @@ var LiveSignalStore = class {
177
243
  reset() {
178
244
  this.nodeActiveAt.clear();
179
245
  this.erroredNodes.clear();
246
+ this.finishedNodes.clear();
180
247
  this.edgeFlowAt.clear();
248
+ this.nodePulse.clear();
249
+ this.edgePulse.clear();
181
250
  this.stopLoop();
182
251
  this.flushNotifications();
183
252
  }
@@ -198,7 +267,7 @@ var LiveSignalStore = class {
198
267
  if (at !== void 0 && this.deps.now() - at <= ACTIVE_WINDOW_MS) {
199
268
  return "active";
200
269
  }
201
- return "idle";
270
+ return this.finishedNodes.has(nodeId) ? "finished" : "idle";
202
271
  }
203
272
  /** Whether an edge is currently flowing. */
204
273
  isEdgeFlowing(edgeId) {
@@ -217,9 +286,14 @@ var LiveSignalStore = class {
217
286
  }
218
287
  };
219
288
  }
289
+ /**
290
+ * Arms the frame loop if it is not running. Signals only record state; the
291
+ * next frame notifies, so a burst of signals costs one pass over the
292
+ * subscribers per frame, not one per signal, and every change shows within a
293
+ * frame.
294
+ */
220
295
  ensureLoop() {
221
296
  if (this.running) {
222
- this.flushNotifications();
223
297
  return;
224
298
  }
225
299
  this.running = true;
@@ -253,8 +327,10 @@ var LiveSignalStore = class {
253
327
  if (state !== "idle") {
254
328
  anyLive = anyLive || state === "active";
255
329
  }
256
- if (this.lastNodeState.get(nodeId) !== state) {
330
+ const pulse = this.getNodePulse(nodeId);
331
+ if (this.lastNodeState.get(nodeId) !== state || this.lastNodePulse.get(nodeId) !== pulse) {
257
332
  this.lastNodeState.set(nodeId, state);
333
+ this.lastNodePulse.set(nodeId, pulse);
258
334
  for (const listener of listeners) {
259
335
  listener();
260
336
  }
@@ -263,8 +339,10 @@ var LiveSignalStore = class {
263
339
  for (const [edgeId, listeners] of this.edgeListeners) {
264
340
  const flowing = this.isEdgeFlowing(edgeId);
265
341
  anyLive = anyLive || flowing;
266
- if (this.lastEdgeFlowing.get(edgeId) !== flowing) {
342
+ const pulse = this.getEdgePulse(edgeId);
343
+ if (this.lastEdgeFlowing.get(edgeId) !== flowing || this.lastEdgePulse.get(edgeId) !== pulse) {
267
344
  this.lastEdgeFlowing.set(edgeId, flowing);
345
+ this.lastEdgePulse.set(edgeId, pulse);
268
346
  for (const listener of listeners) {
269
347
  listener();
270
348
  }
@@ -292,6 +370,22 @@ function useNodeLiveState(nodeId) {
292
370
  () => "idle"
293
371
  );
294
372
  }
373
+ function useNodePulse(nodeId) {
374
+ const store = useContext(LiveSignalContext);
375
+ return useSyncExternalStore(
376
+ (onChange) => store === null ? () => void 0 : store.subscribeNode(nodeId, onChange),
377
+ () => store === null ? 0 : store.getNodePulse(nodeId),
378
+ () => 0
379
+ );
380
+ }
381
+ function useEdgePulse(edgeId) {
382
+ const store = useContext(LiveSignalContext);
383
+ return useSyncExternalStore(
384
+ (onChange) => store === null ? () => void 0 : store.subscribeEdge(edgeId, onChange),
385
+ () => store === null ? 0 : store.getEdgePulse(edgeId),
386
+ () => 0
387
+ );
388
+ }
295
389
  function useEdgeFlowing(edgeId) {
296
390
  const store = useContext(LiveSignalContext);
297
391
  return useSyncExternalStore(
@@ -301,8 +395,15 @@ function useEdgeFlowing(edgeId) {
301
395
  );
302
396
  }
303
397
 
398
+ // src/live/LivePulse.tsx
399
+ import { jsx as jsx6 } from "react/jsx-runtime";
400
+ function LivePulse({ nodeId }) {
401
+ const pulse = useNodePulse(nodeId);
402
+ return pulse === 0 ? null : /* @__PURE__ */ jsx6("span", { className: "hl-live-pulse", "aria-hidden": "true" }, pulse);
403
+ }
404
+
304
405
  // src/nodes/AssetNode.tsx
305
- import { jsx as jsx6, jsxs as jsxs2 } from "react/jsx-runtime";
406
+ import { jsx as jsx7, jsxs as jsxs2 } from "react/jsx-runtime";
306
407
  function stringParam(node, key) {
307
408
  const v = node.params[key];
308
409
  return typeof v === "string" ? v : void 0;
@@ -314,22 +415,24 @@ function AssetNodeChrome({ node, selected }) {
314
415
  const role = stringParam(node, "role");
315
416
  const live = useNodeLiveState(node.id);
316
417
  const outputs = node.ports.filter((p) => p.direction === "O" && !p.isError);
317
- const className = [
418
+ const className2 = [
318
419
  "hl-asset",
319
420
  selected ? "is-selected" : "",
320
421
  live === "active" ? "is-active" : "",
321
- live === "error" ? "is-live-error" : ""
422
+ live === "error" ? "is-live-error" : "",
423
+ live === "finished" ? "is-finished" : ""
322
424
  ].filter(Boolean).join(" ");
323
- return /* @__PURE__ */ jsxs2("div", { className, "data-kind": node.kind, children: [
425
+ return /* @__PURE__ */ jsxs2("div", { className: className2, "data-kind": node.kind, children: [
426
+ /* @__PURE__ */ jsx7(LivePulse, { nodeId: node.id }),
324
427
  /* @__PURE__ */ jsxs2("div", { className: "hl-asset-frame", children: [
325
- imageUrl.length > 0 ? /* @__PURE__ */ jsx6("img", { className: "hl-asset-img", src: imageUrl, alt: caption, draggable: false }) : /* @__PURE__ */ jsx6("div", { className: "hl-asset-placeholder", "aria-hidden": true }),
326
- /* @__PURE__ */ jsx6("div", { className: "hl-asset-scrim" }),
428
+ imageUrl.length > 0 ? /* @__PURE__ */ jsx7("img", { className: "hl-asset-img", src: imageUrl, alt: caption, draggable: false }) : /* @__PURE__ */ jsx7("div", { className: "hl-asset-placeholder", "aria-hidden": true }),
429
+ /* @__PURE__ */ jsx7("div", { className: "hl-asset-scrim" }),
327
430
  /* @__PURE__ */ jsxs2("div", { className: "hl-asset-caption", children: [
328
- /* @__PURE__ */ jsx6("span", { className: "hl-asset-name", children: caption }),
329
- role !== void 0 && /* @__PURE__ */ jsx6("span", { className: "hl-asset-role hl-mono", children: role })
431
+ /* @__PURE__ */ jsx7("span", { className: "hl-asset-name", children: caption }),
432
+ role !== void 0 && /* @__PURE__ */ jsx7("span", { className: "hl-asset-role hl-mono", children: role })
330
433
  ] })
331
434
  ] }),
332
- /* @__PURE__ */ jsx6("ul", { className: "hl-asset-ports", children: outputs.map((port) => /* @__PURE__ */ jsx6(PortRow, { port }, port.id)) })
435
+ /* @__PURE__ */ jsx7("ul", { className: "hl-asset-ports", children: outputs.map((port) => /* @__PURE__ */ jsx7(PortRow, { port }, port.id)) })
333
436
  ] });
334
437
  }
335
438
 
@@ -337,9 +440,9 @@ function AssetNodeChrome({ node, selected }) {
337
440
  import { useCallback, useEffect, useRef, useState } from "react";
338
441
 
339
442
  // src/icons/node-icons.tsx
340
- import { jsx as jsx7 } from "react/jsx-runtime";
443
+ import { jsx as jsx8 } from "react/jsx-runtime";
341
444
  function svg(size, children) {
342
- return /* @__PURE__ */ jsx7(
445
+ return /* @__PURE__ */ jsx8(
343
446
  "svg",
344
447
  {
345
448
  width: size,
@@ -357,49 +460,49 @@ function svg(size, children) {
357
460
  }
358
461
  var BY_CODE = {
359
462
  "image-source": (s) => svg(s, [
360
- /* @__PURE__ */ jsx7("rect", { x: "3", y: "4", width: "18", height: "16", rx: "2.5" }, "r"),
361
- /* @__PURE__ */ jsx7("circle", { cx: "8.5", cy: "9", r: "1.6" }, "c"),
362
- /* @__PURE__ */ jsx7("path", { d: "M4 17l5-5 4 4 3-2 4 4" }, "p")
463
+ /* @__PURE__ */ jsx8("rect", { x: "3", y: "4", width: "18", height: "16", rx: "2.5" }, "r"),
464
+ /* @__PURE__ */ jsx8("circle", { cx: "8.5", cy: "9", r: "1.6" }, "c"),
465
+ /* @__PURE__ */ jsx8("path", { d: "M4 17l5-5 4 4 3-2 4 4" }, "p")
363
466
  ]),
364
- "text-source": (s) => svg(s, [/* @__PURE__ */ jsx7("path", { d: "M5 6h14M12 6v12M9 18h6" }, "p")]),
467
+ "text-source": (s) => svg(s, [/* @__PURE__ */ jsx8("path", { d: "M5 6h14M12 6v12M9 18h6" }, "p")]),
365
468
  "interval-source": (s) => svg(s, [
366
- /* @__PURE__ */ jsx7("circle", { cx: "12", cy: "12", r: "8" }, "c"),
367
- /* @__PURE__ */ jsx7("path", { d: "M12 8v4l3 2" }, "h")
469
+ /* @__PURE__ */ jsx8("circle", { cx: "12", cy: "12", r: "8" }, "c"),
470
+ /* @__PURE__ */ jsx8("path", { d: "M12 8v4l3 2" }, "h")
368
471
  ]),
369
472
  "range-source": (s) => svg(s, [
370
- /* @__PURE__ */ jsx7("path", { d: "M4 12h16" }, "l"),
371
- /* @__PURE__ */ jsx7("circle", { cx: "6", cy: "12", r: "2" }, "a"),
372
- /* @__PURE__ */ jsx7("circle", { cx: "18", cy: "12", r: "2" }, "b")
473
+ /* @__PURE__ */ jsx8("path", { d: "M4 12h16" }, "l"),
474
+ /* @__PURE__ */ jsx8("circle", { cx: "6", cy: "12", r: "2" }, "a"),
475
+ /* @__PURE__ */ jsx8("circle", { cx: "18", cy: "12", r: "2" }, "b")
373
476
  ]),
374
- "constant-source": (s) => svg(s, [/* @__PURE__ */ jsx7("path", { d: "M6 12h12M9 8l-3 4 3 4M15 8l3 4-3 4" }, "p")]),
477
+ "constant-source": (s) => svg(s, [/* @__PURE__ */ jsx8("path", { d: "M6 12h12M9 8l-3 4 3 4M15 8l3 4-3 4" }, "p")]),
375
478
  "map-scale": (s) => svg(s, [
376
- /* @__PURE__ */ jsx7("path", { d: "M4 20V8l8-4" }, "a"),
377
- /* @__PURE__ */ jsx7("path", { d: "M4 8l8 4 8-4M12 12v8" }, "b")
479
+ /* @__PURE__ */ jsx8("path", { d: "M4 20V8l8-4" }, "a"),
480
+ /* @__PURE__ */ jsx8("path", { d: "M4 8l8 4 8-4M12 12v8" }, "b")
378
481
  ]),
379
- uppercase: (s) => svg(s, [/* @__PURE__ */ jsx7("path", { d: "M5 18L9 6l4 12M6.5 14h5M15 10v8M15 10c3 0 3 4 0 4" }, "p")]),
380
- "filter-threshold": (s) => svg(s, [/* @__PURE__ */ jsx7("path", { d: "M4 5h16l-6 7v6l-4-2v-4z" }, "p")]),
381
- "scan-sum": (s) => svg(s, [/* @__PURE__ */ jsx7("path", { d: "M17 6H7l6 6-6 6h10" }, "p")]),
482
+ uppercase: (s) => svg(s, [/* @__PURE__ */ jsx8("path", { d: "M5 18L9 6l4 12M6.5 14h5M15 10v8M15 10c3 0 3 4 0 4" }, "p")]),
483
+ "filter-threshold": (s) => svg(s, [/* @__PURE__ */ jsx8("path", { d: "M4 5h16l-6 7v6l-4-2v-4z" }, "p")]),
484
+ "scan-sum": (s) => svg(s, [/* @__PURE__ */ jsx8("path", { d: "M17 6H7l6 6-6 6h10" }, "p")]),
382
485
  delay: (s) => svg(s, [
383
- /* @__PURE__ */ jsx7("circle", { cx: "12", cy: "13", r: "7" }, "c"),
384
- /* @__PURE__ */ jsx7("path", { d: "M12 10v3l2 2M9 3h6" }, "h")
486
+ /* @__PURE__ */ jsx8("circle", { cx: "12", cy: "13", r: "7" }, "c"),
487
+ /* @__PURE__ */ jsx8("path", { d: "M12 10v3l2 2M9 3h6" }, "h")
385
488
  ]),
386
- merge: (s) => svg(s, [/* @__PURE__ */ jsx7("path", { d: "M6 5v4c0 3 3 3 6 3s6 0 6-3V5M12 12v7" }, "p")]),
489
+ merge: (s) => svg(s, [/* @__PURE__ */ jsx8("path", { d: "M6 5v4c0 3 3 3 6 3s6 0 6-3V5M12 12v7" }, "p")]),
387
490
  "console-sink": (s) => svg(s, [
388
- /* @__PURE__ */ jsx7("rect", { x: "4", y: "5", width: "16", height: "12", rx: "2" }, "r"),
389
- /* @__PURE__ */ jsx7("path", { d: "M8 10l2.5 2L8 14M13 14h3" }, "p")
491
+ /* @__PURE__ */ jsx8("rect", { x: "4", y: "5", width: "16", height: "12", rx: "2" }, "r"),
492
+ /* @__PURE__ */ jsx8("path", { d: "M8 10l2.5 2L8 14M13 14h3" }, "p")
390
493
  ]),
391
494
  "collect-sink": (s) => svg(s, [
392
- /* @__PURE__ */ jsx7("path", { d: "M4 7h16M4 12h16M4 17h10" }, "p")
495
+ /* @__PURE__ */ jsx8("path", { d: "M4 7h16M4 12h16M4 17h10" }, "p")
393
496
  ])
394
497
  };
395
498
  var BY_KIND = {
396
- R: (s) => svg(s, [/* @__PURE__ */ jsx7("path", { d: "M8 5l10 7-10 7z" }, "p")]),
499
+ R: (s) => svg(s, [/* @__PURE__ */ jsx8("path", { d: "M8 5l10 7-10 7z" }, "p")]),
397
500
  // source ▷ (play)
398
- T: (s) => svg(s, [/* @__PURE__ */ jsx7("path", { d: "M4 8h6l4 8h6M14 8h6M4 16h6" }, "p")]),
501
+ T: (s) => svg(s, [/* @__PURE__ */ jsx8("path", { d: "M4 8h6l4 8h6M14 8h6M4 16h6" }, "p")]),
399
502
  // transform
400
503
  W: (s) => svg(s, [
401
- /* @__PURE__ */ jsx7("path", { d: "M5 4h11l3 3v13H5z" }, "p"),
402
- /* @__PURE__ */ jsx7("path", { d: "M9 4v5h6" }, "q")
504
+ /* @__PURE__ */ jsx8("path", { d: "M5 4h11l3 3v13H5z" }, "p"),
505
+ /* @__PURE__ */ jsx8("path", { d: "M9 4v5h6" }, "q")
403
506
  ])
404
507
  // sink (save)
405
508
  };
@@ -410,61 +513,61 @@ function NodeIcon({ code, kind, size = 15 }) {
410
513
  }
411
514
  function EyeIcon({ size = 14 }) {
412
515
  return svg(size, [
413
- /* @__PURE__ */ jsx7("path", { d: "M2 12s3.5-6.5 10-6.5S22 12 22 12s-3.5 6.5-10 6.5S2 12 2 12z" }, "p"),
414
- /* @__PURE__ */ jsx7("circle", { cx: "12", cy: "12", r: "2.6" }, "c")
516
+ /* @__PURE__ */ jsx8("path", { d: "M2 12s3.5-6.5 10-6.5S22 12 22 12s-3.5 6.5-10 6.5S2 12 2 12z" }, "p"),
517
+ /* @__PURE__ */ jsx8("circle", { cx: "12", cy: "12", r: "2.6" }, "c")
415
518
  ]);
416
519
  }
417
520
  function EyeOffIcon({ size = 14 }) {
418
521
  return svg(size, [
419
- /* @__PURE__ */ jsx7("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"),
420
- /* @__PURE__ */ jsx7("path", { d: "M9.9 9.9a2.6 2.6 0 003.7 3.7" }, "e"),
421
- /* @__PURE__ */ jsx7("path", { d: "M3 3l18 18" }, "s")
522
+ /* @__PURE__ */ jsx8("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"),
523
+ /* @__PURE__ */ jsx8("path", { d: "M9.9 9.9a2.6 2.6 0 003.7 3.7" }, "e"),
524
+ /* @__PURE__ */ jsx8("path", { d: "M3 3l18 18" }, "s")
422
525
  ]);
423
526
  }
424
527
  function UnlockIcon({ size = 14 }) {
425
528
  return svg(size, [
426
- /* @__PURE__ */ jsx7("rect", { x: "5", y: "11", width: "14", height: "9", rx: "2" }, "r"),
427
- /* @__PURE__ */ jsx7("path", { d: "M8 11V7a4 4 0 017.9-1" }, "s")
529
+ /* @__PURE__ */ jsx8("rect", { x: "5", y: "11", width: "14", height: "9", rx: "2" }, "r"),
530
+ /* @__PURE__ */ jsx8("path", { d: "M8 11V7a4 4 0 017.9-1" }, "s")
428
531
  ]);
429
532
  }
430
533
  function LockIcon({ size = 14 }) {
431
534
  return svg(size, [
432
- /* @__PURE__ */ jsx7("rect", { x: "5", y: "11", width: "14", height: "9", rx: "2" }, "r"),
433
- /* @__PURE__ */ jsx7("path", { d: "M8 11V7a4 4 0 018 0v4" }, "s")
535
+ /* @__PURE__ */ jsx8("rect", { x: "5", y: "11", width: "14", height: "9", rx: "2" }, "r"),
536
+ /* @__PURE__ */ jsx8("path", { d: "M8 11V7a4 4 0 018 0v4" }, "s")
434
537
  ]);
435
538
  }
436
539
  function WarningIcon({ size = 13 }) {
437
540
  return svg(size, [
438
- /* @__PURE__ */ jsx7("path", { d: "M12 4l9 15H3z" }, "t"),
439
- /* @__PURE__ */ jsx7("path", { d: "M12 10v4" }, "b"),
440
- /* @__PURE__ */ jsx7("path", { d: "M12 17v.5" }, "d")
541
+ /* @__PURE__ */ jsx8("path", { d: "M12 4l9 15H3z" }, "t"),
542
+ /* @__PURE__ */ jsx8("path", { d: "M12 10v4" }, "b"),
543
+ /* @__PURE__ */ jsx8("path", { d: "M12 17v.5" }, "d")
441
544
  ]);
442
545
  }
443
546
  function ErrorIcon({ size = 13 }) {
444
547
  return svg(size, [
445
- /* @__PURE__ */ jsx7("circle", { cx: "12", cy: "12", r: "9" }, "c"),
446
- /* @__PURE__ */ jsx7("path", { d: "M12 7v6" }, "b"),
447
- /* @__PURE__ */ jsx7("path", { d: "M12 16v.5" }, "d")
548
+ /* @__PURE__ */ jsx8("circle", { cx: "12", cy: "12", r: "9" }, "c"),
549
+ /* @__PURE__ */ jsx8("path", { d: "M12 7v6" }, "b"),
550
+ /* @__PURE__ */ jsx8("path", { d: "M12 16v.5" }, "d")
448
551
  ]);
449
552
  }
450
553
 
451
554
  // src/panels/DiagnosticBadge.tsx
452
- import { jsx as jsx8 } from "react/jsx-runtime";
555
+ import { jsx as jsx9 } from "react/jsx-runtime";
453
556
  function DiagnosticBadge({ diagnostic, variant, size = 12 }) {
454
- return /* @__PURE__ */ jsx8(
557
+ return /* @__PURE__ */ jsx9(
455
558
  "span",
456
559
  {
457
560
  className: `${variant} hl-diag-${diagnostic.severity}`,
458
561
  title: diagnostic.message,
459
562
  "aria-label": `${diagnostic.severity}: ${diagnostic.message}`,
460
- children: diagnostic.severity === "error" ? /* @__PURE__ */ jsx8(ErrorIcon, { size }) : /* @__PURE__ */ jsx8(WarningIcon, { size })
563
+ children: diagnostic.severity === "error" ? /* @__PURE__ */ jsx9(ErrorIcon, { size }) : /* @__PURE__ */ jsx9(WarningIcon, { size })
461
564
  }
462
565
  );
463
566
  }
464
567
 
465
568
  // src/nodes/NodeInteractionContext.tsx
466
569
  import { createContext as createContext2, useContext as useContext2 } from "react";
467
- import { jsx as jsx9 } from "react/jsx-runtime";
570
+ import { jsx as jsx10 } from "react/jsx-runtime";
468
571
  var NoopInteraction = {
469
572
  diagnosticFor: () => void 0,
470
573
  rename: () => void 0
@@ -474,14 +577,14 @@ function NodeInteractionProvider({
474
577
  value,
475
578
  children
476
579
  }) {
477
- return /* @__PURE__ */ jsx9(NodeInteractionCtx.Provider, { value, children });
580
+ return /* @__PURE__ */ jsx10(NodeInteractionCtx.Provider, { value, children });
478
581
  }
479
582
  function useNodeInteraction() {
480
583
  return useContext2(NodeInteractionCtx);
481
584
  }
482
585
 
483
586
  // src/nodes/PillNode.tsx
484
- import { jsx as jsx10, jsxs as jsxs3 } from "react/jsx-runtime";
587
+ import { jsx as jsx11, jsxs as jsxs3 } from "react/jsx-runtime";
485
588
  var DOUBLE_CLICK_MS = 320;
486
589
  function PillNodeChrome({ node, selected }) {
487
590
  const live = useNodeLiveState(node.id);
@@ -527,18 +630,20 @@ function PillNodeChrome({ node, selected }) {
527
630
  }, []);
528
631
  const inputs = node.ports.filter((p) => p.direction === "I" && !p.isError);
529
632
  const outputs = node.ports.filter((p) => p.direction === "O" && !p.isError);
530
- const className = [
633
+ const className2 = [
531
634
  "hl-pill",
532
635
  `hl-pill-${node.kind.toLowerCase()}`,
533
636
  selected ? "is-selected" : "",
534
637
  live === "active" ? "is-active" : "",
535
638
  live === "error" ? "is-live-error" : "",
639
+ live === "finished" ? "is-finished" : "",
536
640
  (diagnostic == null ? void 0 : diagnostic.severity) === "error" ? "has-diag-error" : "",
537
641
  (diagnostic == null ? void 0 : diagnostic.severity) === "warning" ? "has-diag-warning" : ""
538
642
  ].filter(Boolean).join(" ");
539
- return /* @__PURE__ */ jsxs3("div", { className, "data-kind": node.kind, children: [
540
- /* @__PURE__ */ jsx10("span", { className: "hl-pill-dot", "aria-hidden": true, children: /* @__PURE__ */ jsx10(NodeIcon, { code: node.code, kind: node.kind, size: 13 }) }),
541
- editing ? /* @__PURE__ */ jsx10(
643
+ return /* @__PURE__ */ jsxs3("div", { className: className2, "data-kind": node.kind, children: [
644
+ /* @__PURE__ */ jsx11(LivePulse, { nodeId: node.id }),
645
+ /* @__PURE__ */ jsx11("span", { className: "hl-pill-dot", "aria-hidden": true, children: /* @__PURE__ */ jsx11(NodeIcon, { code: node.code, kind: node.kind, size: 13 }) }),
646
+ editing ? /* @__PURE__ */ jsx11(
542
647
  "input",
543
648
  {
544
649
  ref: inputRef,
@@ -550,7 +655,7 @@ function PillNodeChrome({ node, selected }) {
550
655
  spellCheck: false,
551
656
  "aria-label": "Rename node"
552
657
  }
553
- ) : /* @__PURE__ */ jsx10(
658
+ ) : /* @__PURE__ */ jsx11(
554
659
  "span",
555
660
  {
556
661
  className: "hl-pill-name",
@@ -561,62 +666,167 @@ function PillNodeChrome({ node, selected }) {
561
666
  children: node.name
562
667
  }
563
668
  ),
564
- diagnostic !== void 0 && /* @__PURE__ */ jsx10(DiagnosticBadge, { diagnostic, variant: "hl-pill-diag", size: 12 }),
565
- /* @__PURE__ */ jsx10("span", { className: "hl-pill-status", "aria-hidden": true }),
566
- /* @__PURE__ */ jsx10("ul", { className: "hl-pill-ports hl-in", children: inputs.map((port) => /* @__PURE__ */ jsx10(PortRow, { port }, port.id)) }),
567
- /* @__PURE__ */ jsx10("ul", { className: "hl-pill-ports hl-out", children: outputs.map((port) => /* @__PURE__ */ jsx10(PortRow, { port }, port.id)) })
669
+ diagnostic !== void 0 && /* @__PURE__ */ jsx11(DiagnosticBadge, { diagnostic, variant: "hl-pill-diag", size: 12 }),
670
+ /* @__PURE__ */ jsx11("span", { className: "hl-pill-status", "aria-hidden": true }),
671
+ /* @__PURE__ */ jsx11("ul", { className: "hl-pill-ports hl-in", children: inputs.map((port) => /* @__PURE__ */ jsx11(PortRow, { port }, port.id)) }),
672
+ /* @__PURE__ */ jsx11("ul", { className: "hl-pill-ports hl-out", children: outputs.map((port) => /* @__PURE__ */ jsx11(PortRow, { port }, port.id)) })
568
673
  ] });
569
674
  }
570
675
 
571
676
  // src/nodes/HlangNode.tsx
572
- import { jsx as jsx11, jsxs as jsxs4 } from "react/jsx-runtime";
677
+ import { jsx as jsx12, jsxs as jsxs4 } from "react/jsx-runtime";
573
678
  function NodeChrome({ node, selected, body }) {
574
679
  const inputs = node.ports.filter((p) => p.direction === "I");
575
680
  const outputs = node.ports.filter((p) => p.direction === "O");
576
681
  const live = useNodeLiveState(node.id);
577
- const className = [
682
+ const className2 = [
578
683
  "hl-node",
579
684
  `hl-${node.kind.toLowerCase()}`,
580
685
  selected ? "is-selected" : "",
581
686
  live === "active" ? "is-active" : "",
582
- live === "error" ? "is-live-error" : ""
687
+ live === "error" ? "is-live-error" : "",
688
+ live === "finished" ? "is-finished" : ""
583
689
  ].filter(Boolean).join(" ");
584
- return /* @__PURE__ */ jsxs4("div", { className, "data-kind": node.kind, children: [
585
- /* @__PURE__ */ jsx11(SignalBar, {}),
690
+ return /* @__PURE__ */ jsxs4("div", { className: className2, "data-kind": node.kind, children: [
691
+ /* @__PURE__ */ jsx12(LivePulse, { nodeId: node.id }),
692
+ /* @__PURE__ */ jsx12(SignalBar, {}),
586
693
  /* @__PURE__ */ jsxs4("header", { className: "hl-head", children: [
587
- /* @__PURE__ */ jsx11("span", { className: "hl-kind", children: node.kind }),
588
- /* @__PURE__ */ jsx11("span", { className: "hl-name", children: node.name }),
589
- /* @__PURE__ */ jsx11("span", { className: "hl-role", children: KIND_ROLE[node.kind] })
694
+ /* @__PURE__ */ jsx12("span", { className: "hl-kind", children: node.kind }),
695
+ /* @__PURE__ */ jsx12("span", { className: "hl-name", children: node.name }),
696
+ /* @__PURE__ */ jsx12("span", { className: "hl-role", children: KIND_ROLE[node.kind] })
590
697
  ] }),
591
- /* @__PURE__ */ jsx11(NodeBodySlot, { node, children: body }),
698
+ /* @__PURE__ */ jsx12(NodeBodySlot, { node, children: body }),
592
699
  /* @__PURE__ */ jsxs4("div", { className: "hl-ports", children: [
593
- /* @__PURE__ */ jsx11("ul", { className: "hl-col hl-in", children: inputs.map((port) => /* @__PURE__ */ jsx11(PortRow, { port }, port.id)) }),
594
- /* @__PURE__ */ jsx11("ul", { className: "hl-col hl-out", children: outputs.map((port) => /* @__PURE__ */ jsx11(PortRow, { port }, port.id)) })
700
+ /* @__PURE__ */ jsx12("ul", { className: "hl-col hl-in", children: inputs.map((port) => /* @__PURE__ */ jsx12(PortRow, { port }, port.id)) }),
701
+ /* @__PURE__ */ jsx12("ul", { className: "hl-col hl-out", children: outputs.map((port) => /* @__PURE__ */ jsx12(PortRow, { port }, port.id)) })
595
702
  ] })
596
703
  ] });
597
704
  }
598
705
  function HlangNode({ data, selected }) {
599
706
  const node = data.editorNode;
600
707
  if (typeof node.params.imageUrl === "string" && node.params.imageUrl.length > 0) {
601
- return /* @__PURE__ */ jsx11(AssetNodeChrome, { node, selected });
708
+ return /* @__PURE__ */ jsx12(AssetNodeChrome, { node, selected });
602
709
  }
603
- return /* @__PURE__ */ jsx11(PillNodeChrome, { node, selected });
710
+ return /* @__PURE__ */ jsx12(PillNodeChrome, { node, selected });
604
711
  }
605
712
 
606
713
  // src/edges/FlowEdge.tsx
607
714
  import { BaseEdge, EdgeLabelRenderer, getBezierPath, useInternalNode } from "@xyflow/react";
608
- import { Fragment, jsx as jsx12, jsxs as jsxs5 } from "react/jsx-runtime";
609
- function useSourceRole(sourceId) {
610
- var _a, _b;
611
- const node = useInternalNode(sourceId);
612
- const editorNode = (_a = node == null ? void 0 : node.data) == null ? void 0 : _a.editorNode;
613
- const role = (_b = editorNode == null ? void 0 : editorNode.params) == null ? void 0 : _b.role;
715
+
716
+ // src/edges/self-loop.ts
717
+ var SELF_LOOP_CLEARANCE = 28;
718
+ var SELF_LOOP_RADIUS = 16;
719
+ function format(value) {
720
+ return String(Math.round(value * 100) / 100);
721
+ }
722
+ function distance(a, b) {
723
+ return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
724
+ }
725
+ function toward(from, to, length) {
726
+ const span = distance(from, to);
727
+ const share = span === 0 ? 0 : length / span;
728
+ return { x: from.x + (to.x - from.x) * share, y: from.y + (to.y - from.y) * share };
729
+ }
730
+ function selfLoopPath(input) {
731
+ var _a, _b, _c, _d, _e;
732
+ const clearance = (_a = input.clearance) != null ? _a : SELF_LOOP_CLEARANCE;
733
+ const radius = (_b = input.radius) != null ? _b : SELF_LOOP_RADIUS;
734
+ const { source, target, node } = input;
735
+ const right = Math.max(source.x, node.x + node.width) + clearance;
736
+ const left = Math.min(target.x, node.x) - clearance;
737
+ const below = node.y + node.height + clearance;
738
+ const points = [
739
+ source,
740
+ { x: right, y: source.y },
741
+ { x: right, y: below },
742
+ { x: left, y: below },
743
+ { x: left, y: target.y },
744
+ target
745
+ ];
746
+ const parts = [`M ${format(source.x)} ${format(source.y)}`];
747
+ for (let i = 1; i < points.length - 1; i += 1) {
748
+ const before = (_c = points[i - 1]) != null ? _c : source;
749
+ const corner = (_d = points[i]) != null ? _d : source;
750
+ const after = (_e = points[i + 1]) != null ? _e : target;
751
+ const r = Math.min(radius, distance(before, corner) / 2, distance(corner, after) / 2);
752
+ if (r <= 0) {
753
+ parts.push(`L ${format(corner.x)} ${format(corner.y)}`);
754
+ continue;
755
+ }
756
+ const enter = toward(corner, before, r);
757
+ const leave = toward(corner, after, r);
758
+ parts.push(
759
+ `L ${format(enter.x)} ${format(enter.y)} Q ${format(corner.x)} ${format(corner.y)} ${format(leave.x)} ${format(leave.y)}`
760
+ );
761
+ }
762
+ parts.push(`L ${format(target.x)} ${format(target.y)}`);
763
+ return { path: parts.join(" "), labelX: (right + left) / 2, labelY: below };
764
+ }
765
+
766
+ // src/edges/edge-bounds.ts
767
+ var DEFAULT_EDGE_CAPACITY = 10;
768
+ function boundsFields(bounds) {
769
+ var _a;
770
+ const bounded = bounds.capacity !== void 0;
771
+ return [
772
+ { key: "bounded", label: "Bounded", type: "toggle", value: bounded },
773
+ { key: "capacity", label: "Capacity", type: "number", value: (_a = bounds.capacity) != null ? _a : DEFAULT_EDGE_CAPACITY, disabled: !bounded },
774
+ {
775
+ key: "dropOldest",
776
+ label: "Drop oldest when full",
777
+ type: "toggle",
778
+ value: bounds.overflow === "drop-oldest",
779
+ disabled: !bounded
780
+ }
781
+ ];
782
+ }
783
+ function nextBounds(bounds, key, value) {
784
+ const { capacity, overflow } = bounds;
785
+ if (key === "bounded") {
786
+ return value === true ? { capacity: capacity != null ? capacity : DEFAULT_EDGE_CAPACITY } : {};
787
+ }
788
+ if (capacity === void 0) {
789
+ return {};
790
+ }
791
+ const keep = overflow === void 0 ? {} : { overflow };
792
+ if (key === "capacity") {
793
+ if (typeof value !== "number" || Number.isNaN(value)) {
794
+ return bounds;
795
+ }
796
+ return { capacity: Math.max(1, Math.round(value)), ...keep };
797
+ }
798
+ if (key === "dropOldest") {
799
+ return value === true ? { capacity, overflow: "drop-oldest" } : { capacity };
800
+ }
801
+ return bounds;
802
+ }
803
+ function capacityLabel(bounds) {
804
+ if (bounds.capacity === void 0) {
805
+ return void 0;
806
+ }
807
+ const label = `[${String(bounds.capacity)}]`;
808
+ return bounds.overflow === "drop-oldest" ? `${label} drop oldest` : label;
809
+ }
810
+
811
+ // src/edges/FlowEdge.tsx
812
+ import { Fragment, jsx as jsx13, jsxs as jsxs5 } from "react/jsx-runtime";
813
+ function roleOf(node) {
814
+ const role = node == null ? void 0 : node.data.editorNode.params.role;
614
815
  return typeof role === "string" && role.length > 0 ? role : void 0;
615
816
  }
616
817
  function FlowEdge(props) {
617
- var _a;
618
- const { id, source, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, data } = props;
619
- const [path] = getBezierPath({
818
+ var _a, _b, _c, _d, _e, _f;
819
+ const { id, source, target, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, data, selected } = props;
820
+ const sourceNode = useInternalNode(source);
821
+ const role = roleOf(sourceNode);
822
+ const width = sourceNode == null ? void 0 : sourceNode.measured.width;
823
+ const height = sourceNode == null ? void 0 : sourceNode.measured.height;
824
+ const loop = source === target && sourceNode !== void 0 && width !== void 0 && height !== void 0 ? selfLoopPath({
825
+ source: { x: sourceX, y: sourceY },
826
+ target: { x: targetX, y: targetY },
827
+ node: { ...sourceNode.internals.positionAbsolute, width, height }
828
+ }) : void 0;
829
+ const [curve] = getBezierPath({
620
830
  sourceX,
621
831
  sourceY,
622
832
  sourcePosition,
@@ -624,25 +834,39 @@ function FlowEdge(props) {
624
834
  targetY,
625
835
  targetPosition
626
836
  });
837
+ const path = (_a = loop == null ? void 0 : loop.path) != null ? _a : curve;
627
838
  const t = 0.62;
628
- const labelX = sourceX + (targetX - sourceX) * t;
629
- const labelY = sourceY + (targetY - sourceY) * t;
630
- const isError = (_a = data == null ? void 0 : data.isError) != null ? _a : false;
839
+ const labelX = (_b = loop == null ? void 0 : loop.labelX) != null ? _b : sourceX + (targetX - sourceX) * t;
840
+ const labelY = (_c = loop == null ? void 0 : loop.labelY) != null ? _c : sourceY + (targetY - sourceY) * t;
841
+ const capacity = capacityLabel(data != null ? data : {});
842
+ const capacityX = (_d = loop == null ? void 0 : loop.labelX) != null ? _d : sourceX + (targetX - sourceX) * 0.38;
843
+ const capacityY = (_e = loop == null ? void 0 : loop.labelY) != null ? _e : sourceY + (targetY - sourceY) * 0.38;
844
+ const isError = (_f = data == null ? void 0 : data.isError) != null ? _f : false;
631
845
  const isValue = (data == null ? void 0 : data.stream) === "value";
632
846
  const flowing = useEdgeFlowing(id);
633
- const role = useSourceRole(source);
634
- const className = [
847
+ const pulse = useEdgePulse(id);
848
+ const className2 = [
635
849
  "hl-edge",
636
850
  isError ? "is-error" : "",
637
851
  isValue ? "is-value" : "is-live",
638
- flowing ? "is-flowing" : ""
852
+ flowing ? "is-flowing" : "",
853
+ selected === true ? "is-selected" : ""
639
854
  ].filter(Boolean).join(" ");
640
855
  return /* @__PURE__ */ jsxs5(Fragment, { children: [
641
- /* @__PURE__ */ jsxs5("g", { className, children: [
642
- /* @__PURE__ */ jsx12(BaseEdge, { id, path, className: "hl-edge-base" }),
643
- /* @__PURE__ */ jsx12("path", { d: path, className: "hl-edge-flow" })
856
+ /* @__PURE__ */ jsxs5("g", { className: className2, children: [
857
+ /* @__PURE__ */ jsx13(BaseEdge, { id, path, className: "hl-edge-base" }),
858
+ /* @__PURE__ */ jsx13("path", { d: path, className: "hl-edge-flow" }),
859
+ pulse > 0 && /* @__PURE__ */ jsx13("path", { d: path, className: "hl-edge-pulse" }, pulse)
644
860
  ] }),
645
- role !== void 0 && /* @__PURE__ */ jsx12(EdgeLabelRenderer, { children: /* @__PURE__ */ jsx12(
861
+ capacity !== void 0 && /* @__PURE__ */ jsx13(EdgeLabelRenderer, { children: /* @__PURE__ */ jsx13(
862
+ "div",
863
+ {
864
+ className: `hl-edge-capacity hl-mono${(data == null ? void 0 : data.overflow) === "drop-oldest" ? " is-lossy" : ""}`,
865
+ style: { transform: `translate(-50%, -50%) translate(${String(capacityX)}px, ${String(capacityY)}px)` },
866
+ children: capacity
867
+ }
868
+ ) }),
869
+ role !== void 0 && /* @__PURE__ */ jsx13(EdgeLabelRenderer, { children: /* @__PURE__ */ jsx13(
646
870
  "div",
647
871
  {
648
872
  className: "hl-edge-label hl-mono",
@@ -664,49 +888,257 @@ function createHlangFlowTypes() {
664
888
  }
665
889
 
666
890
  // src/shell/EditorShell.tsx
667
- import { jsx as jsx13, jsxs as jsxs6 } from "react/jsx-runtime";
891
+ import { useEffect as useEffect2, useId, useRef as useRef3 } from "react";
892
+
893
+ // src/shell/FooterResizer.tsx
894
+ import { jsx as jsx14 } from "react/jsx-runtime";
895
+ function FooterResizer({ resize, controls, label }) {
896
+ const onPointerDown = (event) => {
897
+ if (event.pointerType === "mouse" && event.button !== 0) {
898
+ return;
899
+ }
900
+ event.preventDefault();
901
+ event.currentTarget.setPointerCapture(event.pointerId);
902
+ resize.startDrag(event.clientY);
903
+ };
904
+ const onPointerMove = (event) => {
905
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
906
+ resize.drag(event.clientY);
907
+ }
908
+ };
909
+ const onPointerEnd = (event) => {
910
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
911
+ event.currentTarget.releasePointerCapture(event.pointerId);
912
+ }
913
+ resize.endDrag();
914
+ };
915
+ const onKeyDown = (event) => {
916
+ if (resize.keyDown(event.key, event.shiftKey)) {
917
+ event.preventDefault();
918
+ }
919
+ };
920
+ return /* @__PURE__ */ jsx14(
921
+ "div",
922
+ {
923
+ role: "separator",
924
+ "aria-orientation": "horizontal",
925
+ "aria-label": label,
926
+ "aria-controls": controls,
927
+ "aria-valuemin": resize.bounds.min,
928
+ "aria-valuemax": resize.bounds.max,
929
+ "aria-valuenow": resize.height,
930
+ tabIndex: 0,
931
+ className: `hl-footer-resizer${resize.dragging ? " is-dragging" : ""}`,
932
+ onPointerDown,
933
+ onPointerMove,
934
+ onPointerUp: onPointerEnd,
935
+ onPointerCancel: onPointerEnd,
936
+ onLostPointerCapture: () => {
937
+ resize.endDrag();
938
+ },
939
+ onDoubleClick: () => {
940
+ resize.toggle();
941
+ },
942
+ onKeyDown
943
+ }
944
+ );
945
+ }
946
+
947
+ // src/shell/useFooterResize.ts
948
+ import { useCallback as useCallback2, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
949
+
950
+ // src/shell/footer-size.ts
951
+ var FOOTER_KEY_STEP = 16;
952
+ function footerBounds(options) {
953
+ const { available, minHeight, minCanvasHeight } = options;
954
+ return { min: minHeight, max: Math.max(minHeight, available - minCanvasHeight) };
955
+ }
956
+ function clampFooter(height, bounds) {
957
+ return Math.round(Math.min(bounds.max, Math.max(bounds.min, height)));
958
+ }
959
+ function resizeByKey(height, key, shift, bounds) {
960
+ const step = FOOTER_KEY_STEP * (shift ? 4 : 1);
961
+ switch (key) {
962
+ case "ArrowUp":
963
+ return clampFooter(height + step, bounds);
964
+ case "ArrowDown":
965
+ return clampFooter(height - step, bounds);
966
+ case "Home":
967
+ return bounds.min;
968
+ case "End":
969
+ return bounds.max;
970
+ default:
971
+ return void 0;
972
+ }
973
+ }
974
+ function toggleCollapsed(size, bounds) {
975
+ if (size.height > bounds.min) {
976
+ return { height: bounds.min, restore: size.height };
977
+ }
978
+ return { height: clampFooter(size.restore, bounds), restore: size.restore };
979
+ }
980
+
981
+ // src/shell/useFooterResize.ts
982
+ var DEFAULT_MIN_CANVAS_HEIGHT = 160;
983
+ function useFooterResize(options) {
984
+ var _a;
985
+ const { defaultHeight, minHeight } = options;
986
+ const minCanvasHeight = (_a = options.minCanvasHeight) != null ? _a : DEFAULT_MIN_CANVAS_HEIGHT;
987
+ const [available, setAvailable] = useState2(null);
988
+ const [preferred, setPreferred] = useState2(defaultHeight);
989
+ const [restore, setRestore] = useState2(defaultHeight);
990
+ const [dragging, setDragging] = useState2(false);
991
+ const dragStart = useRef2(null);
992
+ const bounds = useMemo2(
993
+ () => available === null ? { min: minHeight, max: Math.max(minHeight, defaultHeight) } : footerBounds({ available, minHeight, minCanvasHeight }),
994
+ [available, minHeight, minCanvasHeight, defaultHeight]
995
+ );
996
+ const height = clampFooter(preferred, bounds);
997
+ const choose = useCallback2(
998
+ (next) => {
999
+ setPreferred(next);
1000
+ if (next > bounds.min) {
1001
+ setRestore(next);
1002
+ }
1003
+ },
1004
+ [bounds.min]
1005
+ );
1006
+ const measure = useCallback2((next) => {
1007
+ setAvailable(next);
1008
+ }, []);
1009
+ const startDrag = useCallback2(
1010
+ (clientY) => {
1011
+ dragStart.current = { clientY, height };
1012
+ setDragging(true);
1013
+ },
1014
+ [height]
1015
+ );
1016
+ const drag = useCallback2(
1017
+ (clientY) => {
1018
+ const start = dragStart.current;
1019
+ if (start === null) {
1020
+ return;
1021
+ }
1022
+ choose(clampFooter(start.height + (start.clientY - clientY), bounds));
1023
+ },
1024
+ [bounds, choose]
1025
+ );
1026
+ const endDrag = useCallback2(() => {
1027
+ dragStart.current = null;
1028
+ setDragging(false);
1029
+ }, []);
1030
+ const toggle = useCallback2(() => {
1031
+ const next = toggleCollapsed({ height, restore }, bounds);
1032
+ setPreferred(next.height);
1033
+ setRestore(next.restore);
1034
+ }, [height, restore, bounds]);
1035
+ const keyDown = useCallback2(
1036
+ (key, shift) => {
1037
+ if (key === "Enter") {
1038
+ toggle();
1039
+ return true;
1040
+ }
1041
+ const next = resizeByKey(height, key, shift, bounds);
1042
+ if (next === void 0) {
1043
+ return false;
1044
+ }
1045
+ choose(next);
1046
+ return true;
1047
+ },
1048
+ [height, bounds, choose, toggle]
1049
+ );
1050
+ return { height, bounds, dragging, measure, startDrag, drag, endDrag, keyDown, toggle };
1051
+ }
1052
+
1053
+ // src/shell/EditorShell.tsx
1054
+ import { jsx as jsx15, jsxs as jsxs6 } from "react/jsx-runtime";
668
1055
  function EditorShell({
669
1056
  toolbar,
670
1057
  left,
671
1058
  right,
672
1059
  canvas,
673
1060
  footer,
1061
+ footerResize,
674
1062
  leftCollapsed = false,
675
1063
  rightCollapsed = false
676
1064
  }) {
677
- const className = [
1065
+ const shellRef = useRef3(null);
1066
+ const toolbarRef = useRef3(null);
1067
+ const className2 = [
678
1068
  "hl-shell",
679
1069
  leftCollapsed ? "hl-left-collapsed" : "",
680
1070
  rightCollapsed ? "hl-right-collapsed" : ""
681
1071
  ].filter(Boolean).join(" ");
682
- return /* @__PURE__ */ jsxs6("div", { className, children: [
683
- toolbar !== void 0 && /* @__PURE__ */ jsx13("div", { className: "hl-region-toolbar", children: toolbar }),
1072
+ return /* @__PURE__ */ jsxs6("div", { className: className2, ref: shellRef, children: [
1073
+ toolbar !== void 0 && /* @__PURE__ */ jsx15("div", { className: "hl-region-toolbar", ref: toolbarRef, children: toolbar }),
684
1074
  /* @__PURE__ */ jsxs6("div", { className: "hl-region-body", children: [
685
- left !== void 0 && !leftCollapsed && /* @__PURE__ */ jsx13("aside", { className: "hl-region-left", children: left }),
686
- /* @__PURE__ */ jsx13("main", { className: "hl-region-canvas", children: canvas }),
687
- right !== void 0 && !rightCollapsed && /* @__PURE__ */ jsx13("aside", { className: "hl-region-right", children: right })
1075
+ left !== void 0 && !leftCollapsed && /* @__PURE__ */ jsx15("aside", { className: "hl-region-left", children: left }),
1076
+ /* @__PURE__ */ jsx15("main", { className: "hl-region-canvas", children: canvas }),
1077
+ right !== void 0 && !rightCollapsed && /* @__PURE__ */ jsx15("aside", { className: "hl-region-right", children: right })
688
1078
  ] }),
689
- footer !== void 0 && /* @__PURE__ */ jsx13("div", { className: "hl-region-footer", children: footer })
1079
+ footer !== void 0 && (footerResize === void 0 ? /* @__PURE__ */ jsx15("div", { className: "hl-region-footer", children: footer }) : /* @__PURE__ */ jsx15(ResizableFooter, { options: footerResize, shellRef, toolbarRef, children: footer }))
690
1080
  ] });
691
1081
  }
1082
+ function ResizableFooter({
1083
+ options,
1084
+ shellRef,
1085
+ toolbarRef,
1086
+ children
1087
+ }) {
1088
+ var _a;
1089
+ const resize = useFooterResize(options);
1090
+ const id = useId();
1091
+ const { measure } = resize;
1092
+ useEffect2(() => {
1093
+ const shell = shellRef.current;
1094
+ if (shell === null) {
1095
+ return;
1096
+ }
1097
+ const update = () => {
1098
+ var _a2, _b;
1099
+ measure(shell.clientHeight - ((_b = (_a2 = toolbarRef.current) == null ? void 0 : _a2.offsetHeight) != null ? _b : 0));
1100
+ };
1101
+ update();
1102
+ const observer = new ResizeObserver(update);
1103
+ observer.observe(shell);
1104
+ if (toolbarRef.current !== null) {
1105
+ observer.observe(toolbarRef.current);
1106
+ }
1107
+ return () => {
1108
+ observer.disconnect();
1109
+ };
1110
+ }, [measure, shellRef, toolbarRef]);
1111
+ return /* @__PURE__ */ jsxs6(
1112
+ "div",
1113
+ {
1114
+ id,
1115
+ className: `hl-region-footer is-resizable${resize.dragging ? " is-resizing" : ""}`,
1116
+ style: { height: resize.height },
1117
+ children: [
1118
+ /* @__PURE__ */ jsx15(FooterResizer, { resize, controls: id, label: (_a = options.label) != null ? _a : "Resize the panel" }),
1119
+ children
1120
+ ]
1121
+ }
1122
+ );
1123
+ }
692
1124
 
693
1125
  // src/toolbar/Toolbar.tsx
694
1126
  import { useHistory } from "@hlang-tech/editor-react";
695
- import { Fragment as Fragment2, jsx as jsx14, jsxs as jsxs7 } from "react/jsx-runtime";
1127
+ import { Fragment as Fragment2, jsx as jsx16, jsxs as jsxs7 } from "react/jsx-runtime";
696
1128
  function Toolbar({ title, center, actions }) {
697
1129
  return /* @__PURE__ */ jsxs7("header", { className: "hl-toolbar", children: [
698
- /* @__PURE__ */ jsx14("div", { className: "hl-toolbar-brand", children: title != null ? title : /* @__PURE__ */ jsxs7(Fragment2, { children: [
699
- /* @__PURE__ */ jsx14("span", { className: "hl-toolbar-glyph", "aria-hidden": "true" }),
700
- /* @__PURE__ */ jsx14("span", { className: "hl-toolbar-wordmark", children: "hlang" })
1130
+ /* @__PURE__ */ jsx16("div", { className: "hl-toolbar-brand", children: title != null ? title : /* @__PURE__ */ jsxs7(Fragment2, { children: [
1131
+ /* @__PURE__ */ jsx16("span", { className: "hl-toolbar-glyph", "aria-hidden": "true" }),
1132
+ /* @__PURE__ */ jsx16("span", { className: "hl-toolbar-wordmark", children: "hlang" })
701
1133
  ] }) }),
702
- center !== void 0 && /* @__PURE__ */ jsx14("div", { className: "hl-toolbar-center", children: center }),
703
- /* @__PURE__ */ jsx14("div", { className: "hl-toolbar-actions", children: actions })
1134
+ center !== void 0 && /* @__PURE__ */ jsx16("div", { className: "hl-toolbar-center", children: center }),
1135
+ /* @__PURE__ */ jsx16("div", { className: "hl-toolbar-actions", children: actions })
704
1136
  ] });
705
1137
  }
706
1138
  function UndoRedo({ core }) {
707
1139
  const history = useHistory(core);
708
1140
  return /* @__PURE__ */ jsxs7(Fragment2, { children: [
709
- /* @__PURE__ */ jsx14(
1141
+ /* @__PURE__ */ jsx16(
710
1142
  "button",
711
1143
  {
712
1144
  type: "button",
@@ -717,7 +1149,7 @@ function UndoRedo({ core }) {
717
1149
  children: "Undo"
718
1150
  }
719
1151
  ),
720
- /* @__PURE__ */ jsx14(
1152
+ /* @__PURE__ */ jsx16(
721
1153
  "button",
722
1154
  {
723
1155
  type: "button",
@@ -744,7 +1176,7 @@ function RunButton({
744
1176
  onClick: onRun,
745
1177
  "aria-pressed": running,
746
1178
  children: [
747
- /* @__PURE__ */ jsx14(
1179
+ /* @__PURE__ */ jsx16(
748
1180
  "span",
749
1181
  {
750
1182
  className: `hl-toolbar-run-icon${running ? " is-stop" : ""}`,
@@ -757,22 +1189,22 @@ function RunButton({
757
1189
  );
758
1190
  }
759
1191
  function ToolbarStat({ label, value }) {
760
- return /* @__PURE__ */ jsx14("span", { className: "hl-toolbar-stat hl-mono", title: label, children: value });
1192
+ return /* @__PURE__ */ jsx16("span", { className: "hl-toolbar-stat hl-mono", title: label, children: value });
761
1193
  }
762
1194
  function ToolbarGhostButton({
763
1195
  children,
764
1196
  onClick,
765
1197
  title
766
1198
  }) {
767
- return /* @__PURE__ */ jsx14("button", { type: "button", className: "hl-toolbar-btn", onClick, title, children });
1199
+ return /* @__PURE__ */ jsx16("button", { type: "button", className: "hl-toolbar-btn", onClick, title, children });
768
1200
  }
769
1201
 
770
1202
  // src/toolbar/ToolCluster.tsx
771
- import { jsx as jsx15, jsxs as jsxs8 } from "react/jsx-runtime";
1203
+ import { jsx as jsx17, jsxs as jsxs8 } from "react/jsx-runtime";
772
1204
  function ToolCluster({ items }) {
773
- return /* @__PURE__ */ jsx15("div", { className: "hl-toolcluster", role: "toolbar", "aria-label": "Canvas tools", children: items.map((item) => /* @__PURE__ */ jsxs8("span", { className: "hl-toolcluster-slot", children: [
774
- item.separatorBefore === true && /* @__PURE__ */ jsx15("span", { className: "hl-toolcluster-sep", "aria-hidden": true }),
775
- /* @__PURE__ */ jsx15(
1205
+ return /* @__PURE__ */ jsx17("div", { className: "hl-toolcluster", role: "toolbar", "aria-label": "Canvas tools", children: items.map((item) => /* @__PURE__ */ jsxs8("span", { className: "hl-toolcluster-slot", children: [
1206
+ item.separatorBefore === true && /* @__PURE__ */ jsx17("span", { className: "hl-toolcluster-sep", "aria-hidden": true }),
1207
+ /* @__PURE__ */ jsx17(
776
1208
  "button",
777
1209
  {
778
1210
  type: "button",
@@ -790,30 +1222,30 @@ function ToolCluster({ items }) {
790
1222
 
791
1223
  // src/canvas/CanvasChrome.tsx
792
1224
  import { Background, BackgroundVariant } from "@xyflow/react";
793
- import { Fragment as Fragment3, jsx as jsx16 } from "react/jsx-runtime";
1225
+ import { Fragment as Fragment3, jsx as jsx18 } from "react/jsx-runtime";
794
1226
  function CanvasChrome({ background = true }) {
795
- return /* @__PURE__ */ jsx16(Fragment3, { children: background && /* @__PURE__ */ jsx16(Background, { variant: BackgroundVariant.Dots, gap: 26, size: 1, color: "#242830" }) });
1227
+ return /* @__PURE__ */ jsx18(Fragment3, { children: background && /* @__PURE__ */ jsx18(Background, { variant: BackgroundVariant.Dots, gap: 26, size: 1, color: "#242830" }) });
796
1228
  }
797
1229
 
798
1230
  // src/canvas/CanvasDock.tsx
799
- import { jsx as jsx17, jsxs as jsxs9 } from "react/jsx-runtime";
1231
+ import { jsx as jsx19, jsxs as jsxs9 } from "react/jsx-runtime";
800
1232
  function CursorIcon() {
801
- return /* @__PURE__ */ jsx17("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__ */ jsx17("path", { d: "M5 3l6 16 2-6 6-2z" }) });
1233
+ return /* @__PURE__ */ jsx19("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__ */ jsx19("path", { d: "M5 3l6 16 2-6 6-2z" }) });
802
1234
  }
803
1235
  function HandIcon() {
804
- return /* @__PURE__ */ jsx17("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__ */ jsx17("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" }) });
1236
+ return /* @__PURE__ */ jsx19("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__ */ jsx19("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" }) });
805
1237
  }
806
1238
  function PlusIcon() {
807
- return /* @__PURE__ */ jsx17("svg", { width: 17, height: 17, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.7, strokeLinecap: "round", "aria-hidden": true, children: /* @__PURE__ */ jsx17("path", { d: "M12 5v14M5 12h14" }) });
1239
+ return /* @__PURE__ */ jsx19("svg", { width: 17, height: 17, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.7, strokeLinecap: "round", "aria-hidden": true, children: /* @__PURE__ */ jsx19("path", { d: "M12 5v14M5 12h14" }) });
808
1240
  }
809
1241
  function FitIcon() {
810
- return /* @__PURE__ */ jsx17("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__ */ jsx17("path", { d: "M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5" }) });
1242
+ return /* @__PURE__ */ jsx19("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__ */ jsx19("path", { d: "M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5" }) });
811
1243
  }
812
1244
  function UndoIcon() {
813
- return /* @__PURE__ */ jsx17("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__ */ jsx17("path", { d: "M9 7L4 12l5 5M4 12h11a5 5 0 010 10h-1" }) });
1245
+ return /* @__PURE__ */ jsx19("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__ */ jsx19("path", { d: "M9 7L4 12l5 5M4 12h11a5 5 0 010 10h-1" }) });
814
1246
  }
815
1247
  function RedoIcon() {
816
- return /* @__PURE__ */ jsx17("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__ */ jsx17("path", { d: "M15 7l5 5-5 5M20 12H9a5 5 0 000 10h1" }) });
1248
+ return /* @__PURE__ */ jsx19("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__ */ jsx19("path", { d: "M15 7l5 5-5 5M20 12H9a5 5 0 000 10h1" }) });
817
1249
  }
818
1250
  function CanvasDock({
819
1251
  zoom,
@@ -830,7 +1262,7 @@ function CanvasDock({
830
1262
  }) {
831
1263
  const pct = `${String(Math.round(zoom * 100))}%`;
832
1264
  return /* @__PURE__ */ jsxs9("div", { className: "hl-dock", role: "toolbar", "aria-label": "Canvas tools", children: [
833
- /* @__PURE__ */ jsx17(
1265
+ /* @__PURE__ */ jsx19(
834
1266
  "button",
835
1267
  {
836
1268
  className: "hl-dock-btn",
@@ -838,10 +1270,10 @@ function CanvasDock({
838
1270
  disabled: !canUndo,
839
1271
  title: "Undo (\u2318Z)",
840
1272
  "aria-label": "Undo",
841
- children: /* @__PURE__ */ jsx17(UndoIcon, {})
1273
+ children: /* @__PURE__ */ jsx19(UndoIcon, {})
842
1274
  }
843
1275
  ),
844
- /* @__PURE__ */ jsx17(
1276
+ /* @__PURE__ */ jsx19(
845
1277
  "button",
846
1278
  {
847
1279
  className: "hl-dock-btn",
@@ -849,48 +1281,48 @@ function CanvasDock({
849
1281
  disabled: !canRedo,
850
1282
  title: "Redo (\u2318\u21E7Z)",
851
1283
  "aria-label": "Redo",
852
- children: /* @__PURE__ */ jsx17(RedoIcon, {})
1284
+ children: /* @__PURE__ */ jsx19(RedoIcon, {})
853
1285
  }
854
1286
  ),
855
- /* @__PURE__ */ jsx17("span", { className: "hl-dock-sep", "aria-hidden": true }),
856
- /* @__PURE__ */ jsx17(
1287
+ /* @__PURE__ */ jsx19("span", { className: "hl-dock-sep", "aria-hidden": true }),
1288
+ /* @__PURE__ */ jsx19(
857
1289
  "button",
858
1290
  {
859
1291
  className: "hl-dock-btn" + (mode === "select" ? " is-active" : ""),
860
1292
  onClick: () => onModeChange("select"),
861
1293
  title: "Select (V)",
862
1294
  "aria-pressed": mode === "select",
863
- children: /* @__PURE__ */ jsx17(CursorIcon, {})
1295
+ children: /* @__PURE__ */ jsx19(CursorIcon, {})
864
1296
  }
865
1297
  ),
866
- /* @__PURE__ */ jsx17(
1298
+ /* @__PURE__ */ jsx19(
867
1299
  "button",
868
1300
  {
869
1301
  className: "hl-dock-btn" + (mode === "pan" ? " is-active" : ""),
870
1302
  onClick: () => onModeChange("pan"),
871
1303
  title: "Pan (Space)",
872
1304
  "aria-pressed": mode === "pan",
873
- children: /* @__PURE__ */ jsx17(HandIcon, {})
1305
+ children: /* @__PURE__ */ jsx19(HandIcon, {})
874
1306
  }
875
1307
  ),
876
- /* @__PURE__ */ jsx17("button", { className: "hl-dock-btn hl-dock-add", onClick: onAdd, title: "Add node", children: /* @__PURE__ */ jsx17(PlusIcon, {}) }),
877
- /* @__PURE__ */ jsx17("span", { className: "hl-dock-sep", "aria-hidden": true }),
878
- /* @__PURE__ */ jsx17("button", { className: "hl-dock-btn", onClick: onZoomOut, title: "Zoom out", "aria-label": "Zoom out", children: /* @__PURE__ */ jsx17("span", { className: "hl-dock-glyph", children: "\u2212" }) }),
879
- /* @__PURE__ */ jsx17("button", { className: "hl-dock-zoom hl-mono", onClick: onFitView, title: "Fit view", children: pct }),
880
- /* @__PURE__ */ jsx17("button", { className: "hl-dock-btn", onClick: onZoomIn, title: "Zoom in", "aria-label": "Zoom in", children: /* @__PURE__ */ jsx17("span", { className: "hl-dock-glyph", children: "+" }) }),
881
- /* @__PURE__ */ jsx17("button", { className: "hl-dock-btn hl-dock-fit", onClick: onFitView, title: "Fit view", "aria-label": "Fit view", children: /* @__PURE__ */ jsx17(FitIcon, {}) })
1308
+ /* @__PURE__ */ jsx19("button", { className: "hl-dock-btn hl-dock-add", onClick: onAdd, title: "Add node", children: /* @__PURE__ */ jsx19(PlusIcon, {}) }),
1309
+ /* @__PURE__ */ jsx19("span", { className: "hl-dock-sep", "aria-hidden": true }),
1310
+ /* @__PURE__ */ jsx19("button", { className: "hl-dock-btn", onClick: onZoomOut, title: "Zoom out", "aria-label": "Zoom out", children: /* @__PURE__ */ jsx19("span", { className: "hl-dock-glyph", children: "\u2212" }) }),
1311
+ /* @__PURE__ */ jsx19("button", { className: "hl-dock-zoom hl-mono", onClick: onFitView, title: "Fit view", children: pct }),
1312
+ /* @__PURE__ */ jsx19("button", { className: "hl-dock-btn", onClick: onZoomIn, title: "Zoom in", "aria-label": "Zoom in", children: /* @__PURE__ */ jsx19("span", { className: "hl-dock-glyph", children: "+" }) }),
1313
+ /* @__PURE__ */ jsx19("button", { className: "hl-dock-btn hl-dock-fit", onClick: onFitView, title: "Fit view", "aria-label": "Fit view", children: /* @__PURE__ */ jsx19(FitIcon, {}) })
882
1314
  ] });
883
1315
  }
884
1316
 
885
1317
  // src/canvas/EmptyState.tsx
886
- import { jsx as jsx18, jsxs as jsxs10 } from "react/jsx-runtime";
1318
+ import { jsx as jsx20, jsxs as jsxs10 } from "react/jsx-runtime";
887
1319
  function EmptyState({
888
1320
  title = "An empty canvas.",
889
1321
  hint = "add a source \xB7 wire it to a sink \xB7 run"
890
1322
  }) {
891
1323
  return /* @__PURE__ */ jsxs10("div", { className: "hl-empty", "aria-hidden": "true", children: [
892
- /* @__PURE__ */ jsx18("p", { className: "hl-empty-title", children: title }),
893
- /* @__PURE__ */ jsx18("p", { className: "hl-empty-hint", children: hint })
1324
+ /* @__PURE__ */ jsx20("p", { className: "hl-empty-title", children: title }),
1325
+ /* @__PURE__ */ jsx20("p", { className: "hl-empty-hint", children: hint })
894
1326
  ] });
895
1327
  }
896
1328
 
@@ -926,11 +1358,11 @@ function deriveParamSchema(descriptor, node) {
926
1358
  }
927
1359
 
928
1360
  // src/hooks/useNodeParams.ts
929
- import { useCallback as useCallback2, useMemo as useMemo2, useRef as useRef2, useSyncExternalStore as useSyncExternalStore2 } from "react";
1361
+ import { useCallback as useCallback3, useMemo as useMemo3, useRef as useRef4, useSyncExternalStore as useSyncExternalStore2 } from "react";
930
1362
  import { UpdateParamsCommand } from "@hlang-tech/editor-core";
931
1363
  function useNodeParams(core, nodeId) {
932
- const versionRef = useRef2(0);
933
- const subscribe = useCallback2(
1364
+ const versionRef = useRef4(0);
1365
+ const subscribe = useCallback3(
934
1366
  (onStoreChange) => {
935
1367
  const disposable = core.events.on("nodeChanged", (event) => {
936
1368
  if (nodeId !== void 0 && event.node.id === nodeId) {
@@ -944,9 +1376,9 @@ function useNodeParams(core, nodeId) {
944
1376
  },
945
1377
  [core, nodeId]
946
1378
  );
947
- const getSnapshot = useCallback2(() => versionRef.current, []);
1379
+ const getSnapshot = useCallback3(() => versionRef.current, []);
948
1380
  const version = useSyncExternalStore2(subscribe, getSnapshot, getSnapshot);
949
- const setParams = useCallback2(
1381
+ const setParams = useCallback3(
950
1382
  (next) => {
951
1383
  if (nodeId === void 0) {
952
1384
  return;
@@ -955,7 +1387,7 @@ function useNodeParams(core, nodeId) {
955
1387
  },
956
1388
  [core, nodeId]
957
1389
  );
958
- const setParam = useCallback2(
1390
+ const setParam = useCallback3(
959
1391
  (key, value) => {
960
1392
  if (nodeId === void 0) {
961
1393
  return;
@@ -968,7 +1400,7 @@ function useNodeParams(core, nodeId) {
968
1400
  },
969
1401
  [core, nodeId, setParams]
970
1402
  );
971
- return useMemo2(
1403
+ return useMemo3(
972
1404
  () => ({
973
1405
  node: nodeId === void 0 ? void 0 : core.model.getNode(nodeId),
974
1406
  setParam,
@@ -980,19 +1412,58 @@ function useNodeParams(core, nodeId) {
980
1412
  );
981
1413
  }
982
1414
 
1415
+ // src/hooks/useEdgeBounds.ts
1416
+ import { useCallback as useCallback4, useSyncExternalStore as useSyncExternalStore3 } from "react";
1417
+ import { SetEdgeBoundsCommand } from "@hlang-tech/editor-core";
1418
+ function useEdgeBounds(core, edgeId) {
1419
+ const subscribe = useCallback4(
1420
+ (onStoreChange) => {
1421
+ const changed = core.events.on("edgeChanged", (event) => {
1422
+ if (event.edge.id === edgeId) {
1423
+ onStoreChange();
1424
+ }
1425
+ });
1426
+ const removed = core.events.on("edgeRemoved", (event) => {
1427
+ if (event.edgeId === edgeId) {
1428
+ onStoreChange();
1429
+ }
1430
+ });
1431
+ return () => {
1432
+ changed.dispose();
1433
+ removed.dispose();
1434
+ };
1435
+ },
1436
+ [core, edgeId]
1437
+ );
1438
+ const getSnapshot = useCallback4(
1439
+ () => edgeId === void 0 ? void 0 : core.model.getEdge(edgeId),
1440
+ [core, edgeId]
1441
+ );
1442
+ const edge = useSyncExternalStore3(subscribe, getSnapshot, getSnapshot);
1443
+ const setBounds = useCallback4(
1444
+ (bounds) => {
1445
+ if (edgeId !== void 0) {
1446
+ core.commands.execute(new SetEdgeBoundsCommand(edgeId, bounds));
1447
+ }
1448
+ },
1449
+ [core, edgeId]
1450
+ );
1451
+ return { edge, setBounds };
1452
+ }
1453
+
983
1454
  // src/panels/ParamForm.tsx
984
- import { useState as useState2 } from "react";
985
- import { jsx as jsx19, jsxs as jsxs11 } from "react/jsx-runtime";
1455
+ import { useState as useState3 } from "react";
1456
+ import { jsx as jsx21, jsxs as jsxs11 } from "react/jsx-runtime";
986
1457
  function ParamForm({ fields, onCommit }) {
987
- return /* @__PURE__ */ jsx19("div", { className: "hl-form", children: fields.map((field) => /* @__PURE__ */ jsx19(FieldRow, { field, onCommit }, field.key)) });
1458
+ return /* @__PURE__ */ jsx21("div", { className: "hl-form", children: fields.map((field) => /* @__PURE__ */ jsx21(FieldRow, { field, onCommit }, field.key)) });
988
1459
  }
989
1460
  function FieldRow({
990
1461
  field,
991
1462
  onCommit
992
1463
  }) {
993
1464
  return /* @__PURE__ */ jsxs11("label", { className: "hl-field", children: [
994
- /* @__PURE__ */ jsx19("span", { className: "hl-field-label", children: field.label }),
995
- /* @__PURE__ */ jsx19(FieldControl, { field, onCommit })
1465
+ /* @__PURE__ */ jsx21("span", { className: "hl-field-label", children: field.label }),
1466
+ /* @__PURE__ */ jsx21(FieldControl, { field, onCommit })
996
1467
  ] });
997
1468
  }
998
1469
  function FieldControl({
@@ -1001,7 +1472,7 @@ function FieldControl({
1001
1472
  }) {
1002
1473
  switch (field.type) {
1003
1474
  case "toggle":
1004
- return /* @__PURE__ */ jsx19(
1475
+ return /* @__PURE__ */ jsx21(
1005
1476
  "button",
1006
1477
  {
1007
1478
  type: "button",
@@ -1010,17 +1481,17 @@ function FieldControl({
1010
1481
  className: `hl-toggle${field.value ? " is-on" : ""}`,
1011
1482
  disabled: field.disabled,
1012
1483
  onClick: () => onCommit(field.key, !field.value),
1013
- children: /* @__PURE__ */ jsx19("span", { className: "hl-toggle-knob" })
1484
+ children: /* @__PURE__ */ jsx21("span", { className: "hl-toggle-knob" })
1014
1485
  }
1015
1486
  );
1016
1487
  case "number":
1017
- return /* @__PURE__ */ jsx19(NumberControl, { field, onCommit });
1488
+ return /* @__PURE__ */ jsx21(NumberControl, { field, onCommit });
1018
1489
  case "textarea":
1019
- return /* @__PURE__ */ jsx19(TextControl, { field, onCommit, multiline: true });
1490
+ return /* @__PURE__ */ jsx21(TextControl, { field, onCommit, multiline: true });
1020
1491
  case "text":
1021
- return /* @__PURE__ */ jsx19(TextControl, { field, onCommit });
1492
+ return /* @__PURE__ */ jsx21(TextControl, { field, onCommit });
1022
1493
  case "custom":
1023
- return /* @__PURE__ */ jsx19("span", { className: "hl-field-custom hl-mono", title: field.fieldType, children: describeCustom(field.value) });
1494
+ return /* @__PURE__ */ jsx21("span", { className: "hl-field-custom hl-mono", title: field.fieldType, children: describeCustom(field.value) });
1024
1495
  }
1025
1496
  }
1026
1497
  function TextControl({
@@ -1028,7 +1499,7 @@ function TextControl({
1028
1499
  onCommit,
1029
1500
  multiline = false
1030
1501
  }) {
1031
- const [draft, setDraft] = useState2(field.value);
1502
+ const [draft, setDraft] = useState3(field.value);
1032
1503
  const commit = () => {
1033
1504
  if (draft !== field.value) {
1034
1505
  onCommit(field.key, draft);
@@ -1042,7 +1513,7 @@ function TextControl({
1042
1513
  onBlur: commit
1043
1514
  };
1044
1515
  if (multiline) {
1045
- return /* @__PURE__ */ jsx19(
1516
+ return /* @__PURE__ */ jsx21(
1046
1517
  "textarea",
1047
1518
  {
1048
1519
  ...shared,
@@ -1051,7 +1522,7 @@ function TextControl({
1051
1522
  }
1052
1523
  );
1053
1524
  }
1054
- return /* @__PURE__ */ jsx19(
1525
+ return /* @__PURE__ */ jsx21(
1055
1526
  "input",
1056
1527
  {
1057
1528
  ...shared,
@@ -1069,14 +1540,14 @@ function NumberControl({
1069
1540
  field,
1070
1541
  onCommit
1071
1542
  }) {
1072
- const [draft, setDraft] = useState2(String(field.value));
1543
+ const [draft, setDraft] = useState3(String(field.value));
1073
1544
  const commit = () => {
1074
1545
  const parsed = Number(draft);
1075
1546
  if (!Number.isNaN(parsed) && parsed !== field.value) {
1076
1547
  onCommit(field.key, parsed);
1077
1548
  }
1078
1549
  };
1079
- return /* @__PURE__ */ jsx19(
1550
+ return /* @__PURE__ */ jsx21(
1080
1551
  "input",
1081
1552
  {
1082
1553
  className: "hl-input hl-mono",
@@ -1107,40 +1578,267 @@ function describeCustom(value) {
1107
1578
  return String(value);
1108
1579
  }
1109
1580
 
1581
+ // src/panels/ConnectionInspector.tsx
1582
+ import { jsx as jsx22, jsxs as jsxs12 } from "react/jsx-runtime";
1583
+ function ConnectionInspector({ core, edgeId }) {
1584
+ const { edge, setBounds } = useEdgeBounds(core, edgeId);
1585
+ if (edge === void 0) {
1586
+ return null;
1587
+ }
1588
+ const end = (endpoint) => {
1589
+ var _a, _b;
1590
+ return `${(_b = (_a = core.model.getNode(endpoint.nodeId)) == null ? void 0 : _a.name) != null ? _b : endpoint.nodeId}.${endpoint.portId}`;
1591
+ };
1592
+ return /* @__PURE__ */ jsxs12("div", { className: "hl-inspector", children: [
1593
+ /* @__PURE__ */ jsxs12("header", { className: "hl-inspector-head", children: [
1594
+ /* @__PURE__ */ jsx22("span", { className: "hl-inspector-kind hl-connection", children: "\u2192" }),
1595
+ /* @__PURE__ */ jsxs12("div", { className: "hl-inspector-titles", children: [
1596
+ /* @__PURE__ */ jsx22("span", { className: "hl-inspector-name", children: "Connection" }),
1597
+ /* @__PURE__ */ jsxs12("span", { className: "hl-inspector-role hl-mono", children: [
1598
+ end(edge.source),
1599
+ " \u2192 ",
1600
+ end(edge.target)
1601
+ ] })
1602
+ ] })
1603
+ ] }),
1604
+ /* @__PURE__ */ jsxs12("section", { className: "hl-inspector-section", children: [
1605
+ /* @__PURE__ */ jsx22("h3", { className: "hl-inspector-section-title", children: "Back-pressure" }),
1606
+ /* @__PURE__ */ jsx22("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." }),
1607
+ /* @__PURE__ */ jsx22(
1608
+ ParamForm,
1609
+ {
1610
+ fields: boundsFields(edge),
1611
+ onCommit: (key, value) => {
1612
+ setBounds(nextBounds(edge, key, value));
1613
+ }
1614
+ }
1615
+ )
1616
+ ] })
1617
+ ] });
1618
+ }
1619
+
1620
+ // src/live/format.ts
1621
+ function formatDuration(ms) {
1622
+ if (ms === 0) {
1623
+ return "0 ms";
1624
+ }
1625
+ if (ms < 0.01) {
1626
+ return "<0.01 ms";
1627
+ }
1628
+ if (ms < 1) {
1629
+ return `${ms.toFixed(2)} ms`;
1630
+ }
1631
+ if (ms < 10) {
1632
+ return `${ms.toFixed(1)} ms`;
1633
+ }
1634
+ if (ms < 1e3) {
1635
+ return `${Math.round(ms).toString()} ms`;
1636
+ }
1637
+ return `${(ms / 1e3).toFixed(2)} s`;
1638
+ }
1639
+ function formatElapsed(ms) {
1640
+ return `${(ms / 1e3).toFixed(3)}s`;
1641
+ }
1642
+
1643
+ // src/live/preview.ts
1644
+ var PREVIEW_LENGTH = 120;
1645
+ var CIRCULAR = "[Circular]";
1646
+ function functionPreview(value) {
1647
+ return `\u0192 ${value.name === "" ? "anonymous" : value.name}`;
1648
+ }
1649
+ function bigintPreview(value) {
1650
+ return `${value.toString()}n`;
1651
+ }
1652
+ function className(value) {
1653
+ const prototype = Object.getPrototypeOf(value);
1654
+ if (prototype === null || prototype === Object.prototype) {
1655
+ return "Object";
1656
+ }
1657
+ const name = typeof prototype === "object" && "constructor" in prototype ? prototype.constructor : void 0;
1658
+ return typeof name === "function" && name.name !== "" ? name.name : "Object";
1659
+ }
1660
+ function nestedPreview(value) {
1661
+ if (typeof value === "bigint") {
1662
+ return bigintPreview(value);
1663
+ }
1664
+ if (typeof value === "function") {
1665
+ return functionPreview(value);
1666
+ }
1667
+ if (typeof value === "symbol") {
1668
+ return value.toString();
1669
+ }
1670
+ if (value instanceof Error) {
1671
+ return `${value.name}: ${value.message}`;
1672
+ }
1673
+ if (value instanceof Map || value instanceof Set) {
1674
+ return `${value instanceof Map ? "Map" : "Set"}(${String(value.size)})`;
1675
+ }
1676
+ return value;
1677
+ }
1678
+ function json(value) {
1679
+ const ancestors = [];
1680
+ return JSON.stringify(value, function replace(_key, nested) {
1681
+ while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this) {
1682
+ ancestors.pop();
1683
+ }
1684
+ if (typeof nested === "object" && nested !== null) {
1685
+ if (ancestors.includes(nested)) {
1686
+ return CIRCULAR;
1687
+ }
1688
+ ancestors.push(nested);
1689
+ }
1690
+ return nestedPreview(nested);
1691
+ });
1692
+ }
1693
+ function fullPreview(value) {
1694
+ switch (typeof value) {
1695
+ case "string":
1696
+ return JSON.stringify(value);
1697
+ case "bigint":
1698
+ case "function":
1699
+ case "symbol": {
1700
+ const preview = nestedPreview(value);
1701
+ return typeof preview === "string" ? preview : String(preview);
1702
+ }
1703
+ case "object":
1704
+ break;
1705
+ default:
1706
+ return String(value);
1707
+ }
1708
+ if (value === null) {
1709
+ return "null";
1710
+ }
1711
+ if (value instanceof Error || value instanceof Map || value instanceof Set) {
1712
+ return String(nestedPreview(value));
1713
+ }
1714
+ if (value instanceof Date) {
1715
+ return Number.isNaN(value.getTime()) ? "Invalid Date" : value.toISOString();
1716
+ }
1717
+ const name = Array.isArray(value) ? "Array" : className(value);
1718
+ let body;
1719
+ try {
1720
+ body = json(value);
1721
+ } catch {
1722
+ return name;
1723
+ }
1724
+ if (body === void 0) {
1725
+ return name;
1726
+ }
1727
+ return name === "Object" || name === "Array" ? body : `${name} ${body}`;
1728
+ }
1729
+ function previewValue(value, maxLength = PREVIEW_LENGTH) {
1730
+ const preview = fullPreview(value);
1731
+ return preview.length <= maxLength ? preview : `${preview.slice(0, maxLength - 1)}\u2026`;
1732
+ }
1733
+
1734
+ // src/live/RunTraceContext.tsx
1735
+ import { createContext as createContext3, useContext as useContext3, useSyncExternalStore as useSyncExternalStore4 } from "react";
1736
+ import { jsx as jsx23 } from "react/jsx-runtime";
1737
+ var RunTraceContext = createContext3(null);
1738
+ function RunTraceProvider({ trace, children }) {
1739
+ return /* @__PURE__ */ jsx23(RunTraceContext.Provider, { value: trace, children });
1740
+ }
1741
+ function useRunTraceStore() {
1742
+ return useContext3(RunTraceContext);
1743
+ }
1744
+ function useRunTraceVersion(trace) {
1745
+ return useSyncExternalStore4(
1746
+ (onChange) => trace === null ? () => void 0 : trace.subscribe(onChange),
1747
+ () => trace === null ? 0 : trace.getVersion(),
1748
+ () => 0
1749
+ );
1750
+ }
1751
+
1752
+ // src/panels/NodeRunStats.tsx
1753
+ import { jsx as jsx24, jsxs as jsxs13 } from "react/jsx-runtime";
1754
+ function NodeRunStats({ nodeId }) {
1755
+ const trace = useRunTraceStore();
1756
+ useRunTraceVersion(trace);
1757
+ if (trace === null) {
1758
+ return null;
1759
+ }
1760
+ const stats = trace.statsOf(nodeId);
1761
+ return /* @__PURE__ */ jsxs13("section", { className: "hl-inspector-section", children: [
1762
+ /* @__PURE__ */ jsx24("h3", { className: "hl-inspector-section-title", children: "Run" }),
1763
+ stats === void 0 ? /* @__PURE__ */ jsx24("p", { className: "hl-inspector-hint hl-runstats-hint", children: "No activity in this run." }) : /* @__PURE__ */ jsxs13("dl", { className: "hl-runstats", children: [
1764
+ /* @__PURE__ */ jsxs13("div", { className: "hl-runstats-cell", children: [
1765
+ /* @__PURE__ */ jsx24("dt", { children: "In" }),
1766
+ /* @__PURE__ */ jsx24("dd", { className: "hl-mono", children: stats.received })
1767
+ ] }),
1768
+ /* @__PURE__ */ jsxs13("div", { className: "hl-runstats-cell", children: [
1769
+ /* @__PURE__ */ jsx24("dt", { children: "Out" }),
1770
+ /* @__PURE__ */ jsx24("dd", { className: "hl-mono", children: stats.sent })
1771
+ ] }),
1772
+ /* @__PURE__ */ jsxs13("div", { className: `hl-runstats-cell${stats.errors > 0 ? " is-error" : ""}`, children: [
1773
+ /* @__PURE__ */ jsx24("dt", { children: "Errors" }),
1774
+ /* @__PURE__ */ jsx24("dd", { className: "hl-mono", children: stats.errors })
1775
+ ] }),
1776
+ /* @__PURE__ */ jsxs13("div", { className: "hl-runstats-cell", children: [
1777
+ /* @__PURE__ */ jsx24("dt", { children: "Avg" }),
1778
+ /* @__PURE__ */ jsx24("dd", { className: "hl-mono", children: stats.received === 0 ? "\u2014" : formatDuration(stats.totalMs / stats.received) })
1779
+ ] }),
1780
+ /* @__PURE__ */ jsxs13("div", { className: "hl-runstats-cell", children: [
1781
+ /* @__PURE__ */ jsx24("dt", { children: "Max" }),
1782
+ /* @__PURE__ */ jsx24("dd", { className: "hl-mono", children: stats.received === 0 ? "\u2014" : formatDuration(stats.maxMs) })
1783
+ ] }),
1784
+ stats.dropped > 0 && /* @__PURE__ */ jsxs13("div", { className: "hl-runstats-cell is-warning", children: [
1785
+ /* @__PURE__ */ jsx24("dt", { children: "Dropped" }),
1786
+ /* @__PURE__ */ jsx24("dd", { className: "hl-mono", children: stats.dropped })
1787
+ ] }),
1788
+ stats.finished && /* @__PURE__ */ jsxs13("div", { className: "hl-runstats-cell is-done", children: [
1789
+ /* @__PURE__ */ jsx24("dt", { children: "State" }),
1790
+ /* @__PURE__ */ jsx24("dd", { children: "Finished" })
1791
+ ] }),
1792
+ stats.received > 0 && /* @__PURE__ */ jsxs13("div", { className: "hl-runstats-cell is-wide", children: [
1793
+ /* @__PURE__ */ jsx24("dt", { children: "Last in" }),
1794
+ /* @__PURE__ */ jsx24("dd", { className: "hl-mono", children: previewValue(stats.lastReceived) })
1795
+ ] }),
1796
+ stats.sent > 0 && /* @__PURE__ */ jsxs13("div", { className: "hl-runstats-cell is-wide", children: [
1797
+ /* @__PURE__ */ jsx24("dt", { children: "Last out" }),
1798
+ /* @__PURE__ */ jsx24("dd", { className: "hl-mono", children: previewValue(stats.lastSent) })
1799
+ ] })
1800
+ ] })
1801
+ ] });
1802
+ }
1803
+
1110
1804
  // src/panels/InspectorPanel.tsx
1111
- import { jsx as jsx20, jsxs as jsxs12 } from "react/jsx-runtime";
1805
+ import { jsx as jsx25, jsxs as jsxs14 } from "react/jsx-runtime";
1112
1806
  function InspectorPanel({ core, emptyState }) {
1113
1807
  const selection = useSelection(core);
1114
1808
  const selectedNodes2 = selection.selectedNodes;
1115
1809
  const singleId = selectedNodes2.length === 1 ? selectedNodes2[0] : void 0;
1116
1810
  const params = useNodeParams(core, singleId);
1117
1811
  const node = params.node;
1812
+ const singleEdgeId = selectedNodes2.length === 0 && selection.selectedEdges.length === 1 ? selection.selectedEdges[0] : void 0;
1813
+ if (node === void 0 && singleEdgeId !== void 0) {
1814
+ return /* @__PURE__ */ jsx25(ConnectionInspector, { core, edgeId: singleEdgeId });
1815
+ }
1118
1816
  if (node === void 0) {
1119
- return /* @__PURE__ */ jsx20("div", { className: "hl-inspector hl-inspector-empty", children: emptyState != null ? emptyState : /* @__PURE__ */ jsxs12("div", { className: "hl-inspector-blank", children: [
1120
- /* @__PURE__ */ jsx20("span", { className: "hl-inspector-blank-badge", "aria-hidden": true }),
1121
- /* @__PURE__ */ jsx20("p", { className: "hl-inspector-blank-title", children: selectedNodes2.length > 1 ? `${String(selectedNodes2.length)} nodes selected` : "Nothing selected" }),
1122
- /* @__PURE__ */ jsx20("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." })
1817
+ return /* @__PURE__ */ jsx25("div", { className: "hl-inspector hl-inspector-empty", children: emptyState != null ? emptyState : /* @__PURE__ */ jsxs14("div", { className: "hl-inspector-blank", children: [
1818
+ /* @__PURE__ */ jsx25("span", { className: "hl-inspector-blank-badge", "aria-hidden": true }),
1819
+ /* @__PURE__ */ jsx25("p", { className: "hl-inspector-blank-title", children: selectedNodes2.length > 1 ? `${String(selectedNodes2.length)} nodes selected` : "Nothing selected" }),
1820
+ /* @__PURE__ */ jsx25("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." })
1123
1821
  ] }) });
1124
1822
  }
1125
1823
  const descriptor = core.registry.get(node.code);
1126
1824
  const fields = descriptor === void 0 ? [] : deriveParamSchema(descriptor, node);
1127
1825
  const imageUrl = typeof node.params.imageUrl === "string" && node.params.imageUrl.length > 0 ? node.params.imageUrl : void 0;
1128
- return /* @__PURE__ */ jsxs12("div", { className: "hl-inspector", children: [
1129
- /* @__PURE__ */ jsx20("div", { className: `hl-inspector-preview hl-${node.kind.toLowerCase()}`, children: imageUrl !== void 0 ? /* @__PURE__ */ jsx20("img", { className: "hl-inspector-preview-img", src: imageUrl, alt: node.name }) : /* @__PURE__ */ jsx20("span", { className: "hl-inspector-preview-badge hl-mono", children: node.kind }) }),
1130
- /* @__PURE__ */ jsxs12("header", { className: "hl-inspector-head", children: [
1131
- /* @__PURE__ */ jsx20("span", { className: `hl-inspector-kind hl-${node.kind.toLowerCase()}`, children: node.kind }),
1132
- /* @__PURE__ */ jsxs12("div", { className: "hl-inspector-titles", children: [
1133
- /* @__PURE__ */ jsx20("span", { className: "hl-inspector-name", children: node.name }),
1134
- /* @__PURE__ */ jsxs12("span", { className: "hl-inspector-role hl-mono", children: [
1826
+ return /* @__PURE__ */ jsxs14("div", { className: "hl-inspector", children: [
1827
+ /* @__PURE__ */ jsx25("div", { className: `hl-inspector-preview hl-${node.kind.toLowerCase()}`, children: imageUrl !== void 0 ? /* @__PURE__ */ jsx25("img", { className: "hl-inspector-preview-img", src: imageUrl, alt: node.name }) : /* @__PURE__ */ jsx25("span", { className: "hl-inspector-preview-badge hl-mono", children: node.kind }) }),
1828
+ /* @__PURE__ */ jsxs14("header", { className: "hl-inspector-head", children: [
1829
+ /* @__PURE__ */ jsx25("span", { className: `hl-inspector-kind hl-${node.kind.toLowerCase()}`, children: node.kind }),
1830
+ /* @__PURE__ */ jsxs14("div", { className: "hl-inspector-titles", children: [
1831
+ /* @__PURE__ */ jsx25("span", { className: "hl-inspector-name", children: node.name }),
1832
+ /* @__PURE__ */ jsxs14("span", { className: "hl-inspector-role hl-mono", children: [
1135
1833
  node.code,
1136
1834
  " \xB7 ",
1137
1835
  KIND_ROLE[node.kind]
1138
1836
  ] })
1139
1837
  ] })
1140
1838
  ] }),
1141
- /* @__PURE__ */ jsxs12("section", { className: "hl-inspector-section", children: [
1142
- /* @__PURE__ */ jsx20("h3", { className: "hl-inspector-section-title", children: "Parameters" }),
1143
- fields.length === 0 ? /* @__PURE__ */ jsx20("p", { className: "hl-inspector-hint", children: "This node has no parameters." }) : /* @__PURE__ */ jsx20(
1839
+ /* @__PURE__ */ jsxs14("section", { className: "hl-inspector-section", children: [
1840
+ /* @__PURE__ */ jsx25("h3", { className: "hl-inspector-section-title", children: "Parameters" }),
1841
+ fields.length === 0 ? /* @__PURE__ */ jsx25("p", { className: "hl-inspector-hint", children: "This node has no parameters." }) : /* @__PURE__ */ jsx25(
1144
1842
  ParamForm,
1145
1843
  {
1146
1844
  fields,
@@ -1148,16 +1846,17 @@ function InspectorPanel({ core, emptyState }) {
1148
1846
  }
1149
1847
  )
1150
1848
  ] }),
1151
- /* @__PURE__ */ jsxs12("section", { className: "hl-inspector-section", children: [
1152
- /* @__PURE__ */ jsx20("h3", { className: "hl-inspector-section-title", children: "Ports" }),
1153
- /* @__PURE__ */ jsx20("ul", { className: "hl-inspector-ports", children: node.ports.map((port) => /* @__PURE__ */ jsxs12(
1849
+ /* @__PURE__ */ jsx25(NodeRunStats, { nodeId: node.id }),
1850
+ /* @__PURE__ */ jsxs14("section", { className: "hl-inspector-section", children: [
1851
+ /* @__PURE__ */ jsx25("h3", { className: "hl-inspector-section-title", children: "Ports" }),
1852
+ /* @__PURE__ */ jsx25("ul", { className: "hl-inspector-ports", children: node.ports.map((port) => /* @__PURE__ */ jsxs14(
1154
1853
  "li",
1155
1854
  {
1156
1855
  className: `hl-inspector-port${port.isError ? " is-error" : ""}`,
1157
1856
  children: [
1158
- /* @__PURE__ */ jsx20("span", { className: `hl-port-dot hl-dir-${port.direction.toLowerCase()}` }),
1159
- /* @__PURE__ */ jsx20("span", { className: "hl-mono", children: port.name }),
1160
- /* @__PURE__ */ jsx20("span", { className: "hl-inspector-port-dir hl-mono", children: port.direction })
1857
+ /* @__PURE__ */ jsx25("span", { className: `hl-port-dot hl-dir-${port.direction.toLowerCase()}` }),
1858
+ /* @__PURE__ */ jsx25("span", { className: "hl-mono", children: port.name }),
1859
+ /* @__PURE__ */ jsx25("span", { className: "hl-inspector-port-dir hl-mono", children: port.direction })
1161
1860
  ]
1162
1861
  },
1163
1862
  port.id
@@ -1167,14 +1866,14 @@ function InspectorPanel({ core, emptyState }) {
1167
1866
  }
1168
1867
 
1169
1868
  // src/panels/OutlineTree.tsx
1170
- import { useCallback as useCallback3, useEffect as useEffect2, useMemo as useMemo3, useState as useState3 } from "react";
1869
+ import { useCallback as useCallback5, useEffect as useEffect3, useMemo as useMemo4, useState as useState4 } from "react";
1171
1870
  import { GraphTopology } from "@hlang-tech/editor-core";
1172
1871
  import { useSelection as useSelection2, useViewState, useDiagnostics } from "@hlang-tech/editor-react";
1173
- import { jsx as jsx21, jsxs as jsxs13 } from "react/jsx-runtime";
1872
+ import { jsx as jsx26, jsxs as jsxs15 } from "react/jsx-runtime";
1174
1873
  function useTopologyComponents(core) {
1175
- const [tick, setTick] = useState3(0);
1176
- const bump = useCallback3(() => setTick((t) => t + 1), []);
1177
- useEffect2(() => {
1874
+ const [tick, setTick] = useState4(0);
1875
+ const bump = useCallback5(() => setTick((t) => t + 1), []);
1876
+ useEffect3(() => {
1178
1877
  const kinds = ["nodeAdded", "nodeRemoved", "nodeChanged", "edgeAdded", "edgeRemoved"];
1179
1878
  const disposers = kinds.map((k) => core.events.on(k, bump));
1180
1879
  bump();
@@ -1184,7 +1883,7 @@ function useTopologyComponents(core) {
1184
1883
  }
1185
1884
  };
1186
1885
  }, [core, bump]);
1187
- return useMemo3(() => {
1886
+ return useMemo4(() => {
1188
1887
  void tick;
1189
1888
  const topo = new GraphTopology();
1190
1889
  topo.reset(core.model.getNodes(), core.model.getEdges());
@@ -1196,15 +1895,15 @@ function OutlineTree({ core, onFocus }) {
1196
1895
  const selection = useSelection2(core);
1197
1896
  const viewState = useViewState(core);
1198
1897
  const diagnostics = useDiagnostics(core);
1199
- const [collapsed, setCollapsed] = useState3(/* @__PURE__ */ new Set());
1200
- const nodeById = useMemo3(() => {
1898
+ const [collapsed, setCollapsed] = useState4(/* @__PURE__ */ new Set());
1899
+ const nodeById = useMemo4(() => {
1201
1900
  const m = /* @__PURE__ */ new Map();
1202
1901
  for (const n of core.model.getNodes()) {
1203
1902
  m.set(n.id, n);
1204
1903
  }
1205
1904
  return m;
1206
1905
  }, [core, components]);
1207
- const toggleGroup = useCallback3((id) => {
1906
+ const toggleGroup = useCallback5((id) => {
1208
1907
  setCollapsed((prev) => {
1209
1908
  const next = new Set(prev);
1210
1909
  if (next.has(id)) {
@@ -1215,7 +1914,7 @@ function OutlineTree({ core, onFocus }) {
1215
1914
  return next;
1216
1915
  });
1217
1916
  }, []);
1218
- const select = useCallback3(
1917
+ const select = useCallback5(
1219
1918
  (id) => {
1220
1919
  selection.selectNodes([id], "replace");
1221
1920
  onFocus == null ? void 0 : onFocus(id);
@@ -1223,79 +1922,80 @@ function OutlineTree({ core, onFocus }) {
1223
1922
  [selection, onFocus]
1224
1923
  );
1225
1924
  if (components.length === 0) {
1226
- return /* @__PURE__ */ jsx21("p", { className: "hl-outline-empty hl-mono", children: "no layers yet" });
1925
+ return /* @__PURE__ */ jsx26("p", { className: "hl-outline-empty hl-mono", children: "no layers yet" });
1227
1926
  }
1228
1927
  const groups = [...components].sort((a, b) => b.nodeIds.size - a.nodeIds.size);
1229
- return /* @__PURE__ */ jsx21("div", { className: "hl-outline", role: "tree", "aria-label": "Outline", children: groups.map((group, gi) => {
1928
+ return /* @__PURE__ */ jsx26("div", { className: "hl-outline", role: "tree", "aria-label": "Outline", children: groups.map((group, gi) => {
1230
1929
  const members = [...group.nodeIds].map((id) => nodeById.get(id)).filter((n) => n !== void 0);
1231
1930
  const open = !collapsed.has(group.id);
1232
1931
  const groupName = `Flow ${String(gi + 1)}`;
1233
- return /* @__PURE__ */ jsxs13("div", { className: "hl-outline-group", role: "treeitem", "aria-expanded": open, children: [
1234
- /* @__PURE__ */ jsxs13(
1932
+ return /* @__PURE__ */ jsxs15("div", { className: "hl-outline-group", role: "treeitem", "aria-expanded": open, children: [
1933
+ /* @__PURE__ */ jsxs15(
1235
1934
  "button",
1236
1935
  {
1237
1936
  className: "hl-outline-grouphead",
1238
1937
  onClick: () => toggleGroup(group.id),
1239
1938
  children: [
1240
- /* @__PURE__ */ jsx21("span", { className: "hl-outline-caret" + (open ? " is-open" : ""), "aria-hidden": true, children: "\u25B8" }),
1241
- /* @__PURE__ */ jsx21("span", { className: "hl-outline-groupname", children: groupName }),
1242
- /* @__PURE__ */ jsx21("span", { className: "hl-outline-count hl-mono", children: group.nodeIds.size })
1939
+ /* @__PURE__ */ jsx26("span", { className: "hl-outline-caret" + (open ? " is-open" : ""), "aria-hidden": true, children: "\u25B8" }),
1940
+ /* @__PURE__ */ jsx26("span", { className: "hl-outline-groupname", children: groupName }),
1941
+ /* @__PURE__ */ jsx26("span", { className: "hl-outline-count hl-mono", children: group.nodeIds.size })
1243
1942
  ]
1244
1943
  }
1245
1944
  ),
1246
- open && /* @__PURE__ */ jsx21("ul", { className: "hl-outline-members", children: members.map((node) => {
1945
+ open && /* @__PURE__ */ jsx26("ul", { className: "hl-outline-members", children: members.map((node) => {
1247
1946
  const active = selection.isSelected(node.id);
1248
1947
  const isHidden = viewState.isHidden(node.id);
1249
1948
  const isLocked = viewState.isLocked(node.id);
1250
- const hasImage = typeof node.params.imageUrl === "string" && node.params.imageUrl.length > 0;
1251
- return /* @__PURE__ */ jsx21("li", { children: /* @__PURE__ */ jsxs13(
1949
+ const { imageUrl } = node.params;
1950
+ const thumbnail = typeof imageUrl === "string" && imageUrl.length > 0 ? imageUrl : void 0;
1951
+ return /* @__PURE__ */ jsx26("li", { children: /* @__PURE__ */ jsxs15(
1252
1952
  "div",
1253
1953
  {
1254
1954
  className: "hl-outline-row" + (active ? " is-active" : "") + (isHidden ? " is-hidden" : "") + (isLocked ? " is-locked" : ""),
1255
1955
  children: [
1256
- /* @__PURE__ */ jsxs13("button", { className: "hl-outline-main", onClick: () => select(node.id), children: [
1257
- /* @__PURE__ */ jsx21(
1956
+ /* @__PURE__ */ jsxs15("button", { className: "hl-outline-main", onClick: () => select(node.id), children: [
1957
+ /* @__PURE__ */ jsx26(
1258
1958
  "span",
1259
1959
  {
1260
1960
  className: `hl-outline-icon hl-kind-${node.kind.toLowerCase()}`,
1261
1961
  "aria-hidden": true,
1262
- children: hasImage ? /* @__PURE__ */ jsx21(
1962
+ children: thumbnail !== void 0 ? /* @__PURE__ */ jsx26(
1263
1963
  "img",
1264
1964
  {
1265
1965
  className: "hl-outline-thumb",
1266
- src: node.params.imageUrl,
1966
+ src: thumbnail,
1267
1967
  alt: ""
1268
1968
  }
1269
- ) : /* @__PURE__ */ jsx21(NodeIcon, { code: node.code, kind: node.kind, size: 13 })
1969
+ ) : /* @__PURE__ */ jsx26(NodeIcon, { code: node.code, kind: node.kind, size: 13 })
1270
1970
  }
1271
1971
  ),
1272
- /* @__PURE__ */ jsx21("span", { className: "hl-outline-name", children: node.name }),
1972
+ /* @__PURE__ */ jsx26("span", { className: "hl-outline-name", children: node.name }),
1273
1973
  (() => {
1274
1974
  const diag = diagnostics.worstOf(node.id);
1275
- return diag === void 0 ? null : /* @__PURE__ */ jsx21(DiagnosticBadge, { diagnostic: diag, variant: "hl-outline-diag", size: 11 });
1975
+ return diag === void 0 ? null : /* @__PURE__ */ jsx26(DiagnosticBadge, { diagnostic: diag, variant: "hl-outline-diag", size: 11 });
1276
1976
  })(),
1277
- isLocked && /* @__PURE__ */ jsx21("span", { className: "hl-outline-badge", "aria-hidden": true, children: /* @__PURE__ */ jsx21(LockIcon, { size: 11 }) }),
1278
- isHidden && /* @__PURE__ */ jsx21("span", { className: "hl-outline-badge", "aria-hidden": true, children: /* @__PURE__ */ jsx21(EyeOffIcon, { size: 11 }) })
1977
+ isLocked && /* @__PURE__ */ jsx26("span", { className: "hl-outline-badge", "aria-hidden": true, children: /* @__PURE__ */ jsx26(LockIcon, { size: 11 }) }),
1978
+ isHidden && /* @__PURE__ */ jsx26("span", { className: "hl-outline-badge", "aria-hidden": true, children: /* @__PURE__ */ jsx26(EyeOffIcon, { size: 11 }) })
1279
1979
  ] }),
1280
- /* @__PURE__ */ jsxs13("span", { className: "hl-outline-controls", children: [
1281
- /* @__PURE__ */ jsx21(
1980
+ /* @__PURE__ */ jsxs15("span", { className: "hl-outline-controls", children: [
1981
+ /* @__PURE__ */ jsx26(
1282
1982
  "button",
1283
1983
  {
1284
1984
  className: "hl-outline-ctl" + (isHidden ? " is-active-ctl" : ""),
1285
1985
  onClick: () => viewState.toggleHidden(node.id),
1286
1986
  "aria-label": isHidden ? "Show layer" : "Hide layer",
1287
1987
  title: isHidden ? "Show" : "Hide",
1288
- children: isHidden ? /* @__PURE__ */ jsx21(EyeOffIcon, {}) : /* @__PURE__ */ jsx21(EyeIcon, {})
1988
+ children: isHidden ? /* @__PURE__ */ jsx26(EyeOffIcon, {}) : /* @__PURE__ */ jsx26(EyeIcon, {})
1289
1989
  }
1290
1990
  ),
1291
- /* @__PURE__ */ jsx21(
1991
+ /* @__PURE__ */ jsx26(
1292
1992
  "button",
1293
1993
  {
1294
1994
  className: "hl-outline-ctl" + (isLocked ? " is-active-ctl" : ""),
1295
1995
  onClick: () => viewState.toggleLocked(node.id),
1296
1996
  "aria-label": isLocked ? "Unlock layer" : "Lock layer",
1297
1997
  title: isLocked ? "Unlock" : "Lock",
1298
- children: isLocked ? /* @__PURE__ */ jsx21(LockIcon, {}) : /* @__PURE__ */ jsx21(UnlockIcon, {})
1998
+ children: isLocked ? /* @__PURE__ */ jsx26(LockIcon, {}) : /* @__PURE__ */ jsx26(UnlockIcon, {})
1299
1999
  }
1300
2000
  )
1301
2001
  ] })
@@ -1308,21 +2008,21 @@ function OutlineTree({ core, onFocus }) {
1308
2008
  }
1309
2009
 
1310
2010
  // src/panels/PreviewPanel.tsx
1311
- import { jsx as jsx22, jsxs as jsxs14 } from "react/jsx-runtime";
2011
+ import { jsx as jsx27, jsxs as jsxs16 } from "react/jsx-runtime";
1312
2012
  function PreviewPanel({ imageUrl, title = "Preview", status }) {
1313
- return /* @__PURE__ */ jsxs14("section", { className: "hl-preview", "aria-label": "Preview", children: [
1314
- /* @__PURE__ */ jsxs14("div", { className: "hl-preview-head", children: [
1315
- /* @__PURE__ */ jsx22("span", { className: "hl-preview-title", children: title }),
1316
- status !== void 0 && /* @__PURE__ */ jsx22("span", { className: "hl-preview-status hl-mono", children: status })
2013
+ return /* @__PURE__ */ jsxs16("section", { className: "hl-preview", "aria-label": "Preview", children: [
2014
+ /* @__PURE__ */ jsxs16("div", { className: "hl-preview-head", children: [
2015
+ /* @__PURE__ */ jsx27("span", { className: "hl-preview-title", children: title }),
2016
+ status !== void 0 && /* @__PURE__ */ jsx27("span", { className: "hl-preview-status hl-mono", children: status })
1317
2017
  ] }),
1318
- /* @__PURE__ */ jsx22("div", { className: "hl-preview-frame", children: imageUrl !== void 0 && imageUrl.length > 0 ? /* @__PURE__ */ jsx22("img", { className: "hl-preview-img", src: imageUrl, alt: title, draggable: false }) : /* @__PURE__ */ jsx22("div", { className: "hl-preview-placeholder hl-mono", children: "no output yet" }) })
2018
+ /* @__PURE__ */ jsx27("div", { className: "hl-preview-frame", children: imageUrl !== void 0 && imageUrl.length > 0 ? /* @__PURE__ */ jsx27("img", { className: "hl-preview-img", src: imageUrl, alt: title, draggable: false }) : /* @__PURE__ */ jsx27("div", { className: "hl-preview-placeholder hl-mono", children: "no output yet" }) })
1319
2019
  ] });
1320
2020
  }
1321
2021
 
1322
2022
  // src/panels/RailTabs.tsx
1323
- import { jsx as jsx23 } from "react/jsx-runtime";
2023
+ import { jsx as jsx28 } from "react/jsx-runtime";
1324
2024
  function RailTabs({ tabs, active, onChange }) {
1325
- return /* @__PURE__ */ jsx23("div", { className: "hl-railtabs", role: "tablist", children: tabs.map((tab) => /* @__PURE__ */ jsx23(
2025
+ return /* @__PURE__ */ jsx28("div", { className: "hl-railtabs", role: "tablist", children: tabs.map((tab) => /* @__PURE__ */ jsx28(
1326
2026
  "button",
1327
2027
  {
1328
2028
  role: "tab",
@@ -1336,20 +2036,20 @@ function RailTabs({ tabs, active, onChange }) {
1336
2036
  }
1337
2037
 
1338
2038
  // src/panels/AssetGrid.tsx
1339
- import { jsx as jsx24 } from "react/jsx-runtime";
2039
+ import { jsx as jsx29 } from "react/jsx-runtime";
1340
2040
  function AssetGrid({ items, onSelect, emptyHint = "No assets yet" }) {
1341
2041
  if (items.length === 0) {
1342
- return /* @__PURE__ */ jsx24("p", { className: "hl-assetgrid-empty hl-mono", children: emptyHint });
2042
+ return /* @__PURE__ */ jsx29("p", { className: "hl-assetgrid-empty hl-mono", children: emptyHint });
1343
2043
  }
1344
- return /* @__PURE__ */ jsx24("div", { className: "hl-assetgrid", children: items.map((item) => {
2044
+ return /* @__PURE__ */ jsx29("div", { className: "hl-assetgrid", children: items.map((item) => {
1345
2045
  var _a;
1346
- return /* @__PURE__ */ jsx24(
2046
+ return /* @__PURE__ */ jsx29(
1347
2047
  "button",
1348
2048
  {
1349
2049
  className: "hl-assetgrid-cell",
1350
2050
  onClick: () => onSelect == null ? void 0 : onSelect(item.id),
1351
2051
  title: item.label,
1352
- children: /* @__PURE__ */ jsx24("img", { className: "hl-assetgrid-img", src: item.url, alt: (_a = item.label) != null ? _a : "", loading: "lazy" })
2052
+ children: /* @__PURE__ */ jsx29("img", { className: "hl-assetgrid-img", src: item.url, alt: (_a = item.label) != null ? _a : "", loading: "lazy" })
1353
2053
  },
1354
2054
  item.id
1355
2055
  );
@@ -1357,10 +2057,10 @@ function AssetGrid({ items, onSelect, emptyHint = "No assets yet" }) {
1357
2057
  }
1358
2058
 
1359
2059
  // src/panels/PositionPanel.tsx
1360
- import { useCallback as useCallback4, useEffect as useEffect3, useReducer } from "react";
2060
+ import { useCallback as useCallback6, useEffect as useEffect4, useReducer } from "react";
1361
2061
  import { MoveNodeCommand } from "@hlang-tech/editor-core";
1362
2062
  import { useSelection as useSelection3 } from "@hlang-tech/editor-react";
1363
- import { jsx as jsx25, jsxs as jsxs15 } from "react/jsx-runtime";
2063
+ import { jsx as jsx30, jsxs as jsxs17 } from "react/jsx-runtime";
1364
2064
  function selectedNodes(core) {
1365
2065
  const ids = core.selection.getSelectedNodes();
1366
2066
  const out = [];
@@ -1376,7 +2076,7 @@ function PositionPanel({ core, livePosition }) {
1376
2076
  var _a;
1377
2077
  useSelection3(core);
1378
2078
  const [, bump] = useReducer((n) => n + 1, 0);
1379
- useEffect3(() => {
2079
+ useEffect4(() => {
1380
2080
  const kinds = ["nodeChanged", "nodeAdded", "nodeRemoved"];
1381
2081
  const disposers = kinds.map((k) => core.events.on(k, bump));
1382
2082
  return () => {
@@ -1388,7 +2088,7 @@ function PositionPanel({ core, livePosition }) {
1388
2088
  const nodes = selectedNodes(core);
1389
2089
  const single = nodes.length === 1 ? nodes[0] : void 0;
1390
2090
  const shownPos = single !== void 0 ? (_a = livePosition == null ? void 0 : livePosition(single.id)) != null ? _a : single.position : void 0;
1391
- const moveAll = useCallback4(
2091
+ const moveAll = useCallback6(
1392
2092
  (fn) => {
1393
2093
  const targets = selectedNodes(core);
1394
2094
  if (targets.length === 0) {
@@ -1402,7 +2102,7 @@ function PositionPanel({ core, livePosition }) {
1402
2102
  },
1403
2103
  [core]
1404
2104
  );
1405
- const align = useCallback4(
2105
+ const align = useCallback6(
1406
2106
  (kind) => {
1407
2107
  const targets = selectedNodes(core);
1408
2108
  if (targets.length === 0) {
@@ -1435,7 +2135,7 @@ function PositionPanel({ core, livePosition }) {
1435
2135
  },
1436
2136
  [core, moveAll]
1437
2137
  );
1438
- const setCoord = useCallback4(
2138
+ const setCoord = useCallback6(
1439
2139
  (axis, value) => {
1440
2140
  if (single === void 0 || Number.isNaN(value)) {
1441
2141
  return;
@@ -1454,9 +2154,9 @@ function PositionPanel({ core, livePosition }) {
1454
2154
  { kind: "vcenter", label: "Align middle", d: "M4 12h16M9 8v8M15 9v6" },
1455
2155
  { kind: "bottom", label: "Align bottom", d: "M4 19h16M8 7v8M16 12v3" }
1456
2156
  ];
1457
- return /* @__PURE__ */ jsxs15("section", { className: "hl-position", children: [
1458
- /* @__PURE__ */ jsx25("h3", { className: "hl-position-title hl-mono", children: "Position" }),
1459
- /* @__PURE__ */ jsx25("div", { className: "hl-position-align", role: "group", "aria-label": "Align", children: alignButtons.map((b) => /* @__PURE__ */ jsx25(
2157
+ return /* @__PURE__ */ jsxs17("section", { className: "hl-position", children: [
2158
+ /* @__PURE__ */ jsx30("h3", { className: "hl-position-title hl-mono", children: "Position" }),
2159
+ /* @__PURE__ */ jsx30("div", { className: "hl-position-align", role: "group", "aria-label": "Align", children: alignButtons.map((b) => /* @__PURE__ */ jsx30(
1460
2160
  "button",
1461
2161
  {
1462
2162
  className: "hl-position-alignbtn",
@@ -1464,13 +2164,13 @@ function PositionPanel({ core, livePosition }) {
1464
2164
  disabled,
1465
2165
  title: b.label,
1466
2166
  "aria-label": b.label,
1467
- children: /* @__PURE__ */ jsx25("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__ */ jsx25("path", { d: b.d }) })
2167
+ children: /* @__PURE__ */ jsx30("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__ */ jsx30("path", { d: b.d }) })
1468
2168
  },
1469
2169
  b.kind
1470
2170
  )) }),
1471
- /* @__PURE__ */ jsxs15("div", { className: "hl-position-coords", children: [
1472
- /* @__PURE__ */ jsxs15("label", { className: "hl-position-field", children: [
1473
- /* @__PURE__ */ jsx25(
2171
+ /* @__PURE__ */ jsxs17("div", { className: "hl-position-coords", children: [
2172
+ /* @__PURE__ */ jsxs17("label", { className: "hl-position-field", children: [
2173
+ /* @__PURE__ */ jsx30(
1474
2174
  "input",
1475
2175
  {
1476
2176
  type: "number",
@@ -1481,10 +2181,10 @@ function PositionPanel({ core, livePosition }) {
1481
2181
  "aria-label": "X position"
1482
2182
  }
1483
2183
  ),
1484
- /* @__PURE__ */ jsx25("span", { className: "hl-position-axis hl-mono", children: "X" })
2184
+ /* @__PURE__ */ jsx30("span", { className: "hl-position-axis hl-mono", children: "X" })
1485
2185
  ] }),
1486
- /* @__PURE__ */ jsxs15("label", { className: "hl-position-field", children: [
1487
- /* @__PURE__ */ jsx25(
2186
+ /* @__PURE__ */ jsxs17("label", { className: "hl-position-field", children: [
2187
+ /* @__PURE__ */ jsx30(
1488
2188
  "input",
1489
2189
  {
1490
2190
  type: "number",
@@ -1495,24 +2195,24 @@ function PositionPanel({ core, livePosition }) {
1495
2195
  "aria-label": "Y position"
1496
2196
  }
1497
2197
  ),
1498
- /* @__PURE__ */ jsx25("span", { className: "hl-position-axis hl-mono", children: "Y" })
2198
+ /* @__PURE__ */ jsx30("span", { className: "hl-position-axis hl-mono", children: "Y" })
1499
2199
  ] })
1500
2200
  ] })
1501
2201
  ] });
1502
2202
  }
1503
2203
 
1504
2204
  // src/command-palette/NodeSearch.tsx
1505
- import { useCallback as useCallback5, useEffect as useEffect4, useMemo as useMemo4, useRef as useRef3, useState as useState4 } from "react";
1506
- import { Fragment as Fragment4, jsx as jsx26, jsxs as jsxs16 } from "react/jsx-runtime";
2205
+ import { useCallback as useCallback7, useEffect as useEffect5, useMemo as useMemo5, useRef as useRef5, useState as useState5 } from "react";
2206
+ import { Fragment as Fragment4, jsx as jsx31, jsxs as jsxs18 } from "react/jsx-runtime";
1507
2207
  var PANEL_WIDTH = 320;
1508
2208
  var PANEL_MAX_HEIGHT = 360;
1509
2209
  function NodeSearch({ controller }) {
1510
2210
  const { state } = controller;
1511
- const [query, setQuery] = useState4("");
1512
- const [active, setActive] = useState4(0);
1513
- const inputRef = useRef3(null);
2211
+ const [query, setQuery] = useState5("");
2212
+ const [active, setActive] = useState5(0);
2213
+ const inputRef = useRef5(null);
1514
2214
  const isOpen = state !== void 0;
1515
- useEffect4(() => {
2215
+ useEffect5(() => {
1516
2216
  var _a;
1517
2217
  if (isOpen) {
1518
2218
  setQuery("");
@@ -1520,12 +2220,12 @@ function NodeSearch({ controller }) {
1520
2220
  (_a = inputRef.current) == null ? void 0 : _a.focus();
1521
2221
  }
1522
2222
  }, [isOpen]);
1523
- const results = useMemo4(
2223
+ const results = useMemo5(
1524
2224
  () => state !== void 0 ? controller.results(query) : [],
1525
2225
  [controller, state, query]
1526
2226
  );
1527
2227
  const clampedActive = results.length === 0 ? 0 : Math.min(active, results.length - 1);
1528
- const choose = useCallback5(
2228
+ const choose = useCallback7(
1529
2229
  (view) => {
1530
2230
  if (view !== void 0) {
1531
2231
  controller.commit(view);
@@ -1533,7 +2233,7 @@ function NodeSearch({ controller }) {
1533
2233
  },
1534
2234
  [controller]
1535
2235
  );
1536
- const onKeyDown = useCallback5(
2236
+ const onKeyDown = useCallback7(
1537
2237
  (event) => {
1538
2238
  switch (event.key) {
1539
2239
  case "ArrowDown":
@@ -1563,8 +2263,8 @@ function NodeSearch({ controller }) {
1563
2263
  }
1564
2264
  const style = clampToViewport(state.screen.x, state.screen.y);
1565
2265
  const connectMode = state.pending !== void 0;
1566
- return /* @__PURE__ */ jsxs16(Fragment4, { children: [
1567
- /* @__PURE__ */ jsx26(
2266
+ return /* @__PURE__ */ jsxs18(Fragment4, { children: [
2267
+ /* @__PURE__ */ jsx31(
1568
2268
  "div",
1569
2269
  {
1570
2270
  className: "hl-node-search-scrim",
@@ -1573,10 +2273,10 @@ function NodeSearch({ controller }) {
1573
2273
  }
1574
2274
  }
1575
2275
  ),
1576
- /* @__PURE__ */ jsxs16("div", { className: "hl-node-search", style, role: "dialog", "aria-label": "Create node", children: [
1577
- /* @__PURE__ */ jsxs16("div", { className: "hl-node-search-field", children: [
1578
- /* @__PURE__ */ jsx26("span", { className: "hl-node-search-glyph", "aria-hidden": true, children: connectMode ? "\u2933" : "\uFF0B" }),
1579
- /* @__PURE__ */ jsx26(
2276
+ /* @__PURE__ */ jsxs18("div", { className: "hl-node-search", style, role: "dialog", "aria-label": "Create node", children: [
2277
+ /* @__PURE__ */ jsxs18("div", { className: "hl-node-search-field", children: [
2278
+ /* @__PURE__ */ jsx31("span", { className: "hl-node-search-glyph", "aria-hidden": true, children: connectMode ? "\u2933" : "\uFF0B" }),
2279
+ /* @__PURE__ */ jsx31(
1580
2280
  "input",
1581
2281
  {
1582
2282
  ref: inputRef,
@@ -1594,12 +2294,12 @@ function NodeSearch({ controller }) {
1594
2294
  }
1595
2295
  )
1596
2296
  ] }),
1597
- /* @__PURE__ */ jsxs16("ul", { className: "hl-node-search-list", role: "listbox", children: [
1598
- results.length === 0 && /* @__PURE__ */ jsx26("li", { className: "hl-node-search-empty hl-mono", children: "no matching nodes" }),
2297
+ /* @__PURE__ */ jsxs18("ul", { className: "hl-node-search-list", role: "listbox", children: [
2298
+ results.length === 0 && /* @__PURE__ */ jsx31("li", { className: "hl-node-search-empty hl-mono", children: "no matching nodes" }),
1599
2299
  results.map((view, index) => {
1600
2300
  const descriptor = view.result.value;
1601
2301
  const kind = descriptor.kind;
1602
- return /* @__PURE__ */ jsxs16(
2302
+ return /* @__PURE__ */ jsxs18(
1603
2303
  "li",
1604
2304
  {
1605
2305
  role: "option",
@@ -1611,15 +2311,15 @@ function NodeSearch({ controller }) {
1611
2311
  choose(view);
1612
2312
  },
1613
2313
  children: [
1614
- /* @__PURE__ */ jsx26(
2314
+ /* @__PURE__ */ jsx31(
1615
2315
  "span",
1616
2316
  {
1617
2317
  className: `hl-node-search-kind hl-mono hl-kind-${kind.toLowerCase()}`,
1618
2318
  children: kind
1619
2319
  }
1620
2320
  ),
1621
- /* @__PURE__ */ jsx26("span", { className: "hl-node-search-label", children: highlight(descriptor.displayName, view.result.match) }),
1622
- view.connectPortId !== void 0 && /* @__PURE__ */ jsx26("span", { className: "hl-node-search-port hl-mono", children: view.connectPortId })
2321
+ /* @__PURE__ */ jsx31("span", { className: "hl-node-search-label", children: highlight(descriptor.displayName, view.result.match) }),
2322
+ view.connectPortId !== void 0 && /* @__PURE__ */ jsx31("span", { className: "hl-node-search-port hl-mono", children: view.connectPortId })
1623
2323
  ]
1624
2324
  },
1625
2325
  descriptor.code
@@ -1633,7 +2333,7 @@ function highlight(text, match) {
1633
2333
  const hit = new Set(match.positions);
1634
2334
  const chars = Array.from(text);
1635
2335
  return chars.map(
1636
- (ch, i) => hit.has(i) ? /* @__PURE__ */ jsx26("mark", { className: "hl-node-search-hit", children: ch }, i) : /* @__PURE__ */ jsx26("span", { children: ch }, i)
2336
+ (ch, i) => hit.has(i) ? /* @__PURE__ */ jsx31("mark", { className: "hl-node-search-hit", children: ch }, i) : /* @__PURE__ */ jsx31("span", { children: ch }, i)
1637
2337
  );
1638
2338
  }
1639
2339
  function clampToViewport(x, y) {
@@ -1646,7 +2346,7 @@ function clampToViewport(x, y) {
1646
2346
  }
1647
2347
 
1648
2348
  // src/command-palette/useNodeSearch.ts
1649
- import { useCallback as useCallback6, useMemo as useMemo5, useState as useState5 } from "react";
2349
+ import { useCallback as useCallback8, useMemo as useMemo6, useState as useState6 } from "react";
1650
2350
  import { AddNodeCommand, ConnectCommand } from "@hlang-tech/editor-core";
1651
2351
 
1652
2352
  // src/command-palette/fuzzy.ts
@@ -1737,21 +2437,21 @@ function firstCompletingPort(ports, pending, validator) {
1737
2437
 
1738
2438
  // src/command-palette/useNodeSearch.ts
1739
2439
  function useNodeSearch(core) {
1740
- const [state, setState] = useState5(void 0);
1741
- const openBlank = useCallback6((screen, flow) => {
2440
+ const [state, setState] = useState6(void 0);
2441
+ const openBlank = useCallback8((screen, flow) => {
1742
2442
  setState({ screen, flow });
1743
2443
  }, []);
1744
- const openFromConnection = useCallback6(
2444
+ const openFromConnection = useCallback8(
1745
2445
  (screen, flow, pending) => {
1746
2446
  setState({ screen, flow, pending });
1747
2447
  },
1748
2448
  []
1749
2449
  );
1750
- const close = useCallback6(() => {
2450
+ const close = useCallback8(() => {
1751
2451
  setState(void 0);
1752
2452
  }, []);
1753
2453
  const descriptors = core.registry.list();
1754
- const results = useCallback6(
2454
+ const results = useCallback8(
1755
2455
  (query) => {
1756
2456
  if ((state == null ? void 0 : state.pending) !== void 0) {
1757
2457
  return connectableCandidates(
@@ -1765,7 +2465,7 @@ function useNodeSearch(core) {
1765
2465
  },
1766
2466
  [state, descriptors, core]
1767
2467
  );
1768
- const commit = useCallback6(
2468
+ const commit = useCallback8(
1769
2469
  (view) => {
1770
2470
  if (state === void 0) {
1771
2471
  return;
@@ -1789,14 +2489,14 @@ function useNodeSearch(core) {
1789
2489
  },
1790
2490
  [state, core]
1791
2491
  );
1792
- return useMemo5(
2492
+ return useMemo6(
1793
2493
  () => ({ state, openBlank, openFromConnection, close, results, commit }),
1794
2494
  [state, openBlank, openFromConnection, close, results, commit]
1795
2495
  );
1796
2496
  }
1797
2497
 
1798
2498
  // src/command-palette/useNodeSearchTriggers.ts
1799
- import { useCallback as useCallback7, useRef as useRef4 } from "react";
2499
+ import { useCallback as useCallback9, useRef as useRef6 } from "react";
1800
2500
  import { useReactFlow } from "@xyflow/react";
1801
2501
  var DEFAULT_DOUBLE_CLICK_MS = 350;
1802
2502
  function useNodeSearchTriggers(core, search, options = {}) {
@@ -1804,12 +2504,12 @@ function useNodeSearchTriggers(core, search, options = {}) {
1804
2504
  const flow = useReactFlow();
1805
2505
  const doubleClickMs = (_a = options.doubleClickMs) != null ? _a : DEFAULT_DOUBLE_CLICK_MS;
1806
2506
  const now = (_b = options.now) != null ? _b : defaultNow;
1807
- const startRef = useRef4(null);
1808
- const lastPaneClickRef = useRef4(Number.NEGATIVE_INFINITY);
1809
- const onConnectStart = useCallback7((_event, params) => {
2507
+ const startRef = useRef6(null);
2508
+ const lastPaneClickRef = useRef6(Number.NEGATIVE_INFINITY);
2509
+ const onConnectStart = useCallback9((_event, params) => {
1810
2510
  startRef.current = params;
1811
2511
  }, []);
1812
- const onConnectEnd = useCallback7(
2512
+ const onConnectEnd = useCallback9(
1813
2513
  (event, connectionState) => {
1814
2514
  const start = startRef.current;
1815
2515
  startRef.current = null;
@@ -1842,7 +2542,7 @@ function useNodeSearchTriggers(core, search, options = {}) {
1842
2542
  },
1843
2543
  [core, flow, search]
1844
2544
  );
1845
- const onPaneClick = useCallback7(
2545
+ const onPaneClick = useCallback9(
1846
2546
  (event) => {
1847
2547
  const t = now();
1848
2548
  const isDouble = t - lastPaneClickRef.current <= doubleClickMs;
@@ -1874,7 +2574,7 @@ function pointerOf(event) {
1874
2574
  }
1875
2575
 
1876
2576
  // src/hooks/useEditorHotkeys.ts
1877
- import { useEffect as useEffect5 } from "react";
2577
+ import { useEffect as useEffect6 } from "react";
1878
2578
  import { useClipboard } from "@hlang-tech/editor-react";
1879
2579
  function isEditableTarget(target) {
1880
2580
  if (!(target instanceof HTMLElement)) {
@@ -1893,7 +2593,7 @@ function withinScope(target, scope) {
1893
2593
  function useEditorHotkeys(core, options = {}) {
1894
2594
  const clipboard = useClipboard(core);
1895
2595
  const { scope } = options;
1896
- useEffect5(() => {
2596
+ useEffect6(() => {
1897
2597
  const onKeyDown = (event) => {
1898
2598
  if (!withinScope(event.target, scope) || isEditableTarget(event.target)) {
1899
2599
  return;
@@ -1956,33 +2656,33 @@ function useEditorHotkeys(core, options = {}) {
1956
2656
  }
1957
2657
 
1958
2658
  // src/context-menu/NodeContextMenu.tsx
1959
- import { useCallback as useCallback8, useEffect as useEffect6, useMemo as useMemo6, useRef as useRef5, useState as useState6 } from "react";
1960
- import { Fragment as Fragment5, jsx as jsx27, jsxs as jsxs17 } from "react/jsx-runtime";
2659
+ import { useCallback as useCallback10, useEffect as useEffect7, useMemo as useMemo7, useRef as useRef7, useState as useState7 } from "react";
2660
+ import { Fragment as Fragment5, jsx as jsx32, jsxs as jsxs19 } from "react/jsx-runtime";
1961
2661
  var PANEL_WIDTH2 = 208;
1962
2662
  var ITEM_HEIGHT = 34;
1963
2663
  var VIEWPORT_MARGIN = 8;
1964
2664
  var MENU_VPAD = 12;
1965
2665
  function NodeContextMenu({ controller, items }) {
1966
2666
  const { state } = controller;
1967
- const [active, setActive] = useState6(0);
1968
- const menuRef = useRef5(null);
2667
+ const [active, setActive] = useState7(0);
2668
+ const menuRef = useRef7(null);
1969
2669
  const isOpen = state !== void 0;
1970
- useEffect6(() => {
2670
+ useEffect7(() => {
1971
2671
  var _a;
1972
2672
  if (isOpen) {
1973
2673
  setActive(0);
1974
2674
  (_a = menuRef.current) == null ? void 0 : _a.focus();
1975
2675
  }
1976
2676
  }, [isOpen]);
1977
- const rows = useMemo6(
2677
+ const rows = useMemo7(
1978
2678
  () => state !== void 0 ? items(state.nodeId) : [],
1979
2679
  [items, state]
1980
2680
  );
1981
- const enabledIndices = useMemo6(
2681
+ const enabledIndices = useMemo7(
1982
2682
  () => rows.map((r, i) => r.disabled ? -1 : i).filter((i) => i >= 0),
1983
2683
  [rows]
1984
2684
  );
1985
- const move = useCallback8(
2685
+ const move = useCallback10(
1986
2686
  (delta) => {
1987
2687
  if (enabledIndices.length === 0) {
1988
2688
  return;
@@ -1996,7 +2696,7 @@ function NodeContextMenu({ controller, items }) {
1996
2696
  },
1997
2697
  [enabledIndices]
1998
2698
  );
1999
- const choose = useCallback8(
2699
+ const choose = useCallback10(
2000
2700
  (item) => {
2001
2701
  if (item === void 0 || item.disabled) {
2002
2702
  return;
@@ -2006,7 +2706,7 @@ function NodeContextMenu({ controller, items }) {
2006
2706
  },
2007
2707
  [controller]
2008
2708
  );
2009
- const onKeyDown = useCallback8(
2709
+ const onKeyDown = useCallback10(
2010
2710
  (event) => {
2011
2711
  switch (event.key) {
2012
2712
  case "ArrowDown":
@@ -2035,8 +2735,8 @@ function NodeContextMenu({ controller, items }) {
2035
2735
  return null;
2036
2736
  }
2037
2737
  const style = clampToViewport2(state.screen.x, state.screen.y, rows.length);
2038
- return /* @__PURE__ */ jsxs17(Fragment5, { children: [
2039
- /* @__PURE__ */ jsx27(
2738
+ return /* @__PURE__ */ jsxs19(Fragment5, { children: [
2739
+ /* @__PURE__ */ jsx32(
2040
2740
  "div",
2041
2741
  {
2042
2742
  className: "hl-ctx-scrim",
@@ -2049,7 +2749,7 @@ function NodeContextMenu({ controller, items }) {
2049
2749
  }
2050
2750
  }
2051
2751
  ),
2052
- /* @__PURE__ */ jsx27(
2752
+ /* @__PURE__ */ jsx32(
2053
2753
  "div",
2054
2754
  {
2055
2755
  ref: menuRef,
@@ -2059,7 +2759,7 @@ function NodeContextMenu({ controller, items }) {
2059
2759
  "aria-label": "Node actions",
2060
2760
  tabIndex: -1,
2061
2761
  onKeyDown,
2062
- children: rows.map((item, index) => /* @__PURE__ */ jsxs17(
2762
+ children: rows.map((item, index) => /* @__PURE__ */ jsxs19(
2063
2763
  "button",
2064
2764
  {
2065
2765
  type: "button",
@@ -2072,9 +2772,9 @@ function NodeContextMenu({ controller, items }) {
2072
2772
  choose(item);
2073
2773
  },
2074
2774
  children: [
2075
- /* @__PURE__ */ jsx27("span", { className: "hl-ctx-item-icon", "aria-hidden": true, children: item.icon }),
2076
- /* @__PURE__ */ jsx27("span", { className: "hl-ctx-item-label", children: item.label }),
2077
- item.shortcut !== void 0 && /* @__PURE__ */ jsx27("span", { className: "hl-ctx-item-key hl-mono", children: item.shortcut })
2775
+ /* @__PURE__ */ jsx32("span", { className: "hl-ctx-item-icon", "aria-hidden": true, children: item.icon }),
2776
+ /* @__PURE__ */ jsx32("span", { className: "hl-ctx-item-label", children: item.label }),
2777
+ item.shortcut !== void 0 && /* @__PURE__ */ jsx32("span", { className: "hl-ctx-item-key hl-mono", children: item.shortcut })
2078
2778
  ]
2079
2779
  },
2080
2780
  item.id
@@ -2094,21 +2794,21 @@ function clampToViewport2(x, y, rowCount) {
2094
2794
  }
2095
2795
 
2096
2796
  // src/context-menu/useNodeContextMenu.ts
2097
- import { useCallback as useCallback9, useMemo as useMemo7, useState as useState7 } from "react";
2797
+ import { useCallback as useCallback11, useMemo as useMemo8, useState as useState8 } from "react";
2098
2798
  function useNodeContextMenu() {
2099
- const [state, setState] = useState7(void 0);
2100
- const openForNode = useCallback9((screen, nodeId) => {
2799
+ const [state, setState] = useState8(void 0);
2800
+ const openForNode = useCallback11((screen, nodeId) => {
2101
2801
  setState({ screen, nodeId });
2102
2802
  }, []);
2103
- const close = useCallback9(() => {
2803
+ const close = useCallback11(() => {
2104
2804
  setState(void 0);
2105
2805
  }, []);
2106
- return useMemo7(() => ({ state, openForNode, close }), [state, openForNode, close]);
2806
+ return useMemo8(() => ({ state, openForNode, close }), [state, openForNode, close]);
2107
2807
  }
2108
2808
 
2109
2809
  // src/overlays/Cheatsheet.tsx
2110
- import { useEffect as useEffect7, useRef as useRef6 } from "react";
2111
- import { Fragment as Fragment6, jsx as jsx28, jsxs as jsxs18 } from "react/jsx-runtime";
2810
+ import { useEffect as useEffect8, useRef as useRef8 } from "react";
2811
+ import { Fragment as Fragment6, jsx as jsx33, jsxs as jsxs20 } from "react/jsx-runtime";
2112
2812
  var DEFAULT_GROUPS = [
2113
2813
  {
2114
2814
  title: "Edit",
@@ -2144,8 +2844,8 @@ var DEFAULT_GROUPS = [
2144
2844
  }
2145
2845
  ];
2146
2846
  function Cheatsheet({ open, onClose, groups = DEFAULT_GROUPS }) {
2147
- const dialogRef = useRef6(null);
2148
- useEffect7(() => {
2847
+ const dialogRef = useRef8(null);
2848
+ useEffect8(() => {
2149
2849
  var _a;
2150
2850
  if (!open) {
2151
2851
  return;
@@ -2162,9 +2862,9 @@ function Cheatsheet({ open, onClose, groups = DEFAULT_GROUPS }) {
2162
2862
  if (!open) {
2163
2863
  return null;
2164
2864
  }
2165
- return /* @__PURE__ */ jsxs18(Fragment6, { children: [
2166
- /* @__PURE__ */ jsx28("div", { className: "hl-cheat-scrim", onPointerDown: onClose }),
2167
- /* @__PURE__ */ jsxs18(
2865
+ return /* @__PURE__ */ jsxs20(Fragment6, { children: [
2866
+ /* @__PURE__ */ jsx33("div", { className: "hl-cheat-scrim", onPointerDown: onClose }),
2867
+ /* @__PURE__ */ jsxs20(
2168
2868
  "div",
2169
2869
  {
2170
2870
  ref: dialogRef,
@@ -2174,18 +2874,18 @@ function Cheatsheet({ open, onClose, groups = DEFAULT_GROUPS }) {
2174
2874
  "aria-label": "Keyboard shortcuts",
2175
2875
  tabIndex: -1,
2176
2876
  children: [
2177
- /* @__PURE__ */ jsxs18("div", { className: "hl-cheat-head", children: [
2178
- /* @__PURE__ */ jsx28("span", { className: "hl-cheat-title", children: "Shortcuts" }),
2179
- /* @__PURE__ */ jsx28("button", { className: "hl-cheat-close", onClick: onClose, "aria-label": "Close", children: "\u2715" })
2877
+ /* @__PURE__ */ jsxs20("div", { className: "hl-cheat-head", children: [
2878
+ /* @__PURE__ */ jsx33("span", { className: "hl-cheat-title", children: "Shortcuts" }),
2879
+ /* @__PURE__ */ jsx33("button", { className: "hl-cheat-close", onClick: onClose, "aria-label": "Close", children: "\u2715" })
2180
2880
  ] }),
2181
- /* @__PURE__ */ jsx28("div", { className: "hl-cheat-grid", children: groups.map((group) => /* @__PURE__ */ jsxs18("section", { className: "hl-cheat-group", children: [
2182
- /* @__PURE__ */ jsx28("h3", { className: "hl-cheat-group-title hl-mono", children: group.title }),
2183
- /* @__PURE__ */ jsx28("ul", { className: "hl-cheat-rows", children: group.rows.map((row) => (
2881
+ /* @__PURE__ */ jsx33("div", { className: "hl-cheat-grid", children: groups.map((group) => /* @__PURE__ */ jsxs20("section", { className: "hl-cheat-group", children: [
2882
+ /* @__PURE__ */ jsx33("h3", { className: "hl-cheat-group-title hl-mono", children: group.title }),
2883
+ /* @__PURE__ */ jsx33("ul", { className: "hl-cheat-rows", children: group.rows.map((row) => (
2184
2884
  // A row is a binding: one action can have several (two ways to pan),
2185
2885
  // so neither the label nor the keys alone identify it.
2186
- /* @__PURE__ */ jsxs18("li", { className: "hl-cheat-row", children: [
2187
- /* @__PURE__ */ jsx28("span", { className: "hl-cheat-label", children: row.label }),
2188
- /* @__PURE__ */ jsx28("kbd", { className: "hl-cheat-keys hl-mono", children: row.keys })
2886
+ /* @__PURE__ */ jsxs20("li", { className: "hl-cheat-row", children: [
2887
+ /* @__PURE__ */ jsx33("span", { className: "hl-cheat-label", children: row.label }),
2888
+ /* @__PURE__ */ jsx33("kbd", { className: "hl-cheat-keys hl-mono", children: row.keys })
2189
2889
  ] }, `${row.keys}\0${row.label}`)
2190
2890
  )) })
2191
2891
  ] }, group.title)) })
@@ -2196,13 +2896,13 @@ function Cheatsheet({ open, onClose, groups = DEFAULT_GROUPS }) {
2196
2896
  }
2197
2897
 
2198
2898
  // src/overlays/Toast.tsx
2199
- import { createContext as createContext3, useCallback as useCallback10, useContext as useContext3, useMemo as useMemo8, useRef as useRef7, useState as useState8 } from "react";
2200
- import { jsx as jsx29, jsxs as jsxs19 } from "react/jsx-runtime";
2201
- var ToastCtx = createContext3({ show: () => void 0 });
2899
+ import { createContext as createContext4, useCallback as useCallback12, useContext as useContext4, useMemo as useMemo9, useRef as useRef9, useState as useState9 } from "react";
2900
+ import { jsx as jsx34, jsxs as jsxs21 } from "react/jsx-runtime";
2901
+ var ToastCtx = createContext4({ show: () => void 0 });
2202
2902
  function ToastProvider({ children, duration = 2200 }) {
2203
- const [items, setItems] = useState8([]);
2204
- const seq = useRef7(0);
2205
- const show = useCallback10(
2903
+ const [items, setItems] = useState9([]);
2904
+ const seq = useRef9(0);
2905
+ const show = useCallback12(
2206
2906
  (message) => {
2207
2907
  seq.current += 1;
2208
2908
  const id = seq.current;
@@ -2213,22 +2913,22 @@ function ToastProvider({ children, duration = 2200 }) {
2213
2913
  },
2214
2914
  [duration]
2215
2915
  );
2216
- const api = useMemo8(() => ({ show }), [show]);
2217
- return /* @__PURE__ */ jsxs19(ToastCtx.Provider, { value: api, children: [
2916
+ const api = useMemo9(() => ({ show }), [show]);
2917
+ return /* @__PURE__ */ jsxs21(ToastCtx.Provider, { value: api, children: [
2218
2918
  children,
2219
- /* @__PURE__ */ jsx29("div", { className: "hl-toast-stack", "aria-live": "polite", children: items.map((t) => /* @__PURE__ */ jsx29("div", { className: "hl-toast", children: t.message }, t.id)) })
2919
+ /* @__PURE__ */ jsx34("div", { className: "hl-toast-stack", "aria-live": "polite", children: items.map((t) => /* @__PURE__ */ jsx34("div", { className: "hl-toast", children: t.message }, t.id)) })
2220
2920
  ] });
2221
2921
  }
2222
2922
  function useToast() {
2223
- return useContext3(ToastCtx);
2923
+ return useContext4(ToastCtx);
2224
2924
  }
2225
2925
 
2226
2926
  // src/hooks/useLiveSignals.ts
2227
- import { useEffect as useEffect8 } from "react";
2927
+ import { useEffect as useEffect9 } from "react";
2228
2928
  function useLiveSignals(core, handle, explicitStore) {
2229
2929
  const contextStore = useLiveSignalStore();
2230
2930
  const store = explicitStore != null ? explicitStore : contextStore;
2231
- useEffect8(() => {
2931
+ useEffect9(() => {
2232
2932
  if (store === null) {
2233
2933
  return;
2234
2934
  }
@@ -2237,6 +2937,8 @@ function useLiveSignals(core, handle, explicitStore) {
2237
2937
  id: edge.id,
2238
2938
  sourceNodeId: edge.source.nodeId,
2239
2939
  sourcePortId: edge.source.portId,
2940
+ targetNodeId: edge.target.nodeId,
2941
+ targetPortId: edge.target.portId,
2240
2942
  isError: edge.source.portId === "@node/error"
2241
2943
  }));
2242
2944
  store.setEdgeIndex(entries);
@@ -2249,7 +2951,8 @@ function useLiveSignals(core, handle, explicitStore) {
2249
2951
  removed.dispose();
2250
2952
  };
2251
2953
  }, [core, store]);
2252
- useEffect8(() => {
2954
+ useEffect9(() => {
2955
+ var _a, _b, _c;
2253
2956
  if (store === null) {
2254
2957
  return;
2255
2958
  }
@@ -2257,45 +2960,452 @@ function useLiveSignals(core, handle, explicitStore) {
2257
2960
  store.reset();
2258
2961
  return;
2259
2962
  }
2260
- const activitySub = handle.onActivity((e) => store.markActivity(e.nodeId, e.portId));
2963
+ const signalSub = (_b = (_a = handle.onDelivery) == null ? void 0 : _a.call(handle, (event) => {
2964
+ store.markDelivery(event);
2965
+ })) != null ? _b : handle.onActivity((e) => store.markActivity(e.nodeId, e.portId));
2261
2966
  const errorSub = handle.onError((e) => store.markError(e.nodeId));
2967
+ const finishedSub = (_c = handle.onFinished) == null ? void 0 : _c.call(handle, (e) => {
2968
+ store.markFinished(e.nodeId);
2969
+ });
2262
2970
  return () => {
2263
- activitySub.dispose();
2971
+ signalSub.dispose();
2264
2972
  errorSub.dispose();
2973
+ finishedSub == null ? void 0 : finishedSub.dispose();
2265
2974
  store.reset();
2266
2975
  };
2267
2976
  }, [store, handle]);
2268
2977
  }
2978
+
2979
+ // src/live/run-trace.ts
2980
+ var RUN_LOG_CAPACITY = 500;
2981
+ var RunTrace = class {
2982
+ constructor(options = {}) {
2983
+ /** The log as a ring: `ring[(head + i) % capacity]` is the i-th oldest entry. */
2984
+ this.ring = [];
2985
+ this.head = 0;
2986
+ this.droppedCount = 0;
2987
+ this.seq = 0;
2988
+ this.startedAt = 0;
2989
+ this.stats = /* @__PURE__ */ new Map();
2990
+ this.listeners = /* @__PURE__ */ new Set();
2991
+ /** Bumped by every change; the entries cache follows it. */
2992
+ this.revision = 0;
2993
+ /** Bumped once per notified frame; what subscribers compare. */
2994
+ this.version = 0;
2995
+ this.frame = null;
2996
+ /** The entries as an array, rebuilt only after a change. */
2997
+ this.entriesCache = [];
2998
+ this.entriesRevision = -1;
2999
+ var _a, _b;
3000
+ const capacity = (_a = options.capacity) != null ? _a : RUN_LOG_CAPACITY;
3001
+ if (!Number.isInteger(capacity) || capacity < 1) {
3002
+ throw new RangeError("RunTrace capacity must be a positive integer.");
3003
+ }
3004
+ this.capacity = capacity;
3005
+ this.deps = (_b = options.deps) != null ? _b : defaultLiveDeps;
3006
+ }
3007
+ /** Begins a run: clears everything and times entries from now. */
3008
+ start() {
3009
+ this.clear();
3010
+ this.startedAt = this.deps.now();
3011
+ }
3012
+ /** Forgets the log and the statistics. */
3013
+ clear() {
3014
+ this.ring.length = 0;
3015
+ this.head = 0;
3016
+ this.droppedCount = 0;
3017
+ this.stats.clear();
3018
+ this.changed();
3019
+ }
3020
+ recordDelivery(event) {
3021
+ this.append({
3022
+ kind: "delivery",
3023
+ seq: this.nextSeq(),
3024
+ at: this.elapsed(),
3025
+ sourceNodeId: event.sourceNodeId,
3026
+ sourcePortId: event.sourcePortId,
3027
+ targetNodeId: event.targetNodeId,
3028
+ targetPortId: event.targetPortId,
3029
+ message: event.message,
3030
+ duration: event.duration
3031
+ });
3032
+ const source = this.statsFor(event.sourceNodeId);
3033
+ source.sent += 1;
3034
+ source.lastSent = event.message;
3035
+ const target = this.statsFor(event.targetNodeId);
3036
+ target.received += 1;
3037
+ target.totalMs += event.duration;
3038
+ target.maxMs = Math.max(target.maxMs, event.duration);
3039
+ target.lastReceived = event.message;
3040
+ this.changed();
3041
+ }
3042
+ recordError(event) {
3043
+ this.append({ kind: "error", seq: this.nextSeq(), at: this.elapsed(), nodeId: event.nodeId, error: event.error });
3044
+ this.statsFor(event.nodeId).errors += 1;
3045
+ this.changed();
3046
+ }
3047
+ recordEnded(event) {
3048
+ this.append({ kind: "ended", seq: this.nextSeq(), at: this.elapsed(), nodeId: event.nodeId, portId: event.portId });
3049
+ this.statsFor(event.nodeId);
3050
+ this.changed();
3051
+ }
3052
+ recordFinished(event) {
3053
+ this.append({ kind: "finished", seq: this.nextSeq(), at: this.elapsed(), nodeId: event.nodeId });
3054
+ this.statsFor(event.nodeId).finished = true;
3055
+ this.changed();
3056
+ }
3057
+ recordDropped(event) {
3058
+ this.append({
3059
+ kind: "dropped",
3060
+ seq: this.nextSeq(),
3061
+ at: this.elapsed(),
3062
+ sourceNodeId: event.sourceNodeId,
3063
+ sourcePortId: event.sourcePortId,
3064
+ targetNodeId: event.targetNodeId,
3065
+ targetPortId: event.targetPortId,
3066
+ message: event.message
3067
+ });
3068
+ this.statsFor(event.sourceNodeId);
3069
+ this.statsFor(event.targetNodeId).dropped += 1;
3070
+ this.changed();
3071
+ }
3072
+ /** The log, oldest first. The same array is returned until something changes. */
3073
+ entries() {
3074
+ if (this.entriesRevision !== this.revision) {
3075
+ const count = this.ring.length;
3076
+ const ordered = [];
3077
+ for (let i = 0; i < count; i += 1) {
3078
+ const entry = this.ring[(this.head + i) % count];
3079
+ if (entry !== void 0) {
3080
+ ordered.push(entry);
3081
+ }
3082
+ }
3083
+ this.entriesCache = ordered;
3084
+ this.entriesRevision = this.revision;
3085
+ }
3086
+ return this.entriesCache;
3087
+ }
3088
+ /** A node's statistics, or `undefined` when it has done nothing this run. */
3089
+ statsOf(nodeId) {
3090
+ const stats = this.stats.get(nodeId);
3091
+ return stats === void 0 ? void 0 : { ...stats };
3092
+ }
3093
+ /** How many entries the log has let go of to stay within its capacity. */
3094
+ get dropped() {
3095
+ return this.droppedCount;
3096
+ }
3097
+ /** Changes once per notified frame; a snapshot for `useSyncExternalStore`. */
3098
+ getVersion() {
3099
+ return this.version;
3100
+ }
3101
+ subscribe(listener) {
3102
+ this.listeners.add(listener);
3103
+ return () => {
3104
+ this.listeners.delete(listener);
3105
+ };
3106
+ }
3107
+ append(entry) {
3108
+ if (this.ring.length < this.capacity) {
3109
+ this.ring.push(entry);
3110
+ return;
3111
+ }
3112
+ this.ring[this.head] = entry;
3113
+ this.head = (this.head + 1) % this.capacity;
3114
+ this.droppedCount += 1;
3115
+ }
3116
+ nextSeq() {
3117
+ this.seq += 1;
3118
+ return this.seq;
3119
+ }
3120
+ elapsed() {
3121
+ return this.deps.now() - this.startedAt;
3122
+ }
3123
+ statsFor(nodeId) {
3124
+ let stats = this.stats.get(nodeId);
3125
+ if (stats === void 0) {
3126
+ stats = {
3127
+ received: 0,
3128
+ sent: 0,
3129
+ errors: 0,
3130
+ totalMs: 0,
3131
+ maxMs: 0,
3132
+ lastReceived: void 0,
3133
+ lastSent: void 0,
3134
+ dropped: 0,
3135
+ finished: false
3136
+ };
3137
+ this.stats.set(nodeId, stats);
3138
+ }
3139
+ return stats;
3140
+ }
3141
+ /** Schedules one notification for the next frame, however many changes come before it. */
3142
+ changed() {
3143
+ this.revision += 1;
3144
+ if (this.frame !== null) {
3145
+ return;
3146
+ }
3147
+ this.frame = this.deps.schedule(() => {
3148
+ this.frame = null;
3149
+ this.version += 1;
3150
+ for (const listener of [...this.listeners]) {
3151
+ listener();
3152
+ }
3153
+ });
3154
+ }
3155
+ };
3156
+ function involves(entry, nodeId) {
3157
+ switch (entry.kind) {
3158
+ case "delivery":
3159
+ case "dropped":
3160
+ return entry.sourceNodeId === nodeId || entry.targetNodeId === nodeId;
3161
+ case "error":
3162
+ case "ended":
3163
+ case "finished":
3164
+ return entry.nodeId === nodeId;
3165
+ }
3166
+ }
3167
+
3168
+ // src/hooks/useRunTrace.ts
3169
+ import { useEffect as useEffect10 } from "react";
3170
+ function useRunTrace(trace, handle) {
3171
+ useEffect10(() => {
3172
+ var _a, _b, _c, _d;
3173
+ if (handle === void 0) {
3174
+ return;
3175
+ }
3176
+ trace.start();
3177
+ const deliveries = (_a = handle.onDelivery) == null ? void 0 : _a.call(handle, (event) => {
3178
+ trace.recordDelivery(event);
3179
+ });
3180
+ const errors = handle.onError((event) => {
3181
+ trace.recordError(event);
3182
+ });
3183
+ const ended = (_b = handle.onEnded) == null ? void 0 : _b.call(handle, (event) => {
3184
+ trace.recordEnded(event);
3185
+ });
3186
+ const finished = (_c = handle.onFinished) == null ? void 0 : _c.call(handle, (event) => {
3187
+ trace.recordFinished(event);
3188
+ });
3189
+ const dropped = (_d = handle.onDropped) == null ? void 0 : _d.call(handle, (event) => {
3190
+ trace.recordDropped(event);
3191
+ });
3192
+ return () => {
3193
+ deliveries == null ? void 0 : deliveries.dispose();
3194
+ errors.dispose();
3195
+ ended == null ? void 0 : ended.dispose();
3196
+ finished == null ? void 0 : finished.dispose();
3197
+ dropped == null ? void 0 : dropped.dispose();
3198
+ };
3199
+ }, [trace, handle]);
3200
+ }
3201
+
3202
+ // src/hooks/useFollowScroll.ts
3203
+ import { useCallback as useCallback13, useLayoutEffect, useRef as useRef10, useState as useState10 } from "react";
3204
+ var FOLLOW_SLACK_PX = 24;
3205
+ function useFollowScroll() {
3206
+ const ref = useRef10(null);
3207
+ const [follow, setFollow] = useState10(true);
3208
+ useLayoutEffect(() => {
3209
+ const element = ref.current;
3210
+ if (follow && element !== null) {
3211
+ element.scrollTop = element.scrollHeight;
3212
+ }
3213
+ });
3214
+ const onScroll = useCallback13((event) => {
3215
+ const element = event.currentTarget;
3216
+ setFollow(element.scrollHeight - element.scrollTop - element.clientHeight <= FOLLOW_SLACK_PX);
3217
+ }, []);
3218
+ return { ref, onScroll, follow, setFollow };
3219
+ }
3220
+
3221
+ // src/panels/RunLog.tsx
3222
+ import { useState as useState11 } from "react";
3223
+ import { useSelection as useSelection4 } from "@hlang-tech/editor-react";
3224
+ import { jsx as jsx35, jsxs as jsxs22 } from "react/jsx-runtime";
3225
+ function RunLog({ core, onSelectNode, title = "Run log", leading }) {
3226
+ const trace = useRunTraceStore();
3227
+ useRunTraceVersion(trace);
3228
+ const selection = useSelection4(core);
3229
+ const [onlySelected, setOnlySelected] = useState11(false);
3230
+ const { ref: bodyRef, onScroll, follow, setFollow } = useFollowScroll();
3231
+ const all = trace === null ? [] : trace.entries();
3232
+ const selectedId = selection.selectedNodes.length === 1 ? selection.selectedNodes[0] : void 0;
3233
+ const filterId = onlySelected ? selectedId : void 0;
3234
+ const entries = filterId === void 0 ? all : all.filter((entry) => involves(entry, filterId));
3235
+ if (trace === null) {
3236
+ return null;
3237
+ }
3238
+ const label = (nodeId) => {
3239
+ var _a, _b;
3240
+ return (_b = (_a = core.model.getNode(nodeId)) == null ? void 0 : _a.name) != null ? _b : nodeId;
3241
+ };
3242
+ const choose = (nodeId) => {
3243
+ core.selection.selectNodes([nodeId], "replace");
3244
+ onSelectNode == null ? void 0 : onSelectNode(nodeId);
3245
+ };
3246
+ return /* @__PURE__ */ jsxs22("section", { className: "hl-runlog", "aria-label": title, children: [
3247
+ /* @__PURE__ */ jsxs22("header", { className: "hl-runlog-head", children: [
3248
+ leading != null ? leading : /* @__PURE__ */ jsx35("span", { className: "hl-runlog-title", children: title }),
3249
+ /* @__PURE__ */ jsxs22("span", { className: "hl-runlog-count hl-mono", children: [
3250
+ entries.length === all.length ? `${String(all.length)} events` : `${String(entries.length)} of ${String(all.length)} events`,
3251
+ trace.dropped > 0 ? ` \xB7 ${String(trace.dropped)} earlier not kept` : ""
3252
+ ] }),
3253
+ /* @__PURE__ */ jsxs22("span", { className: "hl-runlog-actions", children: [
3254
+ /* @__PURE__ */ jsx35(
3255
+ "button",
3256
+ {
3257
+ type: "button",
3258
+ className: `hl-runlog-toggle${onlySelected ? " is-on" : ""}`,
3259
+ "aria-pressed": onlySelected,
3260
+ disabled: selectedId === void 0 && !onlySelected,
3261
+ title: selectedId === void 0 ? "Select a node to filter by it" : `Only ${label(selectedId)}`,
3262
+ onClick: () => {
3263
+ setOnlySelected((value) => !value);
3264
+ },
3265
+ children: onlySelected && selectedId !== void 0 ? `Only ${label(selectedId)}` : "Selected node"
3266
+ }
3267
+ ),
3268
+ /* @__PURE__ */ jsx35(
3269
+ "button",
3270
+ {
3271
+ type: "button",
3272
+ className: `hl-runlog-toggle${follow ? " is-on" : ""}`,
3273
+ "aria-pressed": follow,
3274
+ onClick: () => {
3275
+ setFollow(!follow);
3276
+ },
3277
+ children: "Follow"
3278
+ }
3279
+ ),
3280
+ /* @__PURE__ */ jsx35(
3281
+ "button",
3282
+ {
3283
+ type: "button",
3284
+ className: "hl-runlog-toggle",
3285
+ disabled: all.length === 0,
3286
+ onClick: () => {
3287
+ trace.clear();
3288
+ },
3289
+ children: "Clear"
3290
+ }
3291
+ )
3292
+ ] })
3293
+ ] }),
3294
+ entries.length === 0 ? /* @__PURE__ */ jsx35("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__ */ jsx35("ol", { className: "hl-runlog-body hl-mono", ref: bodyRef, onScroll, children: entries.map((entry) => /* @__PURE__ */ jsx35(Row, { entry, label, onChoose: choose }, entry.seq)) })
3295
+ ] });
3296
+ }
3297
+ function Row({
3298
+ entry,
3299
+ label,
3300
+ onChoose
3301
+ }) {
3302
+ switch (entry.kind) {
3303
+ case "error":
3304
+ return /* @__PURE__ */ jsxs22(Event, { className: "is-error", at: entry.at, onChoose: () => onChoose(entry.nodeId), ms: "", children: [
3305
+ /* @__PURE__ */ jsx35(NodePort, { node: label(entry.nodeId), port: "error" }),
3306
+ /* @__PURE__ */ jsx35("span", { className: "hl-runlog-value", children: previewValue(entry.error) })
3307
+ ] });
3308
+ case "ended":
3309
+ return /* @__PURE__ */ jsxs22(Event, { className: "is-end", at: entry.at, onChoose: () => onChoose(entry.nodeId), ms: "", children: [
3310
+ /* @__PURE__ */ jsx35(NodePort, { node: label(entry.nodeId), port: entry.portId }),
3311
+ /* @__PURE__ */ jsx35("span", { className: "hl-runlog-value", children: "ended" })
3312
+ ] });
3313
+ case "finished":
3314
+ return /* @__PURE__ */ jsxs22(Event, { className: "is-end", at: entry.at, onChoose: () => onChoose(entry.nodeId), ms: "", children: [
3315
+ /* @__PURE__ */ jsx35(NodePort, { node: label(entry.nodeId), port: "" }),
3316
+ /* @__PURE__ */ jsx35("span", { className: "hl-runlog-value", children: "finished" })
3317
+ ] });
3318
+ case "dropped":
3319
+ return /* @__PURE__ */ jsxs22(Event, { className: "is-dropped", at: entry.at, onChoose: () => onChoose(entry.targetNodeId), ms: "", children: [
3320
+ /* @__PURE__ */ jsx35(Route, { entry, label }),
3321
+ /* @__PURE__ */ jsxs22("span", { className: "hl-runlog-value", children: [
3322
+ "dropped ",
3323
+ previewValue(entry.message)
3324
+ ] })
3325
+ ] });
3326
+ case "delivery":
3327
+ return /* @__PURE__ */ jsxs22(Event, { className: "", at: entry.at, onChoose: () => onChoose(entry.targetNodeId), ms: formatDuration(entry.duration), children: [
3328
+ /* @__PURE__ */ jsx35(Route, { entry, label }),
3329
+ /* @__PURE__ */ jsx35("span", { className: "hl-runlog-value", children: previewValue(entry.message) })
3330
+ ] });
3331
+ }
3332
+ }
3333
+ function Event({
3334
+ className: className2,
3335
+ at,
3336
+ ms,
3337
+ onChoose,
3338
+ children
3339
+ }) {
3340
+ return /* @__PURE__ */ jsx35("li", { className: `hl-runlog-row ${className2}`.trim(), children: /* @__PURE__ */ jsxs22("button", { type: "button", className: "hl-runlog-hit", onClick: onChoose, children: [
3341
+ /* @__PURE__ */ jsx35("span", { className: "hl-runlog-at", children: formatElapsed(at) }),
3342
+ children,
3343
+ /* @__PURE__ */ jsx35("span", { className: "hl-runlog-ms", children: ms })
3344
+ ] }) });
3345
+ }
3346
+ function NodePort({ node, port }) {
3347
+ return /* @__PURE__ */ jsxs22("span", { className: "hl-runlog-route", children: [
3348
+ /* @__PURE__ */ jsx35("span", { className: "hl-runlog-node", children: node }),
3349
+ port === "" ? null : /* @__PURE__ */ jsx35("span", { className: "hl-runlog-port", children: port })
3350
+ ] });
3351
+ }
3352
+ function Route({
3353
+ entry,
3354
+ label
3355
+ }) {
3356
+ return /* @__PURE__ */ jsxs22("span", { className: "hl-runlog-route", children: [
3357
+ /* @__PURE__ */ jsx35("span", { className: "hl-runlog-node", children: label(entry.sourceNodeId) }),
3358
+ /* @__PURE__ */ jsx35("span", { className: "hl-runlog-port", children: entry.sourcePortId }),
3359
+ /* @__PURE__ */ jsx35("span", { className: "hl-runlog-arrow", "aria-label": "to", children: "\u2192" }),
3360
+ /* @__PURE__ */ jsx35("span", { className: "hl-runlog-node", children: label(entry.targetNodeId) }),
3361
+ /* @__PURE__ */ jsx35("span", { className: "hl-runlog-port", children: entry.targetPortId })
3362
+ ] });
3363
+ }
2269
3364
  export {
2270
3365
  AssetGrid,
2271
3366
  AssetNodeChrome,
2272
3367
  CanvasChrome,
2273
3368
  CanvasDock,
2274
3369
  Cheatsheet,
3370
+ ConnectionInspector,
3371
+ DEFAULT_EDGE_CAPACITY,
3372
+ DEFAULT_MIN_CANVAS_HEIGHT,
2275
3373
  DiagnosticBadge,
2276
3374
  EditorShell,
2277
3375
  EditorUIThemeProvider,
2278
3376
  EmptyState,
3377
+ FOLLOW_SLACK_PX,
3378
+ FOOTER_KEY_STEP,
2279
3379
  FlowEdge,
3380
+ FooterResizer,
2280
3381
  HlangNode,
2281
3382
  InspectorPanel,
2282
3383
  KIND_ROLE,
2283
3384
  KIND_TOKEN,
3385
+ LivePulse,
2284
3386
  LiveSignalProvider,
2285
3387
  LiveSignalStore,
2286
3388
  NodeBodySlot,
2287
3389
  NodeChrome,
2288
3390
  NodeContextMenu,
2289
3391
  NodeInteractionProvider,
3392
+ NodeRunStats,
2290
3393
  NodeSearch,
2291
3394
  OutlineTree,
3395
+ PREVIEW_LENGTH,
2292
3396
  ParamForm,
2293
3397
  PillNodeChrome,
2294
3398
  PortRow,
2295
3399
  PositionPanel,
2296
3400
  PreviewPanel,
3401
+ RUN_LOG_CAPACITY,
2297
3402
  RailTabs,
2298
3403
  RunButton,
3404
+ RunLog,
3405
+ RunTrace,
3406
+ RunTraceProvider,
3407
+ SELF_LOOP_CLEARANCE,
3408
+ SELF_LOOP_RADIUS,
2299
3409
  SignalBar,
2300
3410
  TOKEN_NAMES,
2301
3411
  ToastProvider,
@@ -2304,26 +3414,47 @@ export {
2304
3414
  ToolbarGhostButton,
2305
3415
  ToolbarStat,
2306
3416
  UndoRedo,
3417
+ boundsFields,
3418
+ capacityLabel,
3419
+ clampFooter,
2307
3420
  connectableCandidates,
2308
3421
  createHlangFlowTypes,
2309
3422
  createTheme,
3423
+ defaultLiveDeps,
2310
3424
  deriveParamSchema,
2311
3425
  descriptorCandidates,
3426
+ footerBounds,
3427
+ formatDuration,
3428
+ formatElapsed,
2312
3429
  fuzzyRank,
2313
3430
  fuzzyScore,
3431
+ involves,
2314
3432
  isEditableTarget,
3433
+ nextBounds,
3434
+ previewValue,
3435
+ resizeByKey,
2315
3436
  searchNodes,
3437
+ selfLoopPath,
3438
+ toggleCollapsed,
2316
3439
  tokenVar,
3440
+ useEdgeBounds,
2317
3441
  useEdgeFlowing,
3442
+ useEdgePulse,
2318
3443
  useEditorHotkeys,
3444
+ useFollowScroll,
3445
+ useFooterResize,
2319
3446
  useLiveSignalStore,
2320
3447
  useLiveSignals,
2321
3448
  useNodeContextMenu,
2322
3449
  useNodeInteraction,
2323
3450
  useNodeLiveState,
2324
3451
  useNodeParams,
3452
+ useNodePulse,
2325
3453
  useNodeSearch,
2326
3454
  useNodeSearchTriggers,
3455
+ useRunTrace,
3456
+ useRunTraceStore,
3457
+ useRunTraceVersion,
2327
3458
  useToast
2328
3459
  };
2329
3460
  //# sourceMappingURL=index.mjs.map