@hlang-tech/editor-ui 0.3.0 → 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
@@ -66,12 +66,12 @@ function createTheme(overrides) {
66
66
  import { jsx } from "react/jsx-runtime";
67
67
  function EditorUIThemeProvider({
68
68
  theme,
69
- className,
69
+ className: className2,
70
70
  style,
71
71
  children
72
72
  }) {
73
73
  const themeStyle = theme ? createTheme(theme) : void 0;
74
- return /* @__PURE__ */ jsx("div", { className, style: { ...themeStyle, ...style }, children });
74
+ return /* @__PURE__ */ jsx("div", { className: className2, style: { ...themeStyle, ...style }, children });
75
75
  }
76
76
 
77
77
  // src/nodes/SignalBar.tsx
@@ -121,7 +121,10 @@ var ACTIVE_WINDOW_MS = 700;
121
121
  function portKey(nodeId, portId) {
122
122
  return `${nodeId}@${portId}`;
123
123
  }
124
- var defaultDeps = {
124
+ function connectionKey(sourceNodeId, sourcePortId, targetNodeId, targetPortId) {
125
+ return `${portKey(sourceNodeId, sourcePortId)}->${portKey(targetNodeId, targetPortId)}`;
126
+ }
127
+ var defaultLiveDeps = {
125
128
  now: () => typeof performance !== "undefined" ? performance.now() : Date.now(),
126
129
  schedule: (cb) => typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame(cb) : (
127
130
  // `window.setTimeout` returns a numeric handle (the DOM overload),
@@ -132,11 +135,13 @@ var defaultDeps = {
132
135
  cancel: (h) => typeof cancelAnimationFrame !== "undefined" ? cancelAnimationFrame(h) : window.clearTimeout(h)
133
136
  };
134
137
  var LiveSignalStore = class {
135
- constructor(deps = defaultDeps) {
138
+ constructor(deps = defaultLiveDeps) {
136
139
  /** nodeId -> last-active timestamp. */
137
140
  this.nodeActiveAt = /* @__PURE__ */ new Map();
138
141
  /** nodeId set that errored during this run. */
139
142
  this.erroredNodes = /* @__PURE__ */ new Set();
143
+ /** nodeId set that finished during this run (RFC 0001). */
144
+ this.finishedNodes = /* @__PURE__ */ new Set();
140
145
  /** edgeId -> last-flowing timestamp. */
141
146
  this.edgeFlowAt = /* @__PURE__ */ new Map();
142
147
  /** Per-id listeners. */
@@ -147,12 +152,21 @@ var LiveSignalStore = class {
147
152
  this.lastEdgeFlowing = /* @__PURE__ */ new Map();
148
153
  /** Edges sourced by a given (nodeId@portId), for pulse -> edge mapping. */
149
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();
150
163
  this.rafHandle = null;
151
164
  this.running = false;
152
165
  this.deps = deps;
153
166
  }
154
167
  /** Provide the current edge index (call whenever edges change). */
155
168
  setEdgeIndex(edges) {
169
+ var _a;
156
170
  const next = /* @__PURE__ */ new Map();
157
171
  for (const edge of edges) {
158
172
  const key = portKey(edge.sourceNodeId, edge.sourcePortId);
@@ -164,6 +178,44 @@ var LiveSignalStore = class {
164
178
  }
165
179
  }
166
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;
167
219
  }
168
220
  /** Record a node emitting on an output port (drives node + edge activity). */
169
221
  markActivity(nodeId, portId) {
@@ -177,6 +229,11 @@ var LiveSignalStore = class {
177
229
  }
178
230
  this.ensureLoop();
179
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
+ }
180
237
  /** Record a node erroring (sticky for the run). */
181
238
  markError(nodeId) {
182
239
  this.erroredNodes.add(nodeId);
@@ -186,7 +243,10 @@ var LiveSignalStore = class {
186
243
  reset() {
187
244
  this.nodeActiveAt.clear();
188
245
  this.erroredNodes.clear();
246
+ this.finishedNodes.clear();
189
247
  this.edgeFlowAt.clear();
248
+ this.nodePulse.clear();
249
+ this.edgePulse.clear();
190
250
  this.stopLoop();
191
251
  this.flushNotifications();
192
252
  }
@@ -207,7 +267,7 @@ var LiveSignalStore = class {
207
267
  if (at !== void 0 && this.deps.now() - at <= ACTIVE_WINDOW_MS) {
208
268
  return "active";
209
269
  }
210
- return "idle";
270
+ return this.finishedNodes.has(nodeId) ? "finished" : "idle";
211
271
  }
212
272
  /** Whether an edge is currently flowing. */
213
273
  isEdgeFlowing(edgeId) {
@@ -267,8 +327,10 @@ var LiveSignalStore = class {
267
327
  if (state !== "idle") {
268
328
  anyLive = anyLive || state === "active";
269
329
  }
270
- if (this.lastNodeState.get(nodeId) !== state) {
330
+ const pulse = this.getNodePulse(nodeId);
331
+ if (this.lastNodeState.get(nodeId) !== state || this.lastNodePulse.get(nodeId) !== pulse) {
271
332
  this.lastNodeState.set(nodeId, state);
333
+ this.lastNodePulse.set(nodeId, pulse);
272
334
  for (const listener of listeners) {
273
335
  listener();
274
336
  }
@@ -277,8 +339,10 @@ var LiveSignalStore = class {
277
339
  for (const [edgeId, listeners] of this.edgeListeners) {
278
340
  const flowing = this.isEdgeFlowing(edgeId);
279
341
  anyLive = anyLive || flowing;
280
- if (this.lastEdgeFlowing.get(edgeId) !== flowing) {
342
+ const pulse = this.getEdgePulse(edgeId);
343
+ if (this.lastEdgeFlowing.get(edgeId) !== flowing || this.lastEdgePulse.get(edgeId) !== pulse) {
281
344
  this.lastEdgeFlowing.set(edgeId, flowing);
345
+ this.lastEdgePulse.set(edgeId, pulse);
282
346
  for (const listener of listeners) {
283
347
  listener();
284
348
  }
@@ -306,6 +370,22 @@ function useNodeLiveState(nodeId) {
306
370
  () => "idle"
307
371
  );
308
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
+ }
309
389
  function useEdgeFlowing(edgeId) {
310
390
  const store = useContext(LiveSignalContext);
311
391
  return useSyncExternalStore(
@@ -315,8 +395,15 @@ function useEdgeFlowing(edgeId) {
315
395
  );
316
396
  }
317
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
+
318
405
  // src/nodes/AssetNode.tsx
319
- import { jsx as jsx6, jsxs as jsxs2 } from "react/jsx-runtime";
406
+ import { jsx as jsx7, jsxs as jsxs2 } from "react/jsx-runtime";
320
407
  function stringParam(node, key) {
321
408
  const v = node.params[key];
322
409
  return typeof v === "string" ? v : void 0;
@@ -328,22 +415,24 @@ function AssetNodeChrome({ node, selected }) {
328
415
  const role = stringParam(node, "role");
329
416
  const live = useNodeLiveState(node.id);
330
417
  const outputs = node.ports.filter((p) => p.direction === "O" && !p.isError);
331
- const className = [
418
+ const className2 = [
332
419
  "hl-asset",
333
420
  selected ? "is-selected" : "",
334
421
  live === "active" ? "is-active" : "",
335
- live === "error" ? "is-live-error" : ""
422
+ live === "error" ? "is-live-error" : "",
423
+ live === "finished" ? "is-finished" : ""
336
424
  ].filter(Boolean).join(" ");
337
- 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 }),
338
427
  /* @__PURE__ */ jsxs2("div", { className: "hl-asset-frame", children: [
339
- 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 }),
340
- /* @__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" }),
341
430
  /* @__PURE__ */ jsxs2("div", { className: "hl-asset-caption", children: [
342
- /* @__PURE__ */ jsx6("span", { className: "hl-asset-name", children: caption }),
343
- 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 })
344
433
  ] })
345
434
  ] }),
346
- /* @__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)) })
347
436
  ] });
348
437
  }
349
438
 
@@ -351,9 +440,9 @@ function AssetNodeChrome({ node, selected }) {
351
440
  import { useCallback, useEffect, useRef, useState } from "react";
352
441
 
353
442
  // src/icons/node-icons.tsx
354
- import { jsx as jsx7 } from "react/jsx-runtime";
443
+ import { jsx as jsx8 } from "react/jsx-runtime";
355
444
  function svg(size, children) {
356
- return /* @__PURE__ */ jsx7(
445
+ return /* @__PURE__ */ jsx8(
357
446
  "svg",
358
447
  {
359
448
  width: size,
@@ -371,49 +460,49 @@ function svg(size, children) {
371
460
  }
372
461
  var BY_CODE = {
373
462
  "image-source": (s) => svg(s, [
374
- /* @__PURE__ */ jsx7("rect", { x: "3", y: "4", width: "18", height: "16", rx: "2.5" }, "r"),
375
- /* @__PURE__ */ jsx7("circle", { cx: "8.5", cy: "9", r: "1.6" }, "c"),
376
- /* @__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")
377
466
  ]),
378
- "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")]),
379
468
  "interval-source": (s) => svg(s, [
380
- /* @__PURE__ */ jsx7("circle", { cx: "12", cy: "12", r: "8" }, "c"),
381
- /* @__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")
382
471
  ]),
383
472
  "range-source": (s) => svg(s, [
384
- /* @__PURE__ */ jsx7("path", { d: "M4 12h16" }, "l"),
385
- /* @__PURE__ */ jsx7("circle", { cx: "6", cy: "12", r: "2" }, "a"),
386
- /* @__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")
387
476
  ]),
388
- "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")]),
389
478
  "map-scale": (s) => svg(s, [
390
- /* @__PURE__ */ jsx7("path", { d: "M4 20V8l8-4" }, "a"),
391
- /* @__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")
392
481
  ]),
393
- uppercase: (s) => svg(s, [/* @__PURE__ */ jsx7("path", { d: "M5 18L9 6l4 12M6.5 14h5M15 10v8M15 10c3 0 3 4 0 4" }, "p")]),
394
- "filter-threshold": (s) => svg(s, [/* @__PURE__ */ jsx7("path", { d: "M4 5h16l-6 7v6l-4-2v-4z" }, "p")]),
395
- "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")]),
396
485
  delay: (s) => svg(s, [
397
- /* @__PURE__ */ jsx7("circle", { cx: "12", cy: "13", r: "7" }, "c"),
398
- /* @__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")
399
488
  ]),
400
- 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")]),
401
490
  "console-sink": (s) => svg(s, [
402
- /* @__PURE__ */ jsx7("rect", { x: "4", y: "5", width: "16", height: "12", rx: "2" }, "r"),
403
- /* @__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")
404
493
  ]),
405
494
  "collect-sink": (s) => svg(s, [
406
- /* @__PURE__ */ jsx7("path", { d: "M4 7h16M4 12h16M4 17h10" }, "p")
495
+ /* @__PURE__ */ jsx8("path", { d: "M4 7h16M4 12h16M4 17h10" }, "p")
407
496
  ])
408
497
  };
409
498
  var BY_KIND = {
410
- 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")]),
411
500
  // source ▷ (play)
412
- 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")]),
413
502
  // transform
414
503
  W: (s) => svg(s, [
415
- /* @__PURE__ */ jsx7("path", { d: "M5 4h11l3 3v13H5z" }, "p"),
416
- /* @__PURE__ */ jsx7("path", { d: "M9 4v5h6" }, "q")
504
+ /* @__PURE__ */ jsx8("path", { d: "M5 4h11l3 3v13H5z" }, "p"),
505
+ /* @__PURE__ */ jsx8("path", { d: "M9 4v5h6" }, "q")
417
506
  ])
418
507
  // sink (save)
419
508
  };
@@ -424,61 +513,61 @@ function NodeIcon({ code, kind, size = 15 }) {
424
513
  }
425
514
  function EyeIcon({ size = 14 }) {
426
515
  return svg(size, [
427
- /* @__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"),
428
- /* @__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")
429
518
  ]);
430
519
  }
431
520
  function EyeOffIcon({ size = 14 }) {
432
521
  return svg(size, [
433
- /* @__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"),
434
- /* @__PURE__ */ jsx7("path", { d: "M9.9 9.9a2.6 2.6 0 003.7 3.7" }, "e"),
435
- /* @__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")
436
525
  ]);
437
526
  }
438
527
  function UnlockIcon({ size = 14 }) {
439
528
  return svg(size, [
440
- /* @__PURE__ */ jsx7("rect", { x: "5", y: "11", width: "14", height: "9", rx: "2" }, "r"),
441
- /* @__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")
442
531
  ]);
443
532
  }
444
533
  function LockIcon({ size = 14 }) {
445
534
  return svg(size, [
446
- /* @__PURE__ */ jsx7("rect", { x: "5", y: "11", width: "14", height: "9", rx: "2" }, "r"),
447
- /* @__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")
448
537
  ]);
449
538
  }
450
539
  function WarningIcon({ size = 13 }) {
451
540
  return svg(size, [
452
- /* @__PURE__ */ jsx7("path", { d: "M12 4l9 15H3z" }, "t"),
453
- /* @__PURE__ */ jsx7("path", { d: "M12 10v4" }, "b"),
454
- /* @__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")
455
544
  ]);
456
545
  }
457
546
  function ErrorIcon({ size = 13 }) {
458
547
  return svg(size, [
459
- /* @__PURE__ */ jsx7("circle", { cx: "12", cy: "12", r: "9" }, "c"),
460
- /* @__PURE__ */ jsx7("path", { d: "M12 7v6" }, "b"),
461
- /* @__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")
462
551
  ]);
463
552
  }
464
553
 
465
554
  // src/panels/DiagnosticBadge.tsx
466
- import { jsx as jsx8 } from "react/jsx-runtime";
555
+ import { jsx as jsx9 } from "react/jsx-runtime";
467
556
  function DiagnosticBadge({ diagnostic, variant, size = 12 }) {
468
- return /* @__PURE__ */ jsx8(
557
+ return /* @__PURE__ */ jsx9(
469
558
  "span",
470
559
  {
471
560
  className: `${variant} hl-diag-${diagnostic.severity}`,
472
561
  title: diagnostic.message,
473
562
  "aria-label": `${diagnostic.severity}: ${diagnostic.message}`,
474
- 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 })
475
564
  }
476
565
  );
477
566
  }
478
567
 
479
568
  // src/nodes/NodeInteractionContext.tsx
480
569
  import { createContext as createContext2, useContext as useContext2 } from "react";
481
- import { jsx as jsx9 } from "react/jsx-runtime";
570
+ import { jsx as jsx10 } from "react/jsx-runtime";
482
571
  var NoopInteraction = {
483
572
  diagnosticFor: () => void 0,
484
573
  rename: () => void 0
@@ -488,14 +577,14 @@ function NodeInteractionProvider({
488
577
  value,
489
578
  children
490
579
  }) {
491
- return /* @__PURE__ */ jsx9(NodeInteractionCtx.Provider, { value, children });
580
+ return /* @__PURE__ */ jsx10(NodeInteractionCtx.Provider, { value, children });
492
581
  }
493
582
  function useNodeInteraction() {
494
583
  return useContext2(NodeInteractionCtx);
495
584
  }
496
585
 
497
586
  // src/nodes/PillNode.tsx
498
- import { jsx as jsx10, jsxs as jsxs3 } from "react/jsx-runtime";
587
+ import { jsx as jsx11, jsxs as jsxs3 } from "react/jsx-runtime";
499
588
  var DOUBLE_CLICK_MS = 320;
500
589
  function PillNodeChrome({ node, selected }) {
501
590
  const live = useNodeLiveState(node.id);
@@ -541,18 +630,20 @@ function PillNodeChrome({ node, selected }) {
541
630
  }, []);
542
631
  const inputs = node.ports.filter((p) => p.direction === "I" && !p.isError);
543
632
  const outputs = node.ports.filter((p) => p.direction === "O" && !p.isError);
544
- const className = [
633
+ const className2 = [
545
634
  "hl-pill",
546
635
  `hl-pill-${node.kind.toLowerCase()}`,
547
636
  selected ? "is-selected" : "",
548
637
  live === "active" ? "is-active" : "",
549
638
  live === "error" ? "is-live-error" : "",
639
+ live === "finished" ? "is-finished" : "",
550
640
  (diagnostic == null ? void 0 : diagnostic.severity) === "error" ? "has-diag-error" : "",
551
641
  (diagnostic == null ? void 0 : diagnostic.severity) === "warning" ? "has-diag-warning" : ""
552
642
  ].filter(Boolean).join(" ");
553
- return /* @__PURE__ */ jsxs3("div", { className, "data-kind": node.kind, children: [
554
- /* @__PURE__ */ jsx10("span", { className: "hl-pill-dot", "aria-hidden": true, children: /* @__PURE__ */ jsx10(NodeIcon, { code: node.code, kind: node.kind, size: 13 }) }),
555
- 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(
556
647
  "input",
557
648
  {
558
649
  ref: inputRef,
@@ -564,7 +655,7 @@ function PillNodeChrome({ node, selected }) {
564
655
  spellCheck: false,
565
656
  "aria-label": "Rename node"
566
657
  }
567
- ) : /* @__PURE__ */ jsx10(
658
+ ) : /* @__PURE__ */ jsx11(
568
659
  "span",
569
660
  {
570
661
  className: "hl-pill-name",
@@ -575,60 +666,167 @@ function PillNodeChrome({ node, selected }) {
575
666
  children: node.name
576
667
  }
577
668
  ),
578
- diagnostic !== void 0 && /* @__PURE__ */ jsx10(DiagnosticBadge, { diagnostic, variant: "hl-pill-diag", size: 12 }),
579
- /* @__PURE__ */ jsx10("span", { className: "hl-pill-status", "aria-hidden": true }),
580
- /* @__PURE__ */ jsx10("ul", { className: "hl-pill-ports hl-in", children: inputs.map((port) => /* @__PURE__ */ jsx10(PortRow, { port }, port.id)) }),
581
- /* @__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)) })
582
673
  ] });
583
674
  }
584
675
 
585
676
  // src/nodes/HlangNode.tsx
586
- import { jsx as jsx11, jsxs as jsxs4 } from "react/jsx-runtime";
677
+ import { jsx as jsx12, jsxs as jsxs4 } from "react/jsx-runtime";
587
678
  function NodeChrome({ node, selected, body }) {
588
679
  const inputs = node.ports.filter((p) => p.direction === "I");
589
680
  const outputs = node.ports.filter((p) => p.direction === "O");
590
681
  const live = useNodeLiveState(node.id);
591
- const className = [
682
+ const className2 = [
592
683
  "hl-node",
593
684
  `hl-${node.kind.toLowerCase()}`,
594
685
  selected ? "is-selected" : "",
595
686
  live === "active" ? "is-active" : "",
596
- live === "error" ? "is-live-error" : ""
687
+ live === "error" ? "is-live-error" : "",
688
+ live === "finished" ? "is-finished" : ""
597
689
  ].filter(Boolean).join(" ");
598
- return /* @__PURE__ */ jsxs4("div", { className, "data-kind": node.kind, children: [
599
- /* @__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, {}),
600
693
  /* @__PURE__ */ jsxs4("header", { className: "hl-head", children: [
601
- /* @__PURE__ */ jsx11("span", { className: "hl-kind", children: node.kind }),
602
- /* @__PURE__ */ jsx11("span", { className: "hl-name", children: node.name }),
603
- /* @__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] })
604
697
  ] }),
605
- /* @__PURE__ */ jsx11(NodeBodySlot, { node, children: body }),
698
+ /* @__PURE__ */ jsx12(NodeBodySlot, { node, children: body }),
606
699
  /* @__PURE__ */ jsxs4("div", { className: "hl-ports", children: [
607
- /* @__PURE__ */ jsx11("ul", { className: "hl-col hl-in", children: inputs.map((port) => /* @__PURE__ */ jsx11(PortRow, { port }, port.id)) }),
608
- /* @__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)) })
609
702
  ] })
610
703
  ] });
611
704
  }
612
705
  function HlangNode({ data, selected }) {
613
706
  const node = data.editorNode;
614
707
  if (typeof node.params.imageUrl === "string" && node.params.imageUrl.length > 0) {
615
- return /* @__PURE__ */ jsx11(AssetNodeChrome, { node, selected });
708
+ return /* @__PURE__ */ jsx12(AssetNodeChrome, { node, selected });
616
709
  }
617
- return /* @__PURE__ */ jsx11(PillNodeChrome, { node, selected });
710
+ return /* @__PURE__ */ jsx12(PillNodeChrome, { node, selected });
618
711
  }
619
712
 
620
713
  // src/edges/FlowEdge.tsx
621
714
  import { BaseEdge, EdgeLabelRenderer, getBezierPath, useInternalNode } from "@xyflow/react";
622
- import { Fragment, jsx as jsx12, jsxs as jsxs5 } from "react/jsx-runtime";
623
- function useSourceRole(sourceId) {
624
- const node = useInternalNode(sourceId);
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) {
625
814
  const role = node == null ? void 0 : node.data.editorNode.params.role;
626
815
  return typeof role === "string" && role.length > 0 ? role : void 0;
627
816
  }
628
817
  function FlowEdge(props) {
629
- var _a;
630
- const { id, source, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, data } = props;
631
- 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({
632
830
  sourceX,
633
831
  sourceY,
634
832
  sourcePosition,
@@ -636,25 +834,39 @@ function FlowEdge(props) {
636
834
  targetY,
637
835
  targetPosition
638
836
  });
837
+ const path = (_a = loop == null ? void 0 : loop.path) != null ? _a : curve;
639
838
  const t = 0.62;
640
- const labelX = sourceX + (targetX - sourceX) * t;
641
- const labelY = sourceY + (targetY - sourceY) * t;
642
- 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;
643
845
  const isValue = (data == null ? void 0 : data.stream) === "value";
644
846
  const flowing = useEdgeFlowing(id);
645
- const role = useSourceRole(source);
646
- const className = [
847
+ const pulse = useEdgePulse(id);
848
+ const className2 = [
647
849
  "hl-edge",
648
850
  isError ? "is-error" : "",
649
851
  isValue ? "is-value" : "is-live",
650
- flowing ? "is-flowing" : ""
852
+ flowing ? "is-flowing" : "",
853
+ selected === true ? "is-selected" : ""
651
854
  ].filter(Boolean).join(" ");
652
855
  return /* @__PURE__ */ jsxs5(Fragment, { children: [
653
- /* @__PURE__ */ jsxs5("g", { className, children: [
654
- /* @__PURE__ */ jsx12(BaseEdge, { id, path, className: "hl-edge-base" }),
655
- /* @__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)
656
860
  ] }),
657
- 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(
658
870
  "div",
659
871
  {
660
872
  className: "hl-edge-label hl-mono",
@@ -676,49 +888,257 @@ function createHlangFlowTypes() {
676
888
  }
677
889
 
678
890
  // src/shell/EditorShell.tsx
679
- 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";
680
1055
  function EditorShell({
681
1056
  toolbar,
682
1057
  left,
683
1058
  right,
684
1059
  canvas,
685
1060
  footer,
1061
+ footerResize,
686
1062
  leftCollapsed = false,
687
1063
  rightCollapsed = false
688
1064
  }) {
689
- const className = [
1065
+ const shellRef = useRef3(null);
1066
+ const toolbarRef = useRef3(null);
1067
+ const className2 = [
690
1068
  "hl-shell",
691
1069
  leftCollapsed ? "hl-left-collapsed" : "",
692
1070
  rightCollapsed ? "hl-right-collapsed" : ""
693
1071
  ].filter(Boolean).join(" ");
694
- return /* @__PURE__ */ jsxs6("div", { className, children: [
695
- 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 }),
696
1074
  /* @__PURE__ */ jsxs6("div", { className: "hl-region-body", children: [
697
- left !== void 0 && !leftCollapsed && /* @__PURE__ */ jsx13("aside", { className: "hl-region-left", children: left }),
698
- /* @__PURE__ */ jsx13("main", { className: "hl-region-canvas", children: canvas }),
699
- 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 })
700
1078
  ] }),
701
- 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 }))
702
1080
  ] });
703
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
+ }
704
1124
 
705
1125
  // src/toolbar/Toolbar.tsx
706
1126
  import { useHistory } from "@hlang-tech/editor-react";
707
- 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";
708
1128
  function Toolbar({ title, center, actions }) {
709
1129
  return /* @__PURE__ */ jsxs7("header", { className: "hl-toolbar", children: [
710
- /* @__PURE__ */ jsx14("div", { className: "hl-toolbar-brand", children: title != null ? title : /* @__PURE__ */ jsxs7(Fragment2, { children: [
711
- /* @__PURE__ */ jsx14("span", { className: "hl-toolbar-glyph", "aria-hidden": "true" }),
712
- /* @__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" })
713
1133
  ] }) }),
714
- center !== void 0 && /* @__PURE__ */ jsx14("div", { className: "hl-toolbar-center", children: center }),
715
- /* @__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 })
716
1136
  ] });
717
1137
  }
718
1138
  function UndoRedo({ core }) {
719
1139
  const history = useHistory(core);
720
1140
  return /* @__PURE__ */ jsxs7(Fragment2, { children: [
721
- /* @__PURE__ */ jsx14(
1141
+ /* @__PURE__ */ jsx16(
722
1142
  "button",
723
1143
  {
724
1144
  type: "button",
@@ -729,7 +1149,7 @@ function UndoRedo({ core }) {
729
1149
  children: "Undo"
730
1150
  }
731
1151
  ),
732
- /* @__PURE__ */ jsx14(
1152
+ /* @__PURE__ */ jsx16(
733
1153
  "button",
734
1154
  {
735
1155
  type: "button",
@@ -756,7 +1176,7 @@ function RunButton({
756
1176
  onClick: onRun,
757
1177
  "aria-pressed": running,
758
1178
  children: [
759
- /* @__PURE__ */ jsx14(
1179
+ /* @__PURE__ */ jsx16(
760
1180
  "span",
761
1181
  {
762
1182
  className: `hl-toolbar-run-icon${running ? " is-stop" : ""}`,
@@ -769,22 +1189,22 @@ function RunButton({
769
1189
  );
770
1190
  }
771
1191
  function ToolbarStat({ label, value }) {
772
- 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 });
773
1193
  }
774
1194
  function ToolbarGhostButton({
775
1195
  children,
776
1196
  onClick,
777
1197
  title
778
1198
  }) {
779
- 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 });
780
1200
  }
781
1201
 
782
1202
  // src/toolbar/ToolCluster.tsx
783
- import { jsx as jsx15, jsxs as jsxs8 } from "react/jsx-runtime";
1203
+ import { jsx as jsx17, jsxs as jsxs8 } from "react/jsx-runtime";
784
1204
  function ToolCluster({ items }) {
785
- 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: [
786
- item.separatorBefore === true && /* @__PURE__ */ jsx15("span", { className: "hl-toolcluster-sep", "aria-hidden": true }),
787
- /* @__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(
788
1208
  "button",
789
1209
  {
790
1210
  type: "button",
@@ -802,30 +1222,30 @@ function ToolCluster({ items }) {
802
1222
 
803
1223
  // src/canvas/CanvasChrome.tsx
804
1224
  import { Background, BackgroundVariant } from "@xyflow/react";
805
- import { Fragment as Fragment3, jsx as jsx16 } from "react/jsx-runtime";
1225
+ import { Fragment as Fragment3, jsx as jsx18 } from "react/jsx-runtime";
806
1226
  function CanvasChrome({ background = true }) {
807
- 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" }) });
808
1228
  }
809
1229
 
810
1230
  // src/canvas/CanvasDock.tsx
811
- import { jsx as jsx17, jsxs as jsxs9 } from "react/jsx-runtime";
1231
+ import { jsx as jsx19, jsxs as jsxs9 } from "react/jsx-runtime";
812
1232
  function CursorIcon() {
813
- 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" }) });
814
1234
  }
815
1235
  function HandIcon() {
816
- 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" }) });
817
1237
  }
818
1238
  function PlusIcon() {
819
- 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" }) });
820
1240
  }
821
1241
  function FitIcon() {
822
- 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" }) });
823
1243
  }
824
1244
  function UndoIcon() {
825
- 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" }) });
826
1246
  }
827
1247
  function RedoIcon() {
828
- 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" }) });
829
1249
  }
830
1250
  function CanvasDock({
831
1251
  zoom,
@@ -842,7 +1262,7 @@ function CanvasDock({
842
1262
  }) {
843
1263
  const pct = `${String(Math.round(zoom * 100))}%`;
844
1264
  return /* @__PURE__ */ jsxs9("div", { className: "hl-dock", role: "toolbar", "aria-label": "Canvas tools", children: [
845
- /* @__PURE__ */ jsx17(
1265
+ /* @__PURE__ */ jsx19(
846
1266
  "button",
847
1267
  {
848
1268
  className: "hl-dock-btn",
@@ -850,10 +1270,10 @@ function CanvasDock({
850
1270
  disabled: !canUndo,
851
1271
  title: "Undo (\u2318Z)",
852
1272
  "aria-label": "Undo",
853
- children: /* @__PURE__ */ jsx17(UndoIcon, {})
1273
+ children: /* @__PURE__ */ jsx19(UndoIcon, {})
854
1274
  }
855
1275
  ),
856
- /* @__PURE__ */ jsx17(
1276
+ /* @__PURE__ */ jsx19(
857
1277
  "button",
858
1278
  {
859
1279
  className: "hl-dock-btn",
@@ -861,48 +1281,48 @@ function CanvasDock({
861
1281
  disabled: !canRedo,
862
1282
  title: "Redo (\u2318\u21E7Z)",
863
1283
  "aria-label": "Redo",
864
- children: /* @__PURE__ */ jsx17(RedoIcon, {})
1284
+ children: /* @__PURE__ */ jsx19(RedoIcon, {})
865
1285
  }
866
1286
  ),
867
- /* @__PURE__ */ jsx17("span", { className: "hl-dock-sep", "aria-hidden": true }),
868
- /* @__PURE__ */ jsx17(
1287
+ /* @__PURE__ */ jsx19("span", { className: "hl-dock-sep", "aria-hidden": true }),
1288
+ /* @__PURE__ */ jsx19(
869
1289
  "button",
870
1290
  {
871
1291
  className: "hl-dock-btn" + (mode === "select" ? " is-active" : ""),
872
1292
  onClick: () => onModeChange("select"),
873
1293
  title: "Select (V)",
874
1294
  "aria-pressed": mode === "select",
875
- children: /* @__PURE__ */ jsx17(CursorIcon, {})
1295
+ children: /* @__PURE__ */ jsx19(CursorIcon, {})
876
1296
  }
877
1297
  ),
878
- /* @__PURE__ */ jsx17(
1298
+ /* @__PURE__ */ jsx19(
879
1299
  "button",
880
1300
  {
881
1301
  className: "hl-dock-btn" + (mode === "pan" ? " is-active" : ""),
882
1302
  onClick: () => onModeChange("pan"),
883
1303
  title: "Pan (Space)",
884
1304
  "aria-pressed": mode === "pan",
885
- children: /* @__PURE__ */ jsx17(HandIcon, {})
1305
+ children: /* @__PURE__ */ jsx19(HandIcon, {})
886
1306
  }
887
1307
  ),
888
- /* @__PURE__ */ jsx17("button", { className: "hl-dock-btn hl-dock-add", onClick: onAdd, title: "Add node", children: /* @__PURE__ */ jsx17(PlusIcon, {}) }),
889
- /* @__PURE__ */ jsx17("span", { className: "hl-dock-sep", "aria-hidden": true }),
890
- /* @__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" }) }),
891
- /* @__PURE__ */ jsx17("button", { className: "hl-dock-zoom hl-mono", onClick: onFitView, title: "Fit view", children: pct }),
892
- /* @__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: "+" }) }),
893
- /* @__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, {}) })
894
1314
  ] });
895
1315
  }
896
1316
 
897
1317
  // src/canvas/EmptyState.tsx
898
- import { jsx as jsx18, jsxs as jsxs10 } from "react/jsx-runtime";
1318
+ import { jsx as jsx20, jsxs as jsxs10 } from "react/jsx-runtime";
899
1319
  function EmptyState({
900
1320
  title = "An empty canvas.",
901
1321
  hint = "add a source \xB7 wire it to a sink \xB7 run"
902
1322
  }) {
903
1323
  return /* @__PURE__ */ jsxs10("div", { className: "hl-empty", "aria-hidden": "true", children: [
904
- /* @__PURE__ */ jsx18("p", { className: "hl-empty-title", children: title }),
905
- /* @__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 })
906
1326
  ] });
907
1327
  }
908
1328
 
@@ -938,11 +1358,11 @@ function deriveParamSchema(descriptor, node) {
938
1358
  }
939
1359
 
940
1360
  // src/hooks/useNodeParams.ts
941
- 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";
942
1362
  import { UpdateParamsCommand } from "@hlang-tech/editor-core";
943
1363
  function useNodeParams(core, nodeId) {
944
- const versionRef = useRef2(0);
945
- const subscribe = useCallback2(
1364
+ const versionRef = useRef4(0);
1365
+ const subscribe = useCallback3(
946
1366
  (onStoreChange) => {
947
1367
  const disposable = core.events.on("nodeChanged", (event) => {
948
1368
  if (nodeId !== void 0 && event.node.id === nodeId) {
@@ -956,9 +1376,9 @@ function useNodeParams(core, nodeId) {
956
1376
  },
957
1377
  [core, nodeId]
958
1378
  );
959
- const getSnapshot = useCallback2(() => versionRef.current, []);
1379
+ const getSnapshot = useCallback3(() => versionRef.current, []);
960
1380
  const version = useSyncExternalStore2(subscribe, getSnapshot, getSnapshot);
961
- const setParams = useCallback2(
1381
+ const setParams = useCallback3(
962
1382
  (next) => {
963
1383
  if (nodeId === void 0) {
964
1384
  return;
@@ -967,7 +1387,7 @@ function useNodeParams(core, nodeId) {
967
1387
  },
968
1388
  [core, nodeId]
969
1389
  );
970
- const setParam = useCallback2(
1390
+ const setParam = useCallback3(
971
1391
  (key, value) => {
972
1392
  if (nodeId === void 0) {
973
1393
  return;
@@ -980,7 +1400,7 @@ function useNodeParams(core, nodeId) {
980
1400
  },
981
1401
  [core, nodeId, setParams]
982
1402
  );
983
- return useMemo2(
1403
+ return useMemo3(
984
1404
  () => ({
985
1405
  node: nodeId === void 0 ? void 0 : core.model.getNode(nodeId),
986
1406
  setParam,
@@ -992,19 +1412,58 @@ function useNodeParams(core, nodeId) {
992
1412
  );
993
1413
  }
994
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
+
995
1454
  // src/panels/ParamForm.tsx
996
- import { useState as useState2 } from "react";
997
- 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";
998
1457
  function ParamForm({ fields, onCommit }) {
999
- 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)) });
1000
1459
  }
1001
1460
  function FieldRow({
1002
1461
  field,
1003
1462
  onCommit
1004
1463
  }) {
1005
1464
  return /* @__PURE__ */ jsxs11("label", { className: "hl-field", children: [
1006
- /* @__PURE__ */ jsx19("span", { className: "hl-field-label", children: field.label }),
1007
- /* @__PURE__ */ jsx19(FieldControl, { field, onCommit })
1465
+ /* @__PURE__ */ jsx21("span", { className: "hl-field-label", children: field.label }),
1466
+ /* @__PURE__ */ jsx21(FieldControl, { field, onCommit })
1008
1467
  ] });
1009
1468
  }
1010
1469
  function FieldControl({
@@ -1013,7 +1472,7 @@ function FieldControl({
1013
1472
  }) {
1014
1473
  switch (field.type) {
1015
1474
  case "toggle":
1016
- return /* @__PURE__ */ jsx19(
1475
+ return /* @__PURE__ */ jsx21(
1017
1476
  "button",
1018
1477
  {
1019
1478
  type: "button",
@@ -1022,17 +1481,17 @@ function FieldControl({
1022
1481
  className: `hl-toggle${field.value ? " is-on" : ""}`,
1023
1482
  disabled: field.disabled,
1024
1483
  onClick: () => onCommit(field.key, !field.value),
1025
- children: /* @__PURE__ */ jsx19("span", { className: "hl-toggle-knob" })
1484
+ children: /* @__PURE__ */ jsx21("span", { className: "hl-toggle-knob" })
1026
1485
  }
1027
1486
  );
1028
1487
  case "number":
1029
- return /* @__PURE__ */ jsx19(NumberControl, { field, onCommit });
1488
+ return /* @__PURE__ */ jsx21(NumberControl, { field, onCommit });
1030
1489
  case "textarea":
1031
- return /* @__PURE__ */ jsx19(TextControl, { field, onCommit, multiline: true });
1490
+ return /* @__PURE__ */ jsx21(TextControl, { field, onCommit, multiline: true });
1032
1491
  case "text":
1033
- return /* @__PURE__ */ jsx19(TextControl, { field, onCommit });
1492
+ return /* @__PURE__ */ jsx21(TextControl, { field, onCommit });
1034
1493
  case "custom":
1035
- 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) });
1036
1495
  }
1037
1496
  }
1038
1497
  function TextControl({
@@ -1040,7 +1499,7 @@ function TextControl({
1040
1499
  onCommit,
1041
1500
  multiline = false
1042
1501
  }) {
1043
- const [draft, setDraft] = useState2(field.value);
1502
+ const [draft, setDraft] = useState3(field.value);
1044
1503
  const commit = () => {
1045
1504
  if (draft !== field.value) {
1046
1505
  onCommit(field.key, draft);
@@ -1054,7 +1513,7 @@ function TextControl({
1054
1513
  onBlur: commit
1055
1514
  };
1056
1515
  if (multiline) {
1057
- return /* @__PURE__ */ jsx19(
1516
+ return /* @__PURE__ */ jsx21(
1058
1517
  "textarea",
1059
1518
  {
1060
1519
  ...shared,
@@ -1063,7 +1522,7 @@ function TextControl({
1063
1522
  }
1064
1523
  );
1065
1524
  }
1066
- return /* @__PURE__ */ jsx19(
1525
+ return /* @__PURE__ */ jsx21(
1067
1526
  "input",
1068
1527
  {
1069
1528
  ...shared,
@@ -1081,14 +1540,14 @@ function NumberControl({
1081
1540
  field,
1082
1541
  onCommit
1083
1542
  }) {
1084
- const [draft, setDraft] = useState2(String(field.value));
1543
+ const [draft, setDraft] = useState3(String(field.value));
1085
1544
  const commit = () => {
1086
1545
  const parsed = Number(draft);
1087
1546
  if (!Number.isNaN(parsed) && parsed !== field.value) {
1088
1547
  onCommit(field.key, parsed);
1089
1548
  }
1090
1549
  };
1091
- return /* @__PURE__ */ jsx19(
1550
+ return /* @__PURE__ */ jsx21(
1092
1551
  "input",
1093
1552
  {
1094
1553
  className: "hl-input hl-mono",
@@ -1119,40 +1578,267 @@ function describeCustom(value) {
1119
1578
  return String(value);
1120
1579
  }
1121
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
+
1122
1804
  // src/panels/InspectorPanel.tsx
1123
- import { jsx as jsx20, jsxs as jsxs12 } from "react/jsx-runtime";
1805
+ import { jsx as jsx25, jsxs as jsxs14 } from "react/jsx-runtime";
1124
1806
  function InspectorPanel({ core, emptyState }) {
1125
1807
  const selection = useSelection(core);
1126
1808
  const selectedNodes2 = selection.selectedNodes;
1127
1809
  const singleId = selectedNodes2.length === 1 ? selectedNodes2[0] : void 0;
1128
1810
  const params = useNodeParams(core, singleId);
1129
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
+ }
1130
1816
  if (node === void 0) {
1131
- return /* @__PURE__ */ jsx20("div", { className: "hl-inspector hl-inspector-empty", children: emptyState != null ? emptyState : /* @__PURE__ */ jsxs12("div", { className: "hl-inspector-blank", children: [
1132
- /* @__PURE__ */ jsx20("span", { className: "hl-inspector-blank-badge", "aria-hidden": true }),
1133
- /* @__PURE__ */ jsx20("p", { className: "hl-inspector-blank-title", children: selectedNodes2.length > 1 ? `${String(selectedNodes2.length)} nodes selected` : "Nothing selected" }),
1134
- /* @__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." })
1135
1821
  ] }) });
1136
1822
  }
1137
1823
  const descriptor = core.registry.get(node.code);
1138
1824
  const fields = descriptor === void 0 ? [] : deriveParamSchema(descriptor, node);
1139
1825
  const imageUrl = typeof node.params.imageUrl === "string" && node.params.imageUrl.length > 0 ? node.params.imageUrl : void 0;
1140
- return /* @__PURE__ */ jsxs12("div", { className: "hl-inspector", children: [
1141
- /* @__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 }) }),
1142
- /* @__PURE__ */ jsxs12("header", { className: "hl-inspector-head", children: [
1143
- /* @__PURE__ */ jsx20("span", { className: `hl-inspector-kind hl-${node.kind.toLowerCase()}`, children: node.kind }),
1144
- /* @__PURE__ */ jsxs12("div", { className: "hl-inspector-titles", children: [
1145
- /* @__PURE__ */ jsx20("span", { className: "hl-inspector-name", children: node.name }),
1146
- /* @__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: [
1147
1833
  node.code,
1148
1834
  " \xB7 ",
1149
1835
  KIND_ROLE[node.kind]
1150
1836
  ] })
1151
1837
  ] })
1152
1838
  ] }),
1153
- /* @__PURE__ */ jsxs12("section", { className: "hl-inspector-section", children: [
1154
- /* @__PURE__ */ jsx20("h3", { className: "hl-inspector-section-title", children: "Parameters" }),
1155
- 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(
1156
1842
  ParamForm,
1157
1843
  {
1158
1844
  fields,
@@ -1160,16 +1846,17 @@ function InspectorPanel({ core, emptyState }) {
1160
1846
  }
1161
1847
  )
1162
1848
  ] }),
1163
- /* @__PURE__ */ jsxs12("section", { className: "hl-inspector-section", children: [
1164
- /* @__PURE__ */ jsx20("h3", { className: "hl-inspector-section-title", children: "Ports" }),
1165
- /* @__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(
1166
1853
  "li",
1167
1854
  {
1168
1855
  className: `hl-inspector-port${port.isError ? " is-error" : ""}`,
1169
1856
  children: [
1170
- /* @__PURE__ */ jsx20("span", { className: `hl-port-dot hl-dir-${port.direction.toLowerCase()}` }),
1171
- /* @__PURE__ */ jsx20("span", { className: "hl-mono", children: port.name }),
1172
- /* @__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 })
1173
1860
  ]
1174
1861
  },
1175
1862
  port.id
@@ -1179,14 +1866,14 @@ function InspectorPanel({ core, emptyState }) {
1179
1866
  }
1180
1867
 
1181
1868
  // src/panels/OutlineTree.tsx
1182
- 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";
1183
1870
  import { GraphTopology } from "@hlang-tech/editor-core";
1184
1871
  import { useSelection as useSelection2, useViewState, useDiagnostics } from "@hlang-tech/editor-react";
1185
- import { jsx as jsx21, jsxs as jsxs13 } from "react/jsx-runtime";
1872
+ import { jsx as jsx26, jsxs as jsxs15 } from "react/jsx-runtime";
1186
1873
  function useTopologyComponents(core) {
1187
- const [tick, setTick] = useState3(0);
1188
- const bump = useCallback3(() => setTick((t) => t + 1), []);
1189
- useEffect2(() => {
1874
+ const [tick, setTick] = useState4(0);
1875
+ const bump = useCallback5(() => setTick((t) => t + 1), []);
1876
+ useEffect3(() => {
1190
1877
  const kinds = ["nodeAdded", "nodeRemoved", "nodeChanged", "edgeAdded", "edgeRemoved"];
1191
1878
  const disposers = kinds.map((k) => core.events.on(k, bump));
1192
1879
  bump();
@@ -1196,7 +1883,7 @@ function useTopologyComponents(core) {
1196
1883
  }
1197
1884
  };
1198
1885
  }, [core, bump]);
1199
- return useMemo3(() => {
1886
+ return useMemo4(() => {
1200
1887
  void tick;
1201
1888
  const topo = new GraphTopology();
1202
1889
  topo.reset(core.model.getNodes(), core.model.getEdges());
@@ -1208,15 +1895,15 @@ function OutlineTree({ core, onFocus }) {
1208
1895
  const selection = useSelection2(core);
1209
1896
  const viewState = useViewState(core);
1210
1897
  const diagnostics = useDiagnostics(core);
1211
- const [collapsed, setCollapsed] = useState3(/* @__PURE__ */ new Set());
1212
- const nodeById = useMemo3(() => {
1898
+ const [collapsed, setCollapsed] = useState4(/* @__PURE__ */ new Set());
1899
+ const nodeById = useMemo4(() => {
1213
1900
  const m = /* @__PURE__ */ new Map();
1214
1901
  for (const n of core.model.getNodes()) {
1215
1902
  m.set(n.id, n);
1216
1903
  }
1217
1904
  return m;
1218
1905
  }, [core, components]);
1219
- const toggleGroup = useCallback3((id) => {
1906
+ const toggleGroup = useCallback5((id) => {
1220
1907
  setCollapsed((prev) => {
1221
1908
  const next = new Set(prev);
1222
1909
  if (next.has(id)) {
@@ -1227,7 +1914,7 @@ function OutlineTree({ core, onFocus }) {
1227
1914
  return next;
1228
1915
  });
1229
1916
  }, []);
1230
- const select = useCallback3(
1917
+ const select = useCallback5(
1231
1918
  (id) => {
1232
1919
  selection.selectNodes([id], "replace");
1233
1920
  onFocus == null ? void 0 : onFocus(id);
@@ -1235,80 +1922,80 @@ function OutlineTree({ core, onFocus }) {
1235
1922
  [selection, onFocus]
1236
1923
  );
1237
1924
  if (components.length === 0) {
1238
- 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" });
1239
1926
  }
1240
1927
  const groups = [...components].sort((a, b) => b.nodeIds.size - a.nodeIds.size);
1241
- 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) => {
1242
1929
  const members = [...group.nodeIds].map((id) => nodeById.get(id)).filter((n) => n !== void 0);
1243
1930
  const open = !collapsed.has(group.id);
1244
1931
  const groupName = `Flow ${String(gi + 1)}`;
1245
- return /* @__PURE__ */ jsxs13("div", { className: "hl-outline-group", role: "treeitem", "aria-expanded": open, children: [
1246
- /* @__PURE__ */ jsxs13(
1932
+ return /* @__PURE__ */ jsxs15("div", { className: "hl-outline-group", role: "treeitem", "aria-expanded": open, children: [
1933
+ /* @__PURE__ */ jsxs15(
1247
1934
  "button",
1248
1935
  {
1249
1936
  className: "hl-outline-grouphead",
1250
1937
  onClick: () => toggleGroup(group.id),
1251
1938
  children: [
1252
- /* @__PURE__ */ jsx21("span", { className: "hl-outline-caret" + (open ? " is-open" : ""), "aria-hidden": true, children: "\u25B8" }),
1253
- /* @__PURE__ */ jsx21("span", { className: "hl-outline-groupname", children: groupName }),
1254
- /* @__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 })
1255
1942
  ]
1256
1943
  }
1257
1944
  ),
1258
- 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) => {
1259
1946
  const active = selection.isSelected(node.id);
1260
1947
  const isHidden = viewState.isHidden(node.id);
1261
1948
  const isLocked = viewState.isLocked(node.id);
1262
1949
  const { imageUrl } = node.params;
1263
1950
  const thumbnail = typeof imageUrl === "string" && imageUrl.length > 0 ? imageUrl : void 0;
1264
- return /* @__PURE__ */ jsx21("li", { children: /* @__PURE__ */ jsxs13(
1951
+ return /* @__PURE__ */ jsx26("li", { children: /* @__PURE__ */ jsxs15(
1265
1952
  "div",
1266
1953
  {
1267
1954
  className: "hl-outline-row" + (active ? " is-active" : "") + (isHidden ? " is-hidden" : "") + (isLocked ? " is-locked" : ""),
1268
1955
  children: [
1269
- /* @__PURE__ */ jsxs13("button", { className: "hl-outline-main", onClick: () => select(node.id), children: [
1270
- /* @__PURE__ */ jsx21(
1956
+ /* @__PURE__ */ jsxs15("button", { className: "hl-outline-main", onClick: () => select(node.id), children: [
1957
+ /* @__PURE__ */ jsx26(
1271
1958
  "span",
1272
1959
  {
1273
1960
  className: `hl-outline-icon hl-kind-${node.kind.toLowerCase()}`,
1274
1961
  "aria-hidden": true,
1275
- children: thumbnail !== void 0 ? /* @__PURE__ */ jsx21(
1962
+ children: thumbnail !== void 0 ? /* @__PURE__ */ jsx26(
1276
1963
  "img",
1277
1964
  {
1278
1965
  className: "hl-outline-thumb",
1279
1966
  src: thumbnail,
1280
1967
  alt: ""
1281
1968
  }
1282
- ) : /* @__PURE__ */ jsx21(NodeIcon, { code: node.code, kind: node.kind, size: 13 })
1969
+ ) : /* @__PURE__ */ jsx26(NodeIcon, { code: node.code, kind: node.kind, size: 13 })
1283
1970
  }
1284
1971
  ),
1285
- /* @__PURE__ */ jsx21("span", { className: "hl-outline-name", children: node.name }),
1972
+ /* @__PURE__ */ jsx26("span", { className: "hl-outline-name", children: node.name }),
1286
1973
  (() => {
1287
1974
  const diag = diagnostics.worstOf(node.id);
1288
- 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 });
1289
1976
  })(),
1290
- isLocked && /* @__PURE__ */ jsx21("span", { className: "hl-outline-badge", "aria-hidden": true, children: /* @__PURE__ */ jsx21(LockIcon, { size: 11 }) }),
1291
- 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 }) })
1292
1979
  ] }),
1293
- /* @__PURE__ */ jsxs13("span", { className: "hl-outline-controls", children: [
1294
- /* @__PURE__ */ jsx21(
1980
+ /* @__PURE__ */ jsxs15("span", { className: "hl-outline-controls", children: [
1981
+ /* @__PURE__ */ jsx26(
1295
1982
  "button",
1296
1983
  {
1297
1984
  className: "hl-outline-ctl" + (isHidden ? " is-active-ctl" : ""),
1298
1985
  onClick: () => viewState.toggleHidden(node.id),
1299
1986
  "aria-label": isHidden ? "Show layer" : "Hide layer",
1300
1987
  title: isHidden ? "Show" : "Hide",
1301
- children: isHidden ? /* @__PURE__ */ jsx21(EyeOffIcon, {}) : /* @__PURE__ */ jsx21(EyeIcon, {})
1988
+ children: isHidden ? /* @__PURE__ */ jsx26(EyeOffIcon, {}) : /* @__PURE__ */ jsx26(EyeIcon, {})
1302
1989
  }
1303
1990
  ),
1304
- /* @__PURE__ */ jsx21(
1991
+ /* @__PURE__ */ jsx26(
1305
1992
  "button",
1306
1993
  {
1307
1994
  className: "hl-outline-ctl" + (isLocked ? " is-active-ctl" : ""),
1308
1995
  onClick: () => viewState.toggleLocked(node.id),
1309
1996
  "aria-label": isLocked ? "Unlock layer" : "Lock layer",
1310
1997
  title: isLocked ? "Unlock" : "Lock",
1311
- children: isLocked ? /* @__PURE__ */ jsx21(LockIcon, {}) : /* @__PURE__ */ jsx21(UnlockIcon, {})
1998
+ children: isLocked ? /* @__PURE__ */ jsx26(LockIcon, {}) : /* @__PURE__ */ jsx26(UnlockIcon, {})
1312
1999
  }
1313
2000
  )
1314
2001
  ] })
@@ -1321,21 +2008,21 @@ function OutlineTree({ core, onFocus }) {
1321
2008
  }
1322
2009
 
1323
2010
  // src/panels/PreviewPanel.tsx
1324
- import { jsx as jsx22, jsxs as jsxs14 } from "react/jsx-runtime";
2011
+ import { jsx as jsx27, jsxs as jsxs16 } from "react/jsx-runtime";
1325
2012
  function PreviewPanel({ imageUrl, title = "Preview", status }) {
1326
- return /* @__PURE__ */ jsxs14("section", { className: "hl-preview", "aria-label": "Preview", children: [
1327
- /* @__PURE__ */ jsxs14("div", { className: "hl-preview-head", children: [
1328
- /* @__PURE__ */ jsx22("span", { className: "hl-preview-title", children: title }),
1329
- 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 })
1330
2017
  ] }),
1331
- /* @__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" }) })
1332
2019
  ] });
1333
2020
  }
1334
2021
 
1335
2022
  // src/panels/RailTabs.tsx
1336
- import { jsx as jsx23 } from "react/jsx-runtime";
2023
+ import { jsx as jsx28 } from "react/jsx-runtime";
1337
2024
  function RailTabs({ tabs, active, onChange }) {
1338
- 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(
1339
2026
  "button",
1340
2027
  {
1341
2028
  role: "tab",
@@ -1349,20 +2036,20 @@ function RailTabs({ tabs, active, onChange }) {
1349
2036
  }
1350
2037
 
1351
2038
  // src/panels/AssetGrid.tsx
1352
- import { jsx as jsx24 } from "react/jsx-runtime";
2039
+ import { jsx as jsx29 } from "react/jsx-runtime";
1353
2040
  function AssetGrid({ items, onSelect, emptyHint = "No assets yet" }) {
1354
2041
  if (items.length === 0) {
1355
- 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 });
1356
2043
  }
1357
- return /* @__PURE__ */ jsx24("div", { className: "hl-assetgrid", children: items.map((item) => {
2044
+ return /* @__PURE__ */ jsx29("div", { className: "hl-assetgrid", children: items.map((item) => {
1358
2045
  var _a;
1359
- return /* @__PURE__ */ jsx24(
2046
+ return /* @__PURE__ */ jsx29(
1360
2047
  "button",
1361
2048
  {
1362
2049
  className: "hl-assetgrid-cell",
1363
2050
  onClick: () => onSelect == null ? void 0 : onSelect(item.id),
1364
2051
  title: item.label,
1365
- 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" })
1366
2053
  },
1367
2054
  item.id
1368
2055
  );
@@ -1370,10 +2057,10 @@ function AssetGrid({ items, onSelect, emptyHint = "No assets yet" }) {
1370
2057
  }
1371
2058
 
1372
2059
  // src/panels/PositionPanel.tsx
1373
- import { useCallback as useCallback4, useEffect as useEffect3, useReducer } from "react";
2060
+ import { useCallback as useCallback6, useEffect as useEffect4, useReducer } from "react";
1374
2061
  import { MoveNodeCommand } from "@hlang-tech/editor-core";
1375
2062
  import { useSelection as useSelection3 } from "@hlang-tech/editor-react";
1376
- import { jsx as jsx25, jsxs as jsxs15 } from "react/jsx-runtime";
2063
+ import { jsx as jsx30, jsxs as jsxs17 } from "react/jsx-runtime";
1377
2064
  function selectedNodes(core) {
1378
2065
  const ids = core.selection.getSelectedNodes();
1379
2066
  const out = [];
@@ -1389,7 +2076,7 @@ function PositionPanel({ core, livePosition }) {
1389
2076
  var _a;
1390
2077
  useSelection3(core);
1391
2078
  const [, bump] = useReducer((n) => n + 1, 0);
1392
- useEffect3(() => {
2079
+ useEffect4(() => {
1393
2080
  const kinds = ["nodeChanged", "nodeAdded", "nodeRemoved"];
1394
2081
  const disposers = kinds.map((k) => core.events.on(k, bump));
1395
2082
  return () => {
@@ -1401,7 +2088,7 @@ function PositionPanel({ core, livePosition }) {
1401
2088
  const nodes = selectedNodes(core);
1402
2089
  const single = nodes.length === 1 ? nodes[0] : void 0;
1403
2090
  const shownPos = single !== void 0 ? (_a = livePosition == null ? void 0 : livePosition(single.id)) != null ? _a : single.position : void 0;
1404
- const moveAll = useCallback4(
2091
+ const moveAll = useCallback6(
1405
2092
  (fn) => {
1406
2093
  const targets = selectedNodes(core);
1407
2094
  if (targets.length === 0) {
@@ -1415,7 +2102,7 @@ function PositionPanel({ core, livePosition }) {
1415
2102
  },
1416
2103
  [core]
1417
2104
  );
1418
- const align = useCallback4(
2105
+ const align = useCallback6(
1419
2106
  (kind) => {
1420
2107
  const targets = selectedNodes(core);
1421
2108
  if (targets.length === 0) {
@@ -1448,7 +2135,7 @@ function PositionPanel({ core, livePosition }) {
1448
2135
  },
1449
2136
  [core, moveAll]
1450
2137
  );
1451
- const setCoord = useCallback4(
2138
+ const setCoord = useCallback6(
1452
2139
  (axis, value) => {
1453
2140
  if (single === void 0 || Number.isNaN(value)) {
1454
2141
  return;
@@ -1467,9 +2154,9 @@ function PositionPanel({ core, livePosition }) {
1467
2154
  { kind: "vcenter", label: "Align middle", d: "M4 12h16M9 8v8M15 9v6" },
1468
2155
  { kind: "bottom", label: "Align bottom", d: "M4 19h16M8 7v8M16 12v3" }
1469
2156
  ];
1470
- return /* @__PURE__ */ jsxs15("section", { className: "hl-position", children: [
1471
- /* @__PURE__ */ jsx25("h3", { className: "hl-position-title hl-mono", children: "Position" }),
1472
- /* @__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(
1473
2160
  "button",
1474
2161
  {
1475
2162
  className: "hl-position-alignbtn",
@@ -1477,13 +2164,13 @@ function PositionPanel({ core, livePosition }) {
1477
2164
  disabled,
1478
2165
  title: b.label,
1479
2166
  "aria-label": b.label,
1480
- 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 }) })
1481
2168
  },
1482
2169
  b.kind
1483
2170
  )) }),
1484
- /* @__PURE__ */ jsxs15("div", { className: "hl-position-coords", children: [
1485
- /* @__PURE__ */ jsxs15("label", { className: "hl-position-field", children: [
1486
- /* @__PURE__ */ jsx25(
2171
+ /* @__PURE__ */ jsxs17("div", { className: "hl-position-coords", children: [
2172
+ /* @__PURE__ */ jsxs17("label", { className: "hl-position-field", children: [
2173
+ /* @__PURE__ */ jsx30(
1487
2174
  "input",
1488
2175
  {
1489
2176
  type: "number",
@@ -1494,10 +2181,10 @@ function PositionPanel({ core, livePosition }) {
1494
2181
  "aria-label": "X position"
1495
2182
  }
1496
2183
  ),
1497
- /* @__PURE__ */ jsx25("span", { className: "hl-position-axis hl-mono", children: "X" })
2184
+ /* @__PURE__ */ jsx30("span", { className: "hl-position-axis hl-mono", children: "X" })
1498
2185
  ] }),
1499
- /* @__PURE__ */ jsxs15("label", { className: "hl-position-field", children: [
1500
- /* @__PURE__ */ jsx25(
2186
+ /* @__PURE__ */ jsxs17("label", { className: "hl-position-field", children: [
2187
+ /* @__PURE__ */ jsx30(
1501
2188
  "input",
1502
2189
  {
1503
2190
  type: "number",
@@ -1508,24 +2195,24 @@ function PositionPanel({ core, livePosition }) {
1508
2195
  "aria-label": "Y position"
1509
2196
  }
1510
2197
  ),
1511
- /* @__PURE__ */ jsx25("span", { className: "hl-position-axis hl-mono", children: "Y" })
2198
+ /* @__PURE__ */ jsx30("span", { className: "hl-position-axis hl-mono", children: "Y" })
1512
2199
  ] })
1513
2200
  ] })
1514
2201
  ] });
1515
2202
  }
1516
2203
 
1517
2204
  // src/command-palette/NodeSearch.tsx
1518
- import { useCallback as useCallback5, useEffect as useEffect4, useMemo as useMemo4, useRef as useRef3, useState as useState4 } from "react";
1519
- 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";
1520
2207
  var PANEL_WIDTH = 320;
1521
2208
  var PANEL_MAX_HEIGHT = 360;
1522
2209
  function NodeSearch({ controller }) {
1523
2210
  const { state } = controller;
1524
- const [query, setQuery] = useState4("");
1525
- const [active, setActive] = useState4(0);
1526
- const inputRef = useRef3(null);
2211
+ const [query, setQuery] = useState5("");
2212
+ const [active, setActive] = useState5(0);
2213
+ const inputRef = useRef5(null);
1527
2214
  const isOpen = state !== void 0;
1528
- useEffect4(() => {
2215
+ useEffect5(() => {
1529
2216
  var _a;
1530
2217
  if (isOpen) {
1531
2218
  setQuery("");
@@ -1533,12 +2220,12 @@ function NodeSearch({ controller }) {
1533
2220
  (_a = inputRef.current) == null ? void 0 : _a.focus();
1534
2221
  }
1535
2222
  }, [isOpen]);
1536
- const results = useMemo4(
2223
+ const results = useMemo5(
1537
2224
  () => state !== void 0 ? controller.results(query) : [],
1538
2225
  [controller, state, query]
1539
2226
  );
1540
2227
  const clampedActive = results.length === 0 ? 0 : Math.min(active, results.length - 1);
1541
- const choose = useCallback5(
2228
+ const choose = useCallback7(
1542
2229
  (view) => {
1543
2230
  if (view !== void 0) {
1544
2231
  controller.commit(view);
@@ -1546,7 +2233,7 @@ function NodeSearch({ controller }) {
1546
2233
  },
1547
2234
  [controller]
1548
2235
  );
1549
- const onKeyDown = useCallback5(
2236
+ const onKeyDown = useCallback7(
1550
2237
  (event) => {
1551
2238
  switch (event.key) {
1552
2239
  case "ArrowDown":
@@ -1576,8 +2263,8 @@ function NodeSearch({ controller }) {
1576
2263
  }
1577
2264
  const style = clampToViewport(state.screen.x, state.screen.y);
1578
2265
  const connectMode = state.pending !== void 0;
1579
- return /* @__PURE__ */ jsxs16(Fragment4, { children: [
1580
- /* @__PURE__ */ jsx26(
2266
+ return /* @__PURE__ */ jsxs18(Fragment4, { children: [
2267
+ /* @__PURE__ */ jsx31(
1581
2268
  "div",
1582
2269
  {
1583
2270
  className: "hl-node-search-scrim",
@@ -1586,10 +2273,10 @@ function NodeSearch({ controller }) {
1586
2273
  }
1587
2274
  }
1588
2275
  ),
1589
- /* @__PURE__ */ jsxs16("div", { className: "hl-node-search", style, role: "dialog", "aria-label": "Create node", children: [
1590
- /* @__PURE__ */ jsxs16("div", { className: "hl-node-search-field", children: [
1591
- /* @__PURE__ */ jsx26("span", { className: "hl-node-search-glyph", "aria-hidden": true, children: connectMode ? "\u2933" : "\uFF0B" }),
1592
- /* @__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(
1593
2280
  "input",
1594
2281
  {
1595
2282
  ref: inputRef,
@@ -1607,12 +2294,12 @@ function NodeSearch({ controller }) {
1607
2294
  }
1608
2295
  )
1609
2296
  ] }),
1610
- /* @__PURE__ */ jsxs16("ul", { className: "hl-node-search-list", role: "listbox", children: [
1611
- 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" }),
1612
2299
  results.map((view, index) => {
1613
2300
  const descriptor = view.result.value;
1614
2301
  const kind = descriptor.kind;
1615
- return /* @__PURE__ */ jsxs16(
2302
+ return /* @__PURE__ */ jsxs18(
1616
2303
  "li",
1617
2304
  {
1618
2305
  role: "option",
@@ -1624,15 +2311,15 @@ function NodeSearch({ controller }) {
1624
2311
  choose(view);
1625
2312
  },
1626
2313
  children: [
1627
- /* @__PURE__ */ jsx26(
2314
+ /* @__PURE__ */ jsx31(
1628
2315
  "span",
1629
2316
  {
1630
2317
  className: `hl-node-search-kind hl-mono hl-kind-${kind.toLowerCase()}`,
1631
2318
  children: kind
1632
2319
  }
1633
2320
  ),
1634
- /* @__PURE__ */ jsx26("span", { className: "hl-node-search-label", children: highlight(descriptor.displayName, view.result.match) }),
1635
- 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 })
1636
2323
  ]
1637
2324
  },
1638
2325
  descriptor.code
@@ -1646,7 +2333,7 @@ function highlight(text, match) {
1646
2333
  const hit = new Set(match.positions);
1647
2334
  const chars = Array.from(text);
1648
2335
  return chars.map(
1649
- (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)
1650
2337
  );
1651
2338
  }
1652
2339
  function clampToViewport(x, y) {
@@ -1659,7 +2346,7 @@ function clampToViewport(x, y) {
1659
2346
  }
1660
2347
 
1661
2348
  // src/command-palette/useNodeSearch.ts
1662
- 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";
1663
2350
  import { AddNodeCommand, ConnectCommand } from "@hlang-tech/editor-core";
1664
2351
 
1665
2352
  // src/command-palette/fuzzy.ts
@@ -1750,21 +2437,21 @@ function firstCompletingPort(ports, pending, validator) {
1750
2437
 
1751
2438
  // src/command-palette/useNodeSearch.ts
1752
2439
  function useNodeSearch(core) {
1753
- const [state, setState] = useState5(void 0);
1754
- const openBlank = useCallback6((screen, flow) => {
2440
+ const [state, setState] = useState6(void 0);
2441
+ const openBlank = useCallback8((screen, flow) => {
1755
2442
  setState({ screen, flow });
1756
2443
  }, []);
1757
- const openFromConnection = useCallback6(
2444
+ const openFromConnection = useCallback8(
1758
2445
  (screen, flow, pending) => {
1759
2446
  setState({ screen, flow, pending });
1760
2447
  },
1761
2448
  []
1762
2449
  );
1763
- const close = useCallback6(() => {
2450
+ const close = useCallback8(() => {
1764
2451
  setState(void 0);
1765
2452
  }, []);
1766
2453
  const descriptors = core.registry.list();
1767
- const results = useCallback6(
2454
+ const results = useCallback8(
1768
2455
  (query) => {
1769
2456
  if ((state == null ? void 0 : state.pending) !== void 0) {
1770
2457
  return connectableCandidates(
@@ -1778,7 +2465,7 @@ function useNodeSearch(core) {
1778
2465
  },
1779
2466
  [state, descriptors, core]
1780
2467
  );
1781
- const commit = useCallback6(
2468
+ const commit = useCallback8(
1782
2469
  (view) => {
1783
2470
  if (state === void 0) {
1784
2471
  return;
@@ -1802,14 +2489,14 @@ function useNodeSearch(core) {
1802
2489
  },
1803
2490
  [state, core]
1804
2491
  );
1805
- return useMemo5(
2492
+ return useMemo6(
1806
2493
  () => ({ state, openBlank, openFromConnection, close, results, commit }),
1807
2494
  [state, openBlank, openFromConnection, close, results, commit]
1808
2495
  );
1809
2496
  }
1810
2497
 
1811
2498
  // src/command-palette/useNodeSearchTriggers.ts
1812
- import { useCallback as useCallback7, useRef as useRef4 } from "react";
2499
+ import { useCallback as useCallback9, useRef as useRef6 } from "react";
1813
2500
  import { useReactFlow } from "@xyflow/react";
1814
2501
  var DEFAULT_DOUBLE_CLICK_MS = 350;
1815
2502
  function useNodeSearchTriggers(core, search, options = {}) {
@@ -1817,12 +2504,12 @@ function useNodeSearchTriggers(core, search, options = {}) {
1817
2504
  const flow = useReactFlow();
1818
2505
  const doubleClickMs = (_a = options.doubleClickMs) != null ? _a : DEFAULT_DOUBLE_CLICK_MS;
1819
2506
  const now = (_b = options.now) != null ? _b : defaultNow;
1820
- const startRef = useRef4(null);
1821
- const lastPaneClickRef = useRef4(Number.NEGATIVE_INFINITY);
1822
- const onConnectStart = useCallback7((_event, params) => {
2507
+ const startRef = useRef6(null);
2508
+ const lastPaneClickRef = useRef6(Number.NEGATIVE_INFINITY);
2509
+ const onConnectStart = useCallback9((_event, params) => {
1823
2510
  startRef.current = params;
1824
2511
  }, []);
1825
- const onConnectEnd = useCallback7(
2512
+ const onConnectEnd = useCallback9(
1826
2513
  (event, connectionState) => {
1827
2514
  const start = startRef.current;
1828
2515
  startRef.current = null;
@@ -1855,7 +2542,7 @@ function useNodeSearchTriggers(core, search, options = {}) {
1855
2542
  },
1856
2543
  [core, flow, search]
1857
2544
  );
1858
- const onPaneClick = useCallback7(
2545
+ const onPaneClick = useCallback9(
1859
2546
  (event) => {
1860
2547
  const t = now();
1861
2548
  const isDouble = t - lastPaneClickRef.current <= doubleClickMs;
@@ -1887,7 +2574,7 @@ function pointerOf(event) {
1887
2574
  }
1888
2575
 
1889
2576
  // src/hooks/useEditorHotkeys.ts
1890
- import { useEffect as useEffect5 } from "react";
2577
+ import { useEffect as useEffect6 } from "react";
1891
2578
  import { useClipboard } from "@hlang-tech/editor-react";
1892
2579
  function isEditableTarget(target) {
1893
2580
  if (!(target instanceof HTMLElement)) {
@@ -1906,7 +2593,7 @@ function withinScope(target, scope) {
1906
2593
  function useEditorHotkeys(core, options = {}) {
1907
2594
  const clipboard = useClipboard(core);
1908
2595
  const { scope } = options;
1909
- useEffect5(() => {
2596
+ useEffect6(() => {
1910
2597
  const onKeyDown = (event) => {
1911
2598
  if (!withinScope(event.target, scope) || isEditableTarget(event.target)) {
1912
2599
  return;
@@ -1969,33 +2656,33 @@ function useEditorHotkeys(core, options = {}) {
1969
2656
  }
1970
2657
 
1971
2658
  // src/context-menu/NodeContextMenu.tsx
1972
- import { useCallback as useCallback8, useEffect as useEffect6, useMemo as useMemo6, useRef as useRef5, useState as useState6 } from "react";
1973
- 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";
1974
2661
  var PANEL_WIDTH2 = 208;
1975
2662
  var ITEM_HEIGHT = 34;
1976
2663
  var VIEWPORT_MARGIN = 8;
1977
2664
  var MENU_VPAD = 12;
1978
2665
  function NodeContextMenu({ controller, items }) {
1979
2666
  const { state } = controller;
1980
- const [active, setActive] = useState6(0);
1981
- const menuRef = useRef5(null);
2667
+ const [active, setActive] = useState7(0);
2668
+ const menuRef = useRef7(null);
1982
2669
  const isOpen = state !== void 0;
1983
- useEffect6(() => {
2670
+ useEffect7(() => {
1984
2671
  var _a;
1985
2672
  if (isOpen) {
1986
2673
  setActive(0);
1987
2674
  (_a = menuRef.current) == null ? void 0 : _a.focus();
1988
2675
  }
1989
2676
  }, [isOpen]);
1990
- const rows = useMemo6(
2677
+ const rows = useMemo7(
1991
2678
  () => state !== void 0 ? items(state.nodeId) : [],
1992
2679
  [items, state]
1993
2680
  );
1994
- const enabledIndices = useMemo6(
2681
+ const enabledIndices = useMemo7(
1995
2682
  () => rows.map((r, i) => r.disabled ? -1 : i).filter((i) => i >= 0),
1996
2683
  [rows]
1997
2684
  );
1998
- const move = useCallback8(
2685
+ const move = useCallback10(
1999
2686
  (delta) => {
2000
2687
  if (enabledIndices.length === 0) {
2001
2688
  return;
@@ -2009,7 +2696,7 @@ function NodeContextMenu({ controller, items }) {
2009
2696
  },
2010
2697
  [enabledIndices]
2011
2698
  );
2012
- const choose = useCallback8(
2699
+ const choose = useCallback10(
2013
2700
  (item) => {
2014
2701
  if (item === void 0 || item.disabled) {
2015
2702
  return;
@@ -2019,7 +2706,7 @@ function NodeContextMenu({ controller, items }) {
2019
2706
  },
2020
2707
  [controller]
2021
2708
  );
2022
- const onKeyDown = useCallback8(
2709
+ const onKeyDown = useCallback10(
2023
2710
  (event) => {
2024
2711
  switch (event.key) {
2025
2712
  case "ArrowDown":
@@ -2048,8 +2735,8 @@ function NodeContextMenu({ controller, items }) {
2048
2735
  return null;
2049
2736
  }
2050
2737
  const style = clampToViewport2(state.screen.x, state.screen.y, rows.length);
2051
- return /* @__PURE__ */ jsxs17(Fragment5, { children: [
2052
- /* @__PURE__ */ jsx27(
2738
+ return /* @__PURE__ */ jsxs19(Fragment5, { children: [
2739
+ /* @__PURE__ */ jsx32(
2053
2740
  "div",
2054
2741
  {
2055
2742
  className: "hl-ctx-scrim",
@@ -2062,7 +2749,7 @@ function NodeContextMenu({ controller, items }) {
2062
2749
  }
2063
2750
  }
2064
2751
  ),
2065
- /* @__PURE__ */ jsx27(
2752
+ /* @__PURE__ */ jsx32(
2066
2753
  "div",
2067
2754
  {
2068
2755
  ref: menuRef,
@@ -2072,7 +2759,7 @@ function NodeContextMenu({ controller, items }) {
2072
2759
  "aria-label": "Node actions",
2073
2760
  tabIndex: -1,
2074
2761
  onKeyDown,
2075
- children: rows.map((item, index) => /* @__PURE__ */ jsxs17(
2762
+ children: rows.map((item, index) => /* @__PURE__ */ jsxs19(
2076
2763
  "button",
2077
2764
  {
2078
2765
  type: "button",
@@ -2085,9 +2772,9 @@ function NodeContextMenu({ controller, items }) {
2085
2772
  choose(item);
2086
2773
  },
2087
2774
  children: [
2088
- /* @__PURE__ */ jsx27("span", { className: "hl-ctx-item-icon", "aria-hidden": true, children: item.icon }),
2089
- /* @__PURE__ */ jsx27("span", { className: "hl-ctx-item-label", children: item.label }),
2090
- 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 })
2091
2778
  ]
2092
2779
  },
2093
2780
  item.id
@@ -2107,21 +2794,21 @@ function clampToViewport2(x, y, rowCount) {
2107
2794
  }
2108
2795
 
2109
2796
  // src/context-menu/useNodeContextMenu.ts
2110
- 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";
2111
2798
  function useNodeContextMenu() {
2112
- const [state, setState] = useState7(void 0);
2113
- const openForNode = useCallback9((screen, nodeId) => {
2799
+ const [state, setState] = useState8(void 0);
2800
+ const openForNode = useCallback11((screen, nodeId) => {
2114
2801
  setState({ screen, nodeId });
2115
2802
  }, []);
2116
- const close = useCallback9(() => {
2803
+ const close = useCallback11(() => {
2117
2804
  setState(void 0);
2118
2805
  }, []);
2119
- return useMemo7(() => ({ state, openForNode, close }), [state, openForNode, close]);
2806
+ return useMemo8(() => ({ state, openForNode, close }), [state, openForNode, close]);
2120
2807
  }
2121
2808
 
2122
2809
  // src/overlays/Cheatsheet.tsx
2123
- import { useEffect as useEffect7, useRef as useRef6 } from "react";
2124
- 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";
2125
2812
  var DEFAULT_GROUPS = [
2126
2813
  {
2127
2814
  title: "Edit",
@@ -2157,8 +2844,8 @@ var DEFAULT_GROUPS = [
2157
2844
  }
2158
2845
  ];
2159
2846
  function Cheatsheet({ open, onClose, groups = DEFAULT_GROUPS }) {
2160
- const dialogRef = useRef6(null);
2161
- useEffect7(() => {
2847
+ const dialogRef = useRef8(null);
2848
+ useEffect8(() => {
2162
2849
  var _a;
2163
2850
  if (!open) {
2164
2851
  return;
@@ -2175,9 +2862,9 @@ function Cheatsheet({ open, onClose, groups = DEFAULT_GROUPS }) {
2175
2862
  if (!open) {
2176
2863
  return null;
2177
2864
  }
2178
- return /* @__PURE__ */ jsxs18(Fragment6, { children: [
2179
- /* @__PURE__ */ jsx28("div", { className: "hl-cheat-scrim", onPointerDown: onClose }),
2180
- /* @__PURE__ */ jsxs18(
2865
+ return /* @__PURE__ */ jsxs20(Fragment6, { children: [
2866
+ /* @__PURE__ */ jsx33("div", { className: "hl-cheat-scrim", onPointerDown: onClose }),
2867
+ /* @__PURE__ */ jsxs20(
2181
2868
  "div",
2182
2869
  {
2183
2870
  ref: dialogRef,
@@ -2187,18 +2874,18 @@ function Cheatsheet({ open, onClose, groups = DEFAULT_GROUPS }) {
2187
2874
  "aria-label": "Keyboard shortcuts",
2188
2875
  tabIndex: -1,
2189
2876
  children: [
2190
- /* @__PURE__ */ jsxs18("div", { className: "hl-cheat-head", children: [
2191
- /* @__PURE__ */ jsx28("span", { className: "hl-cheat-title", children: "Shortcuts" }),
2192
- /* @__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" })
2193
2880
  ] }),
2194
- /* @__PURE__ */ jsx28("div", { className: "hl-cheat-grid", children: groups.map((group) => /* @__PURE__ */ jsxs18("section", { className: "hl-cheat-group", children: [
2195
- /* @__PURE__ */ jsx28("h3", { className: "hl-cheat-group-title hl-mono", children: group.title }),
2196
- /* @__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) => (
2197
2884
  // A row is a binding: one action can have several (two ways to pan),
2198
2885
  // so neither the label nor the keys alone identify it.
2199
- /* @__PURE__ */ jsxs18("li", { className: "hl-cheat-row", children: [
2200
- /* @__PURE__ */ jsx28("span", { className: "hl-cheat-label", children: row.label }),
2201
- /* @__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 })
2202
2889
  ] }, `${row.keys}\0${row.label}`)
2203
2890
  )) })
2204
2891
  ] }, group.title)) })
@@ -2209,13 +2896,13 @@ function Cheatsheet({ open, onClose, groups = DEFAULT_GROUPS }) {
2209
2896
  }
2210
2897
 
2211
2898
  // src/overlays/Toast.tsx
2212
- import { createContext as createContext3, useCallback as useCallback10, useContext as useContext3, useMemo as useMemo8, useRef as useRef7, useState as useState8 } from "react";
2213
- import { jsx as jsx29, jsxs as jsxs19 } from "react/jsx-runtime";
2214
- 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 });
2215
2902
  function ToastProvider({ children, duration = 2200 }) {
2216
- const [items, setItems] = useState8([]);
2217
- const seq = useRef7(0);
2218
- const show = useCallback10(
2903
+ const [items, setItems] = useState9([]);
2904
+ const seq = useRef9(0);
2905
+ const show = useCallback12(
2219
2906
  (message) => {
2220
2907
  seq.current += 1;
2221
2908
  const id = seq.current;
@@ -2226,22 +2913,22 @@ function ToastProvider({ children, duration = 2200 }) {
2226
2913
  },
2227
2914
  [duration]
2228
2915
  );
2229
- const api = useMemo8(() => ({ show }), [show]);
2230
- return /* @__PURE__ */ jsxs19(ToastCtx.Provider, { value: api, children: [
2916
+ const api = useMemo9(() => ({ show }), [show]);
2917
+ return /* @__PURE__ */ jsxs21(ToastCtx.Provider, { value: api, children: [
2231
2918
  children,
2232
- /* @__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)) })
2233
2920
  ] });
2234
2921
  }
2235
2922
  function useToast() {
2236
- return useContext3(ToastCtx);
2923
+ return useContext4(ToastCtx);
2237
2924
  }
2238
2925
 
2239
2926
  // src/hooks/useLiveSignals.ts
2240
- import { useEffect as useEffect8 } from "react";
2927
+ import { useEffect as useEffect9 } from "react";
2241
2928
  function useLiveSignals(core, handle, explicitStore) {
2242
2929
  const contextStore = useLiveSignalStore();
2243
2930
  const store = explicitStore != null ? explicitStore : contextStore;
2244
- useEffect8(() => {
2931
+ useEffect9(() => {
2245
2932
  if (store === null) {
2246
2933
  return;
2247
2934
  }
@@ -2250,6 +2937,8 @@ function useLiveSignals(core, handle, explicitStore) {
2250
2937
  id: edge.id,
2251
2938
  sourceNodeId: edge.source.nodeId,
2252
2939
  sourcePortId: edge.source.portId,
2940
+ targetNodeId: edge.target.nodeId,
2941
+ targetPortId: edge.target.portId,
2253
2942
  isError: edge.source.portId === "@node/error"
2254
2943
  }));
2255
2944
  store.setEdgeIndex(entries);
@@ -2262,7 +2951,8 @@ function useLiveSignals(core, handle, explicitStore) {
2262
2951
  removed.dispose();
2263
2952
  };
2264
2953
  }, [core, store]);
2265
- useEffect8(() => {
2954
+ useEffect9(() => {
2955
+ var _a, _b, _c;
2266
2956
  if (store === null) {
2267
2957
  return;
2268
2958
  }
@@ -2270,45 +2960,452 @@ function useLiveSignals(core, handle, explicitStore) {
2270
2960
  store.reset();
2271
2961
  return;
2272
2962
  }
2273
- 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));
2274
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
+ });
2275
2970
  return () => {
2276
- activitySub.dispose();
2971
+ signalSub.dispose();
2277
2972
  errorSub.dispose();
2973
+ finishedSub == null ? void 0 : finishedSub.dispose();
2278
2974
  store.reset();
2279
2975
  };
2280
2976
  }, [store, handle]);
2281
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
+ }
2282
3364
  export {
2283
3365
  AssetGrid,
2284
3366
  AssetNodeChrome,
2285
3367
  CanvasChrome,
2286
3368
  CanvasDock,
2287
3369
  Cheatsheet,
3370
+ ConnectionInspector,
3371
+ DEFAULT_EDGE_CAPACITY,
3372
+ DEFAULT_MIN_CANVAS_HEIGHT,
2288
3373
  DiagnosticBadge,
2289
3374
  EditorShell,
2290
3375
  EditorUIThemeProvider,
2291
3376
  EmptyState,
3377
+ FOLLOW_SLACK_PX,
3378
+ FOOTER_KEY_STEP,
2292
3379
  FlowEdge,
3380
+ FooterResizer,
2293
3381
  HlangNode,
2294
3382
  InspectorPanel,
2295
3383
  KIND_ROLE,
2296
3384
  KIND_TOKEN,
3385
+ LivePulse,
2297
3386
  LiveSignalProvider,
2298
3387
  LiveSignalStore,
2299
3388
  NodeBodySlot,
2300
3389
  NodeChrome,
2301
3390
  NodeContextMenu,
2302
3391
  NodeInteractionProvider,
3392
+ NodeRunStats,
2303
3393
  NodeSearch,
2304
3394
  OutlineTree,
3395
+ PREVIEW_LENGTH,
2305
3396
  ParamForm,
2306
3397
  PillNodeChrome,
2307
3398
  PortRow,
2308
3399
  PositionPanel,
2309
3400
  PreviewPanel,
3401
+ RUN_LOG_CAPACITY,
2310
3402
  RailTabs,
2311
3403
  RunButton,
3404
+ RunLog,
3405
+ RunTrace,
3406
+ RunTraceProvider,
3407
+ SELF_LOOP_CLEARANCE,
3408
+ SELF_LOOP_RADIUS,
2312
3409
  SignalBar,
2313
3410
  TOKEN_NAMES,
2314
3411
  ToastProvider,
@@ -2317,26 +3414,47 @@ export {
2317
3414
  ToolbarGhostButton,
2318
3415
  ToolbarStat,
2319
3416
  UndoRedo,
3417
+ boundsFields,
3418
+ capacityLabel,
3419
+ clampFooter,
2320
3420
  connectableCandidates,
2321
3421
  createHlangFlowTypes,
2322
3422
  createTheme,
3423
+ defaultLiveDeps,
2323
3424
  deriveParamSchema,
2324
3425
  descriptorCandidates,
3426
+ footerBounds,
3427
+ formatDuration,
3428
+ formatElapsed,
2325
3429
  fuzzyRank,
2326
3430
  fuzzyScore,
3431
+ involves,
2327
3432
  isEditableTarget,
3433
+ nextBounds,
3434
+ previewValue,
3435
+ resizeByKey,
2328
3436
  searchNodes,
3437
+ selfLoopPath,
3438
+ toggleCollapsed,
2329
3439
  tokenVar,
3440
+ useEdgeBounds,
2330
3441
  useEdgeFlowing,
3442
+ useEdgePulse,
2331
3443
  useEditorHotkeys,
3444
+ useFollowScroll,
3445
+ useFooterResize,
2332
3446
  useLiveSignalStore,
2333
3447
  useLiveSignals,
2334
3448
  useNodeContextMenu,
2335
3449
  useNodeInteraction,
2336
3450
  useNodeLiveState,
2337
3451
  useNodeParams,
3452
+ useNodePulse,
2338
3453
  useNodeSearch,
2339
3454
  useNodeSearchTriggers,
3455
+ useRunTrace,
3456
+ useRunTraceStore,
3457
+ useRunTraceVersion,
2340
3458
  useToast
2341
3459
  };
2342
3460
  //# sourceMappingURL=index.mjs.map