@hlang-tech/editor-ui 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.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,78 +513,86 @@ 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
  }
553
+ function FilterIcon({ size = 14 }) {
554
+ return svg(size, [/* @__PURE__ */ jsx8("path", { d: "M4 5h16l-6 7.5V19l-4-2v-4.5z" }, "f")]);
555
+ }
556
+ function FollowIcon({ size = 14 }) {
557
+ return svg(size, [/* @__PURE__ */ jsx8("path", { d: "M12 4v11M7 10.5l5 5 5-5" }, "a"), /* @__PURE__ */ jsx8("path", { d: "M5 20h14" }, "l")]);
558
+ }
559
+ function ClearIcon({ size = 14 }) {
560
+ return svg(size, [/* @__PURE__ */ jsx8("circle", { cx: "12", cy: "12", r: "8" }, "c"), /* @__PURE__ */ jsx8("path", { d: "M6.5 17.5l11-11" }, "s")]);
561
+ }
464
562
 
465
563
  // src/panels/DiagnosticBadge.tsx
466
- import { jsx as jsx8 } from "react/jsx-runtime";
564
+ import { jsx as jsx9 } from "react/jsx-runtime";
467
565
  function DiagnosticBadge({ diagnostic, variant, size = 12 }) {
468
- return /* @__PURE__ */ jsx8(
566
+ return /* @__PURE__ */ jsx9(
469
567
  "span",
470
568
  {
471
569
  className: `${variant} hl-diag-${diagnostic.severity}`,
472
570
  title: diagnostic.message,
473
571
  "aria-label": `${diagnostic.severity}: ${diagnostic.message}`,
474
- children: diagnostic.severity === "error" ? /* @__PURE__ */ jsx8(ErrorIcon, { size }) : /* @__PURE__ */ jsx8(WarningIcon, { size })
572
+ children: diagnostic.severity === "error" ? /* @__PURE__ */ jsx9(ErrorIcon, { size }) : /* @__PURE__ */ jsx9(WarningIcon, { size })
475
573
  }
476
574
  );
477
575
  }
478
576
 
479
577
  // src/nodes/NodeInteractionContext.tsx
480
578
  import { createContext as createContext2, useContext as useContext2 } from "react";
481
- import { jsx as jsx9 } from "react/jsx-runtime";
579
+ import { jsx as jsx10 } from "react/jsx-runtime";
482
580
  var NoopInteraction = {
483
- diagnosticFor: () => void 0,
484
- rename: () => void 0
581
+ diagnosticFor: () => void 0
485
582
  };
486
583
  var NodeInteractionCtx = createContext2(NoopInteraction);
487
584
  function NodeInteractionProvider({
488
585
  value,
489
586
  children
490
587
  }) {
491
- return /* @__PURE__ */ jsx9(NodeInteractionCtx.Provider, { value, children });
588
+ return /* @__PURE__ */ jsx10(NodeInteractionCtx.Provider, { value, children });
492
589
  }
493
590
  function useNodeInteraction() {
494
591
  return useContext2(NodeInteractionCtx);
495
592
  }
496
593
 
497
594
  // src/nodes/PillNode.tsx
498
- import { jsx as jsx10, jsxs as jsxs3 } from "react/jsx-runtime";
595
+ import { jsx as jsx11, jsxs as jsxs3 } from "react/jsx-runtime";
499
596
  var DOUBLE_CLICK_MS = 320;
500
597
  function PillNodeChrome({ node, selected }) {
501
598
  const live = useNodeLiveState(node.id);
@@ -513,10 +610,11 @@ function PillNodeChrome({ node, selected }) {
513
610
  });
514
611
  }
515
612
  }, [editing, node.name]);
613
+ const { rename } = interaction;
516
614
  const commit = useCallback(() => {
517
615
  setEditing(false);
518
- interaction.rename(node.id, draft);
519
- }, [interaction, node.id, draft]);
616
+ rename == null ? void 0 : rename(node.id, draft);
617
+ }, [rename, node.id, draft]);
520
618
  const onKeyDown = useCallback(
521
619
  (e) => {
522
620
  if (e.key === "Enter") {
@@ -531,6 +629,9 @@ function PillNodeChrome({ node, selected }) {
531
629
  );
532
630
  const lastClick = useRef(0);
533
631
  const onNameClick = useCallback(() => {
632
+ if (rename === void 0) {
633
+ return;
634
+ }
534
635
  const now = performance.now();
535
636
  if (now - lastClick.current < DOUBLE_CLICK_MS) {
536
637
  setEditing(true);
@@ -538,21 +639,23 @@ function PillNodeChrome({ node, selected }) {
538
639
  } else {
539
640
  lastClick.current = now;
540
641
  }
541
- }, []);
642
+ }, [rename]);
542
643
  const inputs = node.ports.filter((p) => p.direction === "I" && !p.isError);
543
644
  const outputs = node.ports.filter((p) => p.direction === "O" && !p.isError);
544
- const className = [
645
+ const className2 = [
545
646
  "hl-pill",
546
647
  `hl-pill-${node.kind.toLowerCase()}`,
547
648
  selected ? "is-selected" : "",
548
649
  live === "active" ? "is-active" : "",
549
650
  live === "error" ? "is-live-error" : "",
651
+ live === "finished" ? "is-finished" : "",
550
652
  (diagnostic == null ? void 0 : diagnostic.severity) === "error" ? "has-diag-error" : "",
551
653
  (diagnostic == null ? void 0 : diagnostic.severity) === "warning" ? "has-diag-warning" : ""
552
654
  ].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(
655
+ return /* @__PURE__ */ jsxs3("div", { className: className2, "data-kind": node.kind, children: [
656
+ /* @__PURE__ */ jsx11(LivePulse, { nodeId: node.id }),
657
+ /* @__PURE__ */ jsx11("span", { className: "hl-pill-dot", "aria-hidden": true, children: /* @__PURE__ */ jsx11(NodeIcon, { code: node.code, kind: node.kind, size: 13 }) }),
658
+ editing ? /* @__PURE__ */ jsx11(
556
659
  "input",
557
660
  {
558
661
  ref: inputRef,
@@ -564,71 +667,178 @@ function PillNodeChrome({ node, selected }) {
564
667
  spellCheck: false,
565
668
  "aria-label": "Rename node"
566
669
  }
567
- ) : /* @__PURE__ */ jsx10(
670
+ ) : /* @__PURE__ */ jsx11(
568
671
  "span",
569
672
  {
570
673
  className: "hl-pill-name",
571
674
  onClick: () => {
572
675
  onNameClick();
573
676
  },
574
- title: "Double-click to rename",
677
+ title: rename === void 0 ? void 0 : "Double-click to rename",
575
678
  children: node.name
576
679
  }
577
680
  ),
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)) })
681
+ diagnostic !== void 0 && /* @__PURE__ */ jsx11(DiagnosticBadge, { diagnostic, variant: "hl-pill-diag", size: 12 }),
682
+ /* @__PURE__ */ jsx11("span", { className: "hl-pill-status", "aria-hidden": true }),
683
+ /* @__PURE__ */ jsx11("ul", { className: "hl-pill-ports hl-in", children: inputs.map((port) => /* @__PURE__ */ jsx11(PortRow, { port }, port.id)) }),
684
+ /* @__PURE__ */ jsx11("ul", { className: "hl-pill-ports hl-out", children: outputs.map((port) => /* @__PURE__ */ jsx11(PortRow, { port }, port.id)) })
582
685
  ] });
583
686
  }
584
687
 
585
688
  // src/nodes/HlangNode.tsx
586
- import { jsx as jsx11, jsxs as jsxs4 } from "react/jsx-runtime";
689
+ import { jsx as jsx12, jsxs as jsxs4 } from "react/jsx-runtime";
587
690
  function NodeChrome({ node, selected, body }) {
588
691
  const inputs = node.ports.filter((p) => p.direction === "I");
589
692
  const outputs = node.ports.filter((p) => p.direction === "O");
590
693
  const live = useNodeLiveState(node.id);
591
- const className = [
694
+ const className2 = [
592
695
  "hl-node",
593
696
  `hl-${node.kind.toLowerCase()}`,
594
697
  selected ? "is-selected" : "",
595
698
  live === "active" ? "is-active" : "",
596
- live === "error" ? "is-live-error" : ""
699
+ live === "error" ? "is-live-error" : "",
700
+ live === "finished" ? "is-finished" : ""
597
701
  ].filter(Boolean).join(" ");
598
- return /* @__PURE__ */ jsxs4("div", { className, "data-kind": node.kind, children: [
599
- /* @__PURE__ */ jsx11(SignalBar, {}),
702
+ return /* @__PURE__ */ jsxs4("div", { className: className2, "data-kind": node.kind, children: [
703
+ /* @__PURE__ */ jsx12(LivePulse, { nodeId: node.id }),
704
+ /* @__PURE__ */ jsx12(SignalBar, {}),
600
705
  /* @__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] })
706
+ /* @__PURE__ */ jsx12("span", { className: "hl-kind", children: node.kind }),
707
+ /* @__PURE__ */ jsx12("span", { className: "hl-name", children: node.name }),
708
+ /* @__PURE__ */ jsx12("span", { className: "hl-role", children: KIND_ROLE[node.kind] })
604
709
  ] }),
605
- /* @__PURE__ */ jsx11(NodeBodySlot, { node, children: body }),
710
+ /* @__PURE__ */ jsx12(NodeBodySlot, { node, children: body }),
606
711
  /* @__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)) })
712
+ /* @__PURE__ */ jsx12("ul", { className: "hl-col hl-in", children: inputs.map((port) => /* @__PURE__ */ jsx12(PortRow, { port }, port.id)) }),
713
+ /* @__PURE__ */ jsx12("ul", { className: "hl-col hl-out", children: outputs.map((port) => /* @__PURE__ */ jsx12(PortRow, { port }, port.id)) })
609
714
  ] })
610
715
  ] });
611
716
  }
612
717
  function HlangNode({ data, selected }) {
613
718
  const node = data.editorNode;
614
719
  if (typeof node.params.imageUrl === "string" && node.params.imageUrl.length > 0) {
615
- return /* @__PURE__ */ jsx11(AssetNodeChrome, { node, selected });
720
+ return /* @__PURE__ */ jsx12(AssetNodeChrome, { node, selected });
616
721
  }
617
- return /* @__PURE__ */ jsx11(PillNodeChrome, { node, selected });
722
+ return /* @__PURE__ */ jsx12(PillNodeChrome, { node, selected });
618
723
  }
619
724
 
620
725
  // src/edges/FlowEdge.tsx
621
726
  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);
727
+
728
+ // src/edges/self-loop.ts
729
+ var SELF_LOOP_CLEARANCE = 28;
730
+ var SELF_LOOP_RADIUS = 16;
731
+ function format(value) {
732
+ return String(Math.round(value * 100) / 100);
733
+ }
734
+ function distance(a, b) {
735
+ return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
736
+ }
737
+ function toward(from, to, length) {
738
+ const span = distance(from, to);
739
+ const share = span === 0 ? 0 : length / span;
740
+ return { x: from.x + (to.x - from.x) * share, y: from.y + (to.y - from.y) * share };
741
+ }
742
+ function selfLoopPath(input) {
743
+ var _a, _b, _c, _d, _e;
744
+ const clearance = (_a = input.clearance) != null ? _a : SELF_LOOP_CLEARANCE;
745
+ const radius = (_b = input.radius) != null ? _b : SELF_LOOP_RADIUS;
746
+ const { source, target, node } = input;
747
+ const right = Math.max(source.x, node.x + node.width) + clearance;
748
+ const left = Math.min(target.x, node.x) - clearance;
749
+ const below = node.y + node.height + clearance;
750
+ const points = [
751
+ source,
752
+ { x: right, y: source.y },
753
+ { x: right, y: below },
754
+ { x: left, y: below },
755
+ { x: left, y: target.y },
756
+ target
757
+ ];
758
+ const parts = [`M ${format(source.x)} ${format(source.y)}`];
759
+ for (let i = 1; i < points.length - 1; i += 1) {
760
+ const before = (_c = points[i - 1]) != null ? _c : source;
761
+ const corner = (_d = points[i]) != null ? _d : source;
762
+ const after = (_e = points[i + 1]) != null ? _e : target;
763
+ const r = Math.min(radius, distance(before, corner) / 2, distance(corner, after) / 2);
764
+ if (r <= 0) {
765
+ parts.push(`L ${format(corner.x)} ${format(corner.y)}`);
766
+ continue;
767
+ }
768
+ const enter = toward(corner, before, r);
769
+ const leave = toward(corner, after, r);
770
+ parts.push(
771
+ `L ${format(enter.x)} ${format(enter.y)} Q ${format(corner.x)} ${format(corner.y)} ${format(leave.x)} ${format(leave.y)}`
772
+ );
773
+ }
774
+ parts.push(`L ${format(target.x)} ${format(target.y)}`);
775
+ return { path: parts.join(" "), labelX: (right + left) / 2, labelY: below };
776
+ }
777
+
778
+ // src/edges/edge-bounds.ts
779
+ var DEFAULT_EDGE_CAPACITY = 10;
780
+ function boundsFields(bounds) {
781
+ var _a;
782
+ const bounded = bounds.capacity !== void 0;
783
+ return [
784
+ { key: "bounded", label: "Bounded", type: "toggle", value: bounded },
785
+ { key: "capacity", label: "Capacity", type: "number", value: (_a = bounds.capacity) != null ? _a : DEFAULT_EDGE_CAPACITY, disabled: !bounded },
786
+ {
787
+ key: "dropOldest",
788
+ label: "Drop oldest when full",
789
+ type: "toggle",
790
+ value: bounds.overflow === "drop-oldest",
791
+ disabled: !bounded
792
+ }
793
+ ];
794
+ }
795
+ function nextBounds(bounds, key, value) {
796
+ const { capacity, overflow } = bounds;
797
+ if (key === "bounded") {
798
+ return value === true ? { capacity: capacity != null ? capacity : DEFAULT_EDGE_CAPACITY } : {};
799
+ }
800
+ if (capacity === void 0) {
801
+ return {};
802
+ }
803
+ const keep = overflow === void 0 ? {} : { overflow };
804
+ if (key === "capacity") {
805
+ if (typeof value !== "number" || Number.isNaN(value)) {
806
+ return bounds;
807
+ }
808
+ return { capacity: Math.max(1, Math.round(value)), ...keep };
809
+ }
810
+ if (key === "dropOldest") {
811
+ return value === true ? { capacity, overflow: "drop-oldest" } : { capacity };
812
+ }
813
+ return bounds;
814
+ }
815
+ function capacityLabel(bounds) {
816
+ if (bounds.capacity === void 0) {
817
+ return void 0;
818
+ }
819
+ const label = `[${String(bounds.capacity)}]`;
820
+ return bounds.overflow === "drop-oldest" ? `${label} drop oldest` : label;
821
+ }
822
+
823
+ // src/edges/FlowEdge.tsx
824
+ import { Fragment, jsx as jsx13, jsxs as jsxs5 } from "react/jsx-runtime";
825
+ function roleOf(node) {
625
826
  const role = node == null ? void 0 : node.data.editorNode.params.role;
626
827
  return typeof role === "string" && role.length > 0 ? role : void 0;
627
828
  }
628
829
  function FlowEdge(props) {
629
- var _a;
630
- const { id, source, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, data } = props;
631
- const [path] = getBezierPath({
830
+ var _a, _b, _c, _d, _e, _f;
831
+ const { id, source, target, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, data, selected } = props;
832
+ const sourceNode = useInternalNode(source);
833
+ const role = roleOf(sourceNode);
834
+ const width = sourceNode == null ? void 0 : sourceNode.measured.width;
835
+ const height = sourceNode == null ? void 0 : sourceNode.measured.height;
836
+ const loop = source === target && sourceNode !== void 0 && width !== void 0 && height !== void 0 ? selfLoopPath({
837
+ source: { x: sourceX, y: sourceY },
838
+ target: { x: targetX, y: targetY },
839
+ node: { ...sourceNode.internals.positionAbsolute, width, height }
840
+ }) : void 0;
841
+ const [curve] = getBezierPath({
632
842
  sourceX,
633
843
  sourceY,
634
844
  sourcePosition,
@@ -636,25 +846,39 @@ function FlowEdge(props) {
636
846
  targetY,
637
847
  targetPosition
638
848
  });
849
+ const path = (_a = loop == null ? void 0 : loop.path) != null ? _a : curve;
639
850
  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;
851
+ const labelX = (_b = loop == null ? void 0 : loop.labelX) != null ? _b : sourceX + (targetX - sourceX) * t;
852
+ const labelY = (_c = loop == null ? void 0 : loop.labelY) != null ? _c : sourceY + (targetY - sourceY) * t;
853
+ const capacity = capacityLabel(data != null ? data : {});
854
+ const capacityX = (_d = loop == null ? void 0 : loop.labelX) != null ? _d : sourceX + (targetX - sourceX) * 0.38;
855
+ const capacityY = (_e = loop == null ? void 0 : loop.labelY) != null ? _e : sourceY + (targetY - sourceY) * 0.38;
856
+ const isError = (_f = data == null ? void 0 : data.isError) != null ? _f : false;
643
857
  const isValue = (data == null ? void 0 : data.stream) === "value";
644
858
  const flowing = useEdgeFlowing(id);
645
- const role = useSourceRole(source);
646
- const className = [
859
+ const pulse = useEdgePulse(id);
860
+ const className2 = [
647
861
  "hl-edge",
648
862
  isError ? "is-error" : "",
649
863
  isValue ? "is-value" : "is-live",
650
- flowing ? "is-flowing" : ""
864
+ flowing ? "is-flowing" : "",
865
+ selected === true ? "is-selected" : ""
651
866
  ].filter(Boolean).join(" ");
652
867
  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" })
868
+ /* @__PURE__ */ jsxs5("g", { className: className2, children: [
869
+ /* @__PURE__ */ jsx13(BaseEdge, { id, path, className: "hl-edge-base" }),
870
+ /* @__PURE__ */ jsx13("path", { d: path, className: "hl-edge-flow" }),
871
+ pulse > 0 && /* @__PURE__ */ jsx13("path", { d: path, className: "hl-edge-pulse" }, pulse)
656
872
  ] }),
657
- role !== void 0 && /* @__PURE__ */ jsx12(EdgeLabelRenderer, { children: /* @__PURE__ */ jsx12(
873
+ capacity !== void 0 && /* @__PURE__ */ jsx13(EdgeLabelRenderer, { children: /* @__PURE__ */ jsx13(
874
+ "div",
875
+ {
876
+ className: `hl-edge-capacity hl-mono${(data == null ? void 0 : data.overflow) === "drop-oldest" ? " is-lossy" : ""}`,
877
+ style: { transform: `translate(-50%, -50%) translate(${String(capacityX)}px, ${String(capacityY)}px)` },
878
+ children: capacity
879
+ }
880
+ ) }),
881
+ role !== void 0 && /* @__PURE__ */ jsx13(EdgeLabelRenderer, { children: /* @__PURE__ */ jsx13(
658
882
  "div",
659
883
  {
660
884
  className: "hl-edge-label hl-mono",
@@ -676,49 +900,257 @@ function createHlangFlowTypes() {
676
900
  }
677
901
 
678
902
  // src/shell/EditorShell.tsx
679
- import { jsx as jsx13, jsxs as jsxs6 } from "react/jsx-runtime";
903
+ import { useEffect as useEffect2, useId, useRef as useRef3 } from "react";
904
+
905
+ // src/shell/FooterResizer.tsx
906
+ import { jsx as jsx14 } from "react/jsx-runtime";
907
+ function FooterResizer({ resize, controls, label }) {
908
+ const onPointerDown = (event) => {
909
+ if (event.pointerType === "mouse" && event.button !== 0) {
910
+ return;
911
+ }
912
+ event.preventDefault();
913
+ event.currentTarget.setPointerCapture(event.pointerId);
914
+ resize.startDrag(event.clientY);
915
+ };
916
+ const onPointerMove = (event) => {
917
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
918
+ resize.drag(event.clientY);
919
+ }
920
+ };
921
+ const onPointerEnd = (event) => {
922
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
923
+ event.currentTarget.releasePointerCapture(event.pointerId);
924
+ }
925
+ resize.endDrag();
926
+ };
927
+ const onKeyDown = (event) => {
928
+ if (resize.keyDown(event.key, event.shiftKey)) {
929
+ event.preventDefault();
930
+ }
931
+ };
932
+ return /* @__PURE__ */ jsx14(
933
+ "div",
934
+ {
935
+ role: "separator",
936
+ "aria-orientation": "horizontal",
937
+ "aria-label": label,
938
+ "aria-controls": controls,
939
+ "aria-valuemin": resize.bounds.min,
940
+ "aria-valuemax": resize.bounds.max,
941
+ "aria-valuenow": resize.height,
942
+ tabIndex: 0,
943
+ className: `hl-footer-resizer${resize.dragging ? " is-dragging" : ""}`,
944
+ onPointerDown,
945
+ onPointerMove,
946
+ onPointerUp: onPointerEnd,
947
+ onPointerCancel: onPointerEnd,
948
+ onLostPointerCapture: () => {
949
+ resize.endDrag();
950
+ },
951
+ onDoubleClick: () => {
952
+ resize.toggle();
953
+ },
954
+ onKeyDown
955
+ }
956
+ );
957
+ }
958
+
959
+ // src/shell/useFooterResize.ts
960
+ import { useCallback as useCallback2, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
961
+
962
+ // src/shell/footer-size.ts
963
+ var FOOTER_KEY_STEP = 16;
964
+ function footerBounds(options) {
965
+ const { available, minHeight, minCanvasHeight } = options;
966
+ return { min: minHeight, max: Math.max(minHeight, available - minCanvasHeight) };
967
+ }
968
+ function clampFooter(height, bounds) {
969
+ return Math.round(Math.min(bounds.max, Math.max(bounds.min, height)));
970
+ }
971
+ function resizeByKey(height, key, shift, bounds) {
972
+ const step = FOOTER_KEY_STEP * (shift ? 4 : 1);
973
+ switch (key) {
974
+ case "ArrowUp":
975
+ return clampFooter(height + step, bounds);
976
+ case "ArrowDown":
977
+ return clampFooter(height - step, bounds);
978
+ case "Home":
979
+ return bounds.min;
980
+ case "End":
981
+ return bounds.max;
982
+ default:
983
+ return void 0;
984
+ }
985
+ }
986
+ function toggleCollapsed(size, bounds) {
987
+ if (size.height > bounds.min) {
988
+ return { height: bounds.min, restore: size.height };
989
+ }
990
+ return { height: clampFooter(size.restore, bounds), restore: size.restore };
991
+ }
992
+
993
+ // src/shell/useFooterResize.ts
994
+ var DEFAULT_MIN_CANVAS_HEIGHT = 160;
995
+ function useFooterResize(options) {
996
+ var _a;
997
+ const { defaultHeight, minHeight } = options;
998
+ const minCanvasHeight = (_a = options.minCanvasHeight) != null ? _a : DEFAULT_MIN_CANVAS_HEIGHT;
999
+ const [available, setAvailable] = useState2(null);
1000
+ const [preferred, setPreferred] = useState2(defaultHeight);
1001
+ const [restore, setRestore] = useState2(defaultHeight);
1002
+ const [dragging, setDragging] = useState2(false);
1003
+ const dragStart = useRef2(null);
1004
+ const bounds = useMemo2(
1005
+ () => available === null ? { min: minHeight, max: Math.max(minHeight, defaultHeight) } : footerBounds({ available, minHeight, minCanvasHeight }),
1006
+ [available, minHeight, minCanvasHeight, defaultHeight]
1007
+ );
1008
+ const height = clampFooter(preferred, bounds);
1009
+ const choose = useCallback2(
1010
+ (next) => {
1011
+ setPreferred(next);
1012
+ if (next > bounds.min) {
1013
+ setRestore(next);
1014
+ }
1015
+ },
1016
+ [bounds.min]
1017
+ );
1018
+ const measure = useCallback2((next) => {
1019
+ setAvailable(next);
1020
+ }, []);
1021
+ const startDrag = useCallback2(
1022
+ (clientY) => {
1023
+ dragStart.current = { clientY, height };
1024
+ setDragging(true);
1025
+ },
1026
+ [height]
1027
+ );
1028
+ const drag = useCallback2(
1029
+ (clientY) => {
1030
+ const start = dragStart.current;
1031
+ if (start === null) {
1032
+ return;
1033
+ }
1034
+ choose(clampFooter(start.height + (start.clientY - clientY), bounds));
1035
+ },
1036
+ [bounds, choose]
1037
+ );
1038
+ const endDrag = useCallback2(() => {
1039
+ dragStart.current = null;
1040
+ setDragging(false);
1041
+ }, []);
1042
+ const toggle = useCallback2(() => {
1043
+ const next = toggleCollapsed({ height, restore }, bounds);
1044
+ setPreferred(next.height);
1045
+ setRestore(next.restore);
1046
+ }, [height, restore, bounds]);
1047
+ const keyDown = useCallback2(
1048
+ (key, shift) => {
1049
+ if (key === "Enter") {
1050
+ toggle();
1051
+ return true;
1052
+ }
1053
+ const next = resizeByKey(height, key, shift, bounds);
1054
+ if (next === void 0) {
1055
+ return false;
1056
+ }
1057
+ choose(next);
1058
+ return true;
1059
+ },
1060
+ [height, bounds, choose, toggle]
1061
+ );
1062
+ return { height, bounds, dragging, measure, startDrag, drag, endDrag, keyDown, toggle };
1063
+ }
1064
+
1065
+ // src/shell/EditorShell.tsx
1066
+ import { jsx as jsx15, jsxs as jsxs6 } from "react/jsx-runtime";
680
1067
  function EditorShell({
681
1068
  toolbar,
682
1069
  left,
683
1070
  right,
684
1071
  canvas,
685
1072
  footer,
1073
+ footerResize,
686
1074
  leftCollapsed = false,
687
1075
  rightCollapsed = false
688
1076
  }) {
689
- const className = [
1077
+ const shellRef = useRef3(null);
1078
+ const toolbarRef = useRef3(null);
1079
+ const className2 = [
690
1080
  "hl-shell",
691
1081
  leftCollapsed ? "hl-left-collapsed" : "",
692
1082
  rightCollapsed ? "hl-right-collapsed" : ""
693
1083
  ].filter(Boolean).join(" ");
694
- return /* @__PURE__ */ jsxs6("div", { className, children: [
695
- toolbar !== void 0 && /* @__PURE__ */ jsx13("div", { className: "hl-region-toolbar", children: toolbar }),
1084
+ return /* @__PURE__ */ jsxs6("div", { className: className2, ref: shellRef, children: [
1085
+ toolbar !== void 0 && /* @__PURE__ */ jsx15("div", { className: "hl-region-toolbar", ref: toolbarRef, children: toolbar }),
696
1086
  /* @__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 })
1087
+ left !== void 0 && !leftCollapsed && /* @__PURE__ */ jsx15("aside", { className: "hl-region-left", children: left }),
1088
+ /* @__PURE__ */ jsx15("main", { className: "hl-region-canvas", children: canvas }),
1089
+ right !== void 0 && !rightCollapsed && /* @__PURE__ */ jsx15("aside", { className: "hl-region-right", children: right })
700
1090
  ] }),
701
- footer !== void 0 && /* @__PURE__ */ jsx13("div", { className: "hl-region-footer", children: footer })
1091
+ 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
1092
  ] });
703
1093
  }
1094
+ function ResizableFooter({
1095
+ options,
1096
+ shellRef,
1097
+ toolbarRef,
1098
+ children
1099
+ }) {
1100
+ var _a;
1101
+ const resize = useFooterResize(options);
1102
+ const id = useId();
1103
+ const { measure } = resize;
1104
+ useEffect2(() => {
1105
+ const shell = shellRef.current;
1106
+ if (shell === null) {
1107
+ return;
1108
+ }
1109
+ const update = () => {
1110
+ var _a2, _b;
1111
+ measure(shell.clientHeight - ((_b = (_a2 = toolbarRef.current) == null ? void 0 : _a2.offsetHeight) != null ? _b : 0));
1112
+ };
1113
+ update();
1114
+ const observer = new ResizeObserver(update);
1115
+ observer.observe(shell);
1116
+ if (toolbarRef.current !== null) {
1117
+ observer.observe(toolbarRef.current);
1118
+ }
1119
+ return () => {
1120
+ observer.disconnect();
1121
+ };
1122
+ }, [measure, shellRef, toolbarRef]);
1123
+ return /* @__PURE__ */ jsxs6(
1124
+ "div",
1125
+ {
1126
+ id,
1127
+ className: `hl-region-footer is-resizable${resize.dragging ? " is-resizing" : ""}`,
1128
+ style: { height: resize.height },
1129
+ children: [
1130
+ /* @__PURE__ */ jsx15(FooterResizer, { resize, controls: id, label: (_a = options.label) != null ? _a : "Resize the panel" }),
1131
+ children
1132
+ ]
1133
+ }
1134
+ );
1135
+ }
704
1136
 
705
1137
  // src/toolbar/Toolbar.tsx
706
1138
  import { useHistory } from "@hlang-tech/editor-react";
707
- import { Fragment as Fragment2, jsx as jsx14, jsxs as jsxs7 } from "react/jsx-runtime";
1139
+ import { Fragment as Fragment2, jsx as jsx16, jsxs as jsxs7 } from "react/jsx-runtime";
708
1140
  function Toolbar({ title, center, actions }) {
709
1141
  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" })
1142
+ /* @__PURE__ */ jsx16("div", { className: "hl-toolbar-brand", children: title != null ? title : /* @__PURE__ */ jsxs7(Fragment2, { children: [
1143
+ /* @__PURE__ */ jsx16("span", { className: "hl-toolbar-glyph", "aria-hidden": "true" }),
1144
+ /* @__PURE__ */ jsx16("span", { className: "hl-toolbar-wordmark", children: "hlang" })
713
1145
  ] }) }),
714
- center !== void 0 && /* @__PURE__ */ jsx14("div", { className: "hl-toolbar-center", children: center }),
715
- /* @__PURE__ */ jsx14("div", { className: "hl-toolbar-actions", children: actions })
1146
+ center !== void 0 && /* @__PURE__ */ jsx16("div", { className: "hl-toolbar-center", children: center }),
1147
+ /* @__PURE__ */ jsx16("div", { className: "hl-toolbar-actions", children: actions })
716
1148
  ] });
717
1149
  }
718
1150
  function UndoRedo({ core }) {
719
1151
  const history = useHistory(core);
720
1152
  return /* @__PURE__ */ jsxs7(Fragment2, { children: [
721
- /* @__PURE__ */ jsx14(
1153
+ /* @__PURE__ */ jsx16(
722
1154
  "button",
723
1155
  {
724
1156
  type: "button",
@@ -729,7 +1161,7 @@ function UndoRedo({ core }) {
729
1161
  children: "Undo"
730
1162
  }
731
1163
  ),
732
- /* @__PURE__ */ jsx14(
1164
+ /* @__PURE__ */ jsx16(
733
1165
  "button",
734
1166
  {
735
1167
  type: "button",
@@ -756,7 +1188,7 @@ function RunButton({
756
1188
  onClick: onRun,
757
1189
  "aria-pressed": running,
758
1190
  children: [
759
- /* @__PURE__ */ jsx14(
1191
+ /* @__PURE__ */ jsx16(
760
1192
  "span",
761
1193
  {
762
1194
  className: `hl-toolbar-run-icon${running ? " is-stop" : ""}`,
@@ -769,22 +1201,31 @@ function RunButton({
769
1201
  );
770
1202
  }
771
1203
  function ToolbarStat({ label, value }) {
772
- return /* @__PURE__ */ jsx14("span", { className: "hl-toolbar-stat hl-mono", title: label, children: value });
1204
+ return /* @__PURE__ */ jsx16("span", { className: "hl-toolbar-stat hl-mono", title: label, children: value });
773
1205
  }
774
1206
  function ToolbarGhostButton({
775
1207
  children,
776
1208
  onClick,
777
1209
  title
778
1210
  }) {
779
- return /* @__PURE__ */ jsx14("button", { type: "button", className: "hl-toolbar-btn", onClick, title, children });
1211
+ return /* @__PURE__ */ jsx16("button", { type: "button", className: "hl-toolbar-btn", onClick, title, children });
1212
+ }
1213
+
1214
+ // src/toolbar/ReadOnlyBadge.tsx
1215
+ import { jsx as jsx17, jsxs as jsxs8 } from "react/jsx-runtime";
1216
+ function ReadOnlyBadge({ label = "View only", title }) {
1217
+ return /* @__PURE__ */ jsxs8("span", { className: "hl-readonly-badge", title: title != null ? title : label, children: [
1218
+ /* @__PURE__ */ jsx17(LockIcon, { size: 12 }),
1219
+ /* @__PURE__ */ jsx17("span", { className: "hl-readonly-badge-label", children: label })
1220
+ ] });
780
1221
  }
781
1222
 
782
1223
  // src/toolbar/ToolCluster.tsx
783
- import { jsx as jsx15, jsxs as jsxs8 } from "react/jsx-runtime";
1224
+ import { jsx as jsx18, jsxs as jsxs9 } from "react/jsx-runtime";
784
1225
  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(
1226
+ return /* @__PURE__ */ jsx18("div", { className: "hl-toolcluster", role: "toolbar", "aria-label": "Canvas tools", children: items.map((item) => /* @__PURE__ */ jsxs9("span", { className: "hl-toolcluster-slot", children: [
1227
+ item.separatorBefore === true && /* @__PURE__ */ jsx18("span", { className: "hl-toolcluster-sep", "aria-hidden": true }),
1228
+ /* @__PURE__ */ jsx18(
788
1229
  "button",
789
1230
  {
790
1231
  type: "button",
@@ -802,30 +1243,30 @@ function ToolCluster({ items }) {
802
1243
 
803
1244
  // src/canvas/CanvasChrome.tsx
804
1245
  import { Background, BackgroundVariant } from "@xyflow/react";
805
- import { Fragment as Fragment3, jsx as jsx16 } from "react/jsx-runtime";
1246
+ import { Fragment as Fragment3, jsx as jsx19 } from "react/jsx-runtime";
806
1247
  function CanvasChrome({ background = true }) {
807
- return /* @__PURE__ */ jsx16(Fragment3, { children: background && /* @__PURE__ */ jsx16(Background, { variant: BackgroundVariant.Dots, gap: 26, size: 1, color: "#242830" }) });
1248
+ return /* @__PURE__ */ jsx19(Fragment3, { children: background && /* @__PURE__ */ jsx19(Background, { variant: BackgroundVariant.Dots, gap: 26, size: 1, color: "#242830" }) });
808
1249
  }
809
1250
 
810
1251
  // src/canvas/CanvasDock.tsx
811
- import { jsx as jsx17, jsxs as jsxs9 } from "react/jsx-runtime";
1252
+ import { Fragment as Fragment4, jsx as jsx20, jsxs as jsxs10 } from "react/jsx-runtime";
812
1253
  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" }) });
1254
+ return /* @__PURE__ */ jsx20("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__ */ jsx20("path", { d: "M5 3l6 16 2-6 6-2z" }) });
814
1255
  }
815
1256
  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" }) });
1257
+ return /* @__PURE__ */ jsx20("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__ */ jsx20("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
1258
  }
818
1259
  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" }) });
1260
+ return /* @__PURE__ */ jsx20("svg", { width: 17, height: 17, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.7, strokeLinecap: "round", "aria-hidden": true, children: /* @__PURE__ */ jsx20("path", { d: "M12 5v14M5 12h14" }) });
820
1261
  }
821
1262
  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" }) });
1263
+ return /* @__PURE__ */ jsx20("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__ */ jsx20("path", { d: "M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5" }) });
823
1264
  }
824
1265
  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" }) });
1266
+ return /* @__PURE__ */ jsx20("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__ */ jsx20("path", { d: "M9 7L4 12l5 5M4 12h11a5 5 0 010 10h-1" }) });
826
1267
  }
827
1268
  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" }) });
1269
+ return /* @__PURE__ */ jsx20("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__ */ jsx20("path", { d: "M15 7l5 5-5 5M20 12H9a5 5 0 000 10h1" }) });
829
1270
  }
830
1271
  function CanvasDock({
831
1272
  zoom,
@@ -838,76 +1279,79 @@ function CanvasDock({
838
1279
  onUndo,
839
1280
  onRedo,
840
1281
  canUndo,
841
- canRedo
1282
+ canRedo,
1283
+ readOnly = false
842
1284
  }) {
843
1285
  const pct = `${String(Math.round(zoom * 100))}%`;
844
- return /* @__PURE__ */ jsxs9("div", { className: "hl-dock", role: "toolbar", "aria-label": "Canvas tools", children: [
845
- /* @__PURE__ */ jsx17(
846
- "button",
847
- {
848
- className: "hl-dock-btn",
849
- onClick: onUndo,
850
- disabled: !canUndo,
851
- title: "Undo (\u2318Z)",
852
- "aria-label": "Undo",
853
- children: /* @__PURE__ */ jsx17(UndoIcon, {})
854
- }
855
- ),
856
- /* @__PURE__ */ jsx17(
857
- "button",
858
- {
859
- className: "hl-dock-btn",
860
- onClick: onRedo,
861
- disabled: !canRedo,
862
- title: "Redo (\u2318\u21E7Z)",
863
- "aria-label": "Redo",
864
- children: /* @__PURE__ */ jsx17(RedoIcon, {})
865
- }
866
- ),
867
- /* @__PURE__ */ jsx17("span", { className: "hl-dock-sep", "aria-hidden": true }),
868
- /* @__PURE__ */ jsx17(
869
- "button",
870
- {
871
- className: "hl-dock-btn" + (mode === "select" ? " is-active" : ""),
872
- onClick: () => onModeChange("select"),
873
- title: "Select (V)",
874
- "aria-pressed": mode === "select",
875
- children: /* @__PURE__ */ jsx17(CursorIcon, {})
876
- }
877
- ),
878
- /* @__PURE__ */ jsx17(
879
- "button",
880
- {
881
- className: "hl-dock-btn" + (mode === "pan" ? " is-active" : ""),
882
- onClick: () => onModeChange("pan"),
883
- title: "Pan (Space)",
884
- "aria-pressed": mode === "pan",
885
- children: /* @__PURE__ */ jsx17(HandIcon, {})
886
- }
887
- ),
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, {}) })
1286
+ return /* @__PURE__ */ jsxs10("div", { className: "hl-dock", role: "toolbar", "aria-label": "Canvas tools", children: [
1287
+ readOnly ? null : /* @__PURE__ */ jsxs10(Fragment4, { children: [
1288
+ /* @__PURE__ */ jsx20(
1289
+ "button",
1290
+ {
1291
+ className: "hl-dock-btn",
1292
+ onClick: onUndo,
1293
+ disabled: !canUndo,
1294
+ title: "Undo (\u2318Z)",
1295
+ "aria-label": "Undo",
1296
+ children: /* @__PURE__ */ jsx20(UndoIcon, {})
1297
+ }
1298
+ ),
1299
+ /* @__PURE__ */ jsx20(
1300
+ "button",
1301
+ {
1302
+ className: "hl-dock-btn",
1303
+ onClick: onRedo,
1304
+ disabled: !canRedo,
1305
+ title: "Redo (\u2318\u21E7Z)",
1306
+ "aria-label": "Redo",
1307
+ children: /* @__PURE__ */ jsx20(RedoIcon, {})
1308
+ }
1309
+ ),
1310
+ /* @__PURE__ */ jsx20("span", { className: "hl-dock-sep", "aria-hidden": true }),
1311
+ /* @__PURE__ */ jsx20(
1312
+ "button",
1313
+ {
1314
+ className: "hl-dock-btn" + (mode === "select" ? " is-active" : ""),
1315
+ onClick: () => onModeChange("select"),
1316
+ title: "Select (V)",
1317
+ "aria-pressed": mode === "select",
1318
+ children: /* @__PURE__ */ jsx20(CursorIcon, {})
1319
+ }
1320
+ ),
1321
+ /* @__PURE__ */ jsx20(
1322
+ "button",
1323
+ {
1324
+ className: "hl-dock-btn" + (mode === "pan" ? " is-active" : ""),
1325
+ onClick: () => onModeChange("pan"),
1326
+ title: "Pan (Space)",
1327
+ "aria-pressed": mode === "pan",
1328
+ children: /* @__PURE__ */ jsx20(HandIcon, {})
1329
+ }
1330
+ ),
1331
+ /* @__PURE__ */ jsx20("button", { className: "hl-dock-btn hl-dock-add", onClick: onAdd, title: "Add node", children: /* @__PURE__ */ jsx20(PlusIcon, {}) }),
1332
+ /* @__PURE__ */ jsx20("span", { className: "hl-dock-sep", "aria-hidden": true })
1333
+ ] }),
1334
+ /* @__PURE__ */ jsx20("button", { className: "hl-dock-btn", onClick: onZoomOut, title: "Zoom out", "aria-label": "Zoom out", children: /* @__PURE__ */ jsx20("span", { className: "hl-dock-glyph", children: "\u2212" }) }),
1335
+ /* @__PURE__ */ jsx20("button", { className: "hl-dock-zoom hl-mono", onClick: onFitView, title: "Fit view", children: pct }),
1336
+ /* @__PURE__ */ jsx20("button", { className: "hl-dock-btn", onClick: onZoomIn, title: "Zoom in", "aria-label": "Zoom in", children: /* @__PURE__ */ jsx20("span", { className: "hl-dock-glyph", children: "+" }) }),
1337
+ /* @__PURE__ */ jsx20("button", { className: "hl-dock-btn hl-dock-fit", onClick: onFitView, title: "Fit view", "aria-label": "Fit view", children: /* @__PURE__ */ jsx20(FitIcon, {}) })
894
1338
  ] });
895
1339
  }
896
1340
 
897
1341
  // src/canvas/EmptyState.tsx
898
- import { jsx as jsx18, jsxs as jsxs10 } from "react/jsx-runtime";
1342
+ import { jsx as jsx21, jsxs as jsxs11 } from "react/jsx-runtime";
899
1343
  function EmptyState({
900
1344
  title = "An empty canvas.",
901
1345
  hint = "add a source \xB7 wire it to a sink \xB7 run"
902
1346
  }) {
903
- 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 })
1347
+ return /* @__PURE__ */ jsxs11("div", { className: "hl-empty", "aria-hidden": "true", children: [
1348
+ /* @__PURE__ */ jsx21("p", { className: "hl-empty-title", children: title }),
1349
+ /* @__PURE__ */ jsx21("p", { className: "hl-empty-hint", children: hint })
906
1350
  ] });
907
1351
  }
908
1352
 
909
1353
  // src/panels/InspectorPanel.tsx
910
- import { useSelection } from "@hlang-tech/editor-react";
1354
+ import { useReadOnly as useReadOnly2, useSelection } from "@hlang-tech/editor-react";
911
1355
 
912
1356
  // src/schema/param-field.ts
913
1357
  function humanizeKey(key) {
@@ -938,11 +1382,11 @@ function deriveParamSchema(descriptor, node) {
938
1382
  }
939
1383
 
940
1384
  // src/hooks/useNodeParams.ts
941
- import { useCallback as useCallback2, useMemo as useMemo2, useRef as useRef2, useSyncExternalStore as useSyncExternalStore2 } from "react";
1385
+ import { useCallback as useCallback3, useMemo as useMemo3, useRef as useRef4, useSyncExternalStore as useSyncExternalStore2 } from "react";
942
1386
  import { UpdateParamsCommand } from "@hlang-tech/editor-core";
943
1387
  function useNodeParams(core, nodeId) {
944
- const versionRef = useRef2(0);
945
- const subscribe = useCallback2(
1388
+ const versionRef = useRef4(0);
1389
+ const subscribe = useCallback3(
946
1390
  (onStoreChange) => {
947
1391
  const disposable = core.events.on("nodeChanged", (event) => {
948
1392
  if (nodeId !== void 0 && event.node.id === nodeId) {
@@ -956,9 +1400,9 @@ function useNodeParams(core, nodeId) {
956
1400
  },
957
1401
  [core, nodeId]
958
1402
  );
959
- const getSnapshot = useCallback2(() => versionRef.current, []);
1403
+ const getSnapshot = useCallback3(() => versionRef.current, []);
960
1404
  const version = useSyncExternalStore2(subscribe, getSnapshot, getSnapshot);
961
- const setParams = useCallback2(
1405
+ const setParams = useCallback3(
962
1406
  (next) => {
963
1407
  if (nodeId === void 0) {
964
1408
  return;
@@ -967,7 +1411,7 @@ function useNodeParams(core, nodeId) {
967
1411
  },
968
1412
  [core, nodeId]
969
1413
  );
970
- const setParam = useCallback2(
1414
+ const setParam = useCallback3(
971
1415
  (key, value) => {
972
1416
  if (nodeId === void 0) {
973
1417
  return;
@@ -980,7 +1424,7 @@ function useNodeParams(core, nodeId) {
980
1424
  },
981
1425
  [core, nodeId, setParams]
982
1426
  );
983
- return useMemo2(
1427
+ return useMemo3(
984
1428
  () => ({
985
1429
  node: nodeId === void 0 ? void 0 : core.model.getNode(nodeId),
986
1430
  setParam,
@@ -992,19 +1436,61 @@ function useNodeParams(core, nodeId) {
992
1436
  );
993
1437
  }
994
1438
 
1439
+ // src/panels/ConnectionInspector.tsx
1440
+ import { useReadOnly } from "@hlang-tech/editor-react";
1441
+
1442
+ // src/hooks/useEdgeBounds.ts
1443
+ import { useCallback as useCallback4, useSyncExternalStore as useSyncExternalStore3 } from "react";
1444
+ import { SetEdgeBoundsCommand } from "@hlang-tech/editor-core";
1445
+ function useEdgeBounds(core, edgeId) {
1446
+ const subscribe = useCallback4(
1447
+ (onStoreChange) => {
1448
+ const changed = core.events.on("edgeChanged", (event) => {
1449
+ if (event.edge.id === edgeId) {
1450
+ onStoreChange();
1451
+ }
1452
+ });
1453
+ const removed = core.events.on("edgeRemoved", (event) => {
1454
+ if (event.edgeId === edgeId) {
1455
+ onStoreChange();
1456
+ }
1457
+ });
1458
+ return () => {
1459
+ changed.dispose();
1460
+ removed.dispose();
1461
+ };
1462
+ },
1463
+ [core, edgeId]
1464
+ );
1465
+ const getSnapshot = useCallback4(
1466
+ () => edgeId === void 0 ? void 0 : core.model.getEdge(edgeId),
1467
+ [core, edgeId]
1468
+ );
1469
+ const edge = useSyncExternalStore3(subscribe, getSnapshot, getSnapshot);
1470
+ const setBounds = useCallback4(
1471
+ (bounds) => {
1472
+ if (edgeId !== void 0) {
1473
+ core.commands.execute(new SetEdgeBoundsCommand(edgeId, bounds));
1474
+ }
1475
+ },
1476
+ [core, edgeId]
1477
+ );
1478
+ return { edge, setBounds };
1479
+ }
1480
+
995
1481
  // src/panels/ParamForm.tsx
996
- import { useState as useState2 } from "react";
997
- import { jsx as jsx19, jsxs as jsxs11 } from "react/jsx-runtime";
998
- function ParamForm({ fields, onCommit }) {
999
- return /* @__PURE__ */ jsx19("div", { className: "hl-form", children: fields.map((field) => /* @__PURE__ */ jsx19(FieldRow, { field, onCommit }, field.key)) });
1482
+ import { useState as useState3 } from "react";
1483
+ import { jsx as jsx22, jsxs as jsxs12 } from "react/jsx-runtime";
1484
+ function ParamForm({ fields, onCommit, disabled = false }) {
1485
+ return /* @__PURE__ */ jsx22("fieldset", { className: "hl-form", disabled, children: fields.map((field) => /* @__PURE__ */ jsx22(FieldRow, { field, onCommit }, field.key)) });
1000
1486
  }
1001
1487
  function FieldRow({
1002
1488
  field,
1003
1489
  onCommit
1004
1490
  }) {
1005
- 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 })
1491
+ return /* @__PURE__ */ jsxs12("label", { className: "hl-field", children: [
1492
+ /* @__PURE__ */ jsx22("span", { className: "hl-field-label", children: field.label }),
1493
+ /* @__PURE__ */ jsx22(FieldControl, { field, onCommit })
1008
1494
  ] });
1009
1495
  }
1010
1496
  function FieldControl({
@@ -1013,7 +1499,7 @@ function FieldControl({
1013
1499
  }) {
1014
1500
  switch (field.type) {
1015
1501
  case "toggle":
1016
- return /* @__PURE__ */ jsx19(
1502
+ return /* @__PURE__ */ jsx22(
1017
1503
  "button",
1018
1504
  {
1019
1505
  type: "button",
@@ -1022,17 +1508,17 @@ function FieldControl({
1022
1508
  className: `hl-toggle${field.value ? " is-on" : ""}`,
1023
1509
  disabled: field.disabled,
1024
1510
  onClick: () => onCommit(field.key, !field.value),
1025
- children: /* @__PURE__ */ jsx19("span", { className: "hl-toggle-knob" })
1511
+ children: /* @__PURE__ */ jsx22("span", { className: "hl-toggle-knob" })
1026
1512
  }
1027
1513
  );
1028
1514
  case "number":
1029
- return /* @__PURE__ */ jsx19(NumberControl, { field, onCommit });
1515
+ return /* @__PURE__ */ jsx22(NumberControl, { field, onCommit });
1030
1516
  case "textarea":
1031
- return /* @__PURE__ */ jsx19(TextControl, { field, onCommit, multiline: true });
1517
+ return /* @__PURE__ */ jsx22(TextControl, { field, onCommit, multiline: true });
1032
1518
  case "text":
1033
- return /* @__PURE__ */ jsx19(TextControl, { field, onCommit });
1519
+ return /* @__PURE__ */ jsx22(TextControl, { field, onCommit });
1034
1520
  case "custom":
1035
- return /* @__PURE__ */ jsx19("span", { className: "hl-field-custom hl-mono", title: field.fieldType, children: describeCustom(field.value) });
1521
+ return /* @__PURE__ */ jsx22("span", { className: "hl-field-custom hl-mono", title: field.fieldType, children: describeCustom(field.value) });
1036
1522
  }
1037
1523
  }
1038
1524
  function TextControl({
@@ -1040,7 +1526,7 @@ function TextControl({
1040
1526
  onCommit,
1041
1527
  multiline = false
1042
1528
  }) {
1043
- const [draft, setDraft] = useState2(field.value);
1529
+ const [draft, setDraft] = useState3(field.value);
1044
1530
  const commit = () => {
1045
1531
  if (draft !== field.value) {
1046
1532
  onCommit(field.key, draft);
@@ -1054,7 +1540,7 @@ function TextControl({
1054
1540
  onBlur: commit
1055
1541
  };
1056
1542
  if (multiline) {
1057
- return /* @__PURE__ */ jsx19(
1543
+ return /* @__PURE__ */ jsx22(
1058
1544
  "textarea",
1059
1545
  {
1060
1546
  ...shared,
@@ -1063,7 +1549,7 @@ function TextControl({
1063
1549
  }
1064
1550
  );
1065
1551
  }
1066
- return /* @__PURE__ */ jsx19(
1552
+ return /* @__PURE__ */ jsx22(
1067
1553
  "input",
1068
1554
  {
1069
1555
  ...shared,
@@ -1081,14 +1567,14 @@ function NumberControl({
1081
1567
  field,
1082
1568
  onCommit
1083
1569
  }) {
1084
- const [draft, setDraft] = useState2(String(field.value));
1570
+ const [draft, setDraft] = useState3(String(field.value));
1085
1571
  const commit = () => {
1086
1572
  const parsed = Number(draft);
1087
1573
  if (!Number.isNaN(parsed) && parsed !== field.value) {
1088
1574
  onCommit(field.key, parsed);
1089
1575
  }
1090
1576
  };
1091
- return /* @__PURE__ */ jsx19(
1577
+ return /* @__PURE__ */ jsx22(
1092
1578
  "input",
1093
1579
  {
1094
1580
  className: "hl-input hl-mono",
@@ -1119,57 +1605,289 @@ function describeCustom(value) {
1119
1605
  return String(value);
1120
1606
  }
1121
1607
 
1608
+ // src/panels/ConnectionInspector.tsx
1609
+ import { jsx as jsx23, jsxs as jsxs13 } from "react/jsx-runtime";
1610
+ function ConnectionInspector({ core, edgeId }) {
1611
+ const { edge, setBounds } = useEdgeBounds(core, edgeId);
1612
+ const readOnly = useReadOnly(core);
1613
+ if (edge === void 0) {
1614
+ return null;
1615
+ }
1616
+ const end = (endpoint) => {
1617
+ var _a, _b;
1618
+ return `${(_b = (_a = core.model.getNode(endpoint.nodeId)) == null ? void 0 : _a.name) != null ? _b : endpoint.nodeId}.${endpoint.portId}`;
1619
+ };
1620
+ return /* @__PURE__ */ jsxs13("div", { className: "hl-inspector", children: [
1621
+ /* @__PURE__ */ jsxs13("header", { className: "hl-inspector-head", children: [
1622
+ /* @__PURE__ */ jsx23("span", { className: "hl-inspector-kind hl-connection", children: "\u2192" }),
1623
+ /* @__PURE__ */ jsxs13("div", { className: "hl-inspector-titles", children: [
1624
+ /* @__PURE__ */ jsx23("span", { className: "hl-inspector-name", children: "Connection" }),
1625
+ /* @__PURE__ */ jsxs13("span", { className: "hl-inspector-role hl-mono", children: [
1626
+ end(edge.source),
1627
+ " \u2192 ",
1628
+ end(edge.target)
1629
+ ] })
1630
+ ] })
1631
+ ] }),
1632
+ /* @__PURE__ */ jsxs13("section", { className: "hl-inspector-section", children: [
1633
+ /* @__PURE__ */ jsx23("h3", { className: "hl-inspector-section-title", children: "Back-pressure" }),
1634
+ /* @__PURE__ */ jsx23("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." }),
1635
+ /* @__PURE__ */ jsx23(
1636
+ ParamForm,
1637
+ {
1638
+ fields: boundsFields(edge),
1639
+ disabled: readOnly,
1640
+ onCommit: (key, value) => {
1641
+ setBounds(nextBounds(edge, key, value));
1642
+ }
1643
+ }
1644
+ )
1645
+ ] })
1646
+ ] });
1647
+ }
1648
+
1649
+ // src/live/format.ts
1650
+ function formatDuration(ms) {
1651
+ if (ms === 0) {
1652
+ return "0 ms";
1653
+ }
1654
+ if (ms < 0.01) {
1655
+ return "<0.01 ms";
1656
+ }
1657
+ if (ms < 1) {
1658
+ return `${ms.toFixed(2)} ms`;
1659
+ }
1660
+ if (ms < 10) {
1661
+ return `${ms.toFixed(1)} ms`;
1662
+ }
1663
+ if (ms < 1e3) {
1664
+ return `${Math.round(ms).toString()} ms`;
1665
+ }
1666
+ return `${(ms / 1e3).toFixed(2)} s`;
1667
+ }
1668
+ function formatElapsed(ms) {
1669
+ return `${(ms / 1e3).toFixed(3)}s`;
1670
+ }
1671
+
1672
+ // src/live/preview.ts
1673
+ var PREVIEW_LENGTH = 120;
1674
+ var CIRCULAR = "[Circular]";
1675
+ function functionPreview(value) {
1676
+ return `\u0192 ${value.name === "" ? "anonymous" : value.name}`;
1677
+ }
1678
+ function bigintPreview(value) {
1679
+ return `${value.toString()}n`;
1680
+ }
1681
+ function className(value) {
1682
+ const prototype = Object.getPrototypeOf(value);
1683
+ if (prototype === null || prototype === Object.prototype) {
1684
+ return "Object";
1685
+ }
1686
+ const name = typeof prototype === "object" && "constructor" in prototype ? prototype.constructor : void 0;
1687
+ return typeof name === "function" && name.name !== "" ? name.name : "Object";
1688
+ }
1689
+ function nestedPreview(value) {
1690
+ if (typeof value === "bigint") {
1691
+ return bigintPreview(value);
1692
+ }
1693
+ if (typeof value === "function") {
1694
+ return functionPreview(value);
1695
+ }
1696
+ if (typeof value === "symbol") {
1697
+ return value.toString();
1698
+ }
1699
+ if (value instanceof Error) {
1700
+ return `${value.name}: ${value.message}`;
1701
+ }
1702
+ if (value instanceof Map || value instanceof Set) {
1703
+ return `${value instanceof Map ? "Map" : "Set"}(${String(value.size)})`;
1704
+ }
1705
+ return value;
1706
+ }
1707
+ function json(value) {
1708
+ const ancestors = [];
1709
+ return JSON.stringify(value, function replace(_key, nested) {
1710
+ while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this) {
1711
+ ancestors.pop();
1712
+ }
1713
+ if (typeof nested === "object" && nested !== null) {
1714
+ if (ancestors.includes(nested)) {
1715
+ return CIRCULAR;
1716
+ }
1717
+ ancestors.push(nested);
1718
+ }
1719
+ return nestedPreview(nested);
1720
+ });
1721
+ }
1722
+ function fullPreview(value) {
1723
+ switch (typeof value) {
1724
+ case "string":
1725
+ return JSON.stringify(value);
1726
+ case "bigint":
1727
+ case "function":
1728
+ case "symbol": {
1729
+ const preview = nestedPreview(value);
1730
+ return typeof preview === "string" ? preview : String(preview);
1731
+ }
1732
+ case "object":
1733
+ break;
1734
+ default:
1735
+ return String(value);
1736
+ }
1737
+ if (value === null) {
1738
+ return "null";
1739
+ }
1740
+ if (value instanceof Error || value instanceof Map || value instanceof Set) {
1741
+ return String(nestedPreview(value));
1742
+ }
1743
+ if (value instanceof Date) {
1744
+ return Number.isNaN(value.getTime()) ? "Invalid Date" : value.toISOString();
1745
+ }
1746
+ const name = Array.isArray(value) ? "Array" : className(value);
1747
+ let body;
1748
+ try {
1749
+ body = json(value);
1750
+ } catch {
1751
+ return name;
1752
+ }
1753
+ if (body === void 0) {
1754
+ return name;
1755
+ }
1756
+ return name === "Object" || name === "Array" ? body : `${name} ${body}`;
1757
+ }
1758
+ function previewValue(value, maxLength = PREVIEW_LENGTH) {
1759
+ const preview = fullPreview(value);
1760
+ return preview.length <= maxLength ? preview : `${preview.slice(0, maxLength - 1)}\u2026`;
1761
+ }
1762
+
1763
+ // src/live/RunTraceContext.tsx
1764
+ import { createContext as createContext3, useContext as useContext3, useSyncExternalStore as useSyncExternalStore4 } from "react";
1765
+ import { jsx as jsx24 } from "react/jsx-runtime";
1766
+ var RunTraceContext = createContext3(null);
1767
+ function RunTraceProvider({ trace, children }) {
1768
+ return /* @__PURE__ */ jsx24(RunTraceContext.Provider, { value: trace, children });
1769
+ }
1770
+ function useRunTraceStore() {
1771
+ return useContext3(RunTraceContext);
1772
+ }
1773
+ function useRunTraceVersion(trace) {
1774
+ return useSyncExternalStore4(
1775
+ (onChange) => trace === null ? () => void 0 : trace.subscribe(onChange),
1776
+ () => trace === null ? 0 : trace.getVersion(),
1777
+ () => 0
1778
+ );
1779
+ }
1780
+
1781
+ // src/panels/NodeRunStats.tsx
1782
+ import { jsx as jsx25, jsxs as jsxs14 } from "react/jsx-runtime";
1783
+ function NodeRunStats({ nodeId }) {
1784
+ const trace = useRunTraceStore();
1785
+ useRunTraceVersion(trace);
1786
+ if (trace === null) {
1787
+ return null;
1788
+ }
1789
+ const stats = trace.statsOf(nodeId);
1790
+ return /* @__PURE__ */ jsxs14("section", { className: "hl-inspector-section", children: [
1791
+ /* @__PURE__ */ jsx25("h3", { className: "hl-inspector-section-title", children: "Run" }),
1792
+ stats === void 0 ? /* @__PURE__ */ jsx25("p", { className: "hl-inspector-hint hl-runstats-hint", children: "No activity in this run." }) : /* @__PURE__ */ jsxs14("dl", { className: "hl-runstats", children: [
1793
+ /* @__PURE__ */ jsxs14("div", { className: "hl-runstats-cell", children: [
1794
+ /* @__PURE__ */ jsx25("dt", { children: "In" }),
1795
+ /* @__PURE__ */ jsx25("dd", { className: "hl-mono", children: stats.received })
1796
+ ] }),
1797
+ /* @__PURE__ */ jsxs14("div", { className: "hl-runstats-cell", children: [
1798
+ /* @__PURE__ */ jsx25("dt", { children: "Out" }),
1799
+ /* @__PURE__ */ jsx25("dd", { className: "hl-mono", children: stats.sent })
1800
+ ] }),
1801
+ /* @__PURE__ */ jsxs14("div", { className: `hl-runstats-cell${stats.errors > 0 ? " is-error" : ""}`, children: [
1802
+ /* @__PURE__ */ jsx25("dt", { children: "Errors" }),
1803
+ /* @__PURE__ */ jsx25("dd", { className: "hl-mono", children: stats.errors })
1804
+ ] }),
1805
+ /* @__PURE__ */ jsxs14("div", { className: "hl-runstats-cell", children: [
1806
+ /* @__PURE__ */ jsx25("dt", { children: "Avg" }),
1807
+ /* @__PURE__ */ jsx25("dd", { className: "hl-mono", children: stats.received === 0 ? "\u2014" : formatDuration(stats.totalMs / stats.received) })
1808
+ ] }),
1809
+ /* @__PURE__ */ jsxs14("div", { className: "hl-runstats-cell", children: [
1810
+ /* @__PURE__ */ jsx25("dt", { children: "Max" }),
1811
+ /* @__PURE__ */ jsx25("dd", { className: "hl-mono", children: stats.received === 0 ? "\u2014" : formatDuration(stats.maxMs) })
1812
+ ] }),
1813
+ stats.dropped > 0 && /* @__PURE__ */ jsxs14("div", { className: "hl-runstats-cell is-warning", children: [
1814
+ /* @__PURE__ */ jsx25("dt", { children: "Dropped" }),
1815
+ /* @__PURE__ */ jsx25("dd", { className: "hl-mono", children: stats.dropped })
1816
+ ] }),
1817
+ stats.finished && /* @__PURE__ */ jsxs14("div", { className: "hl-runstats-cell is-done", children: [
1818
+ /* @__PURE__ */ jsx25("dt", { children: "State" }),
1819
+ /* @__PURE__ */ jsx25("dd", { children: "Finished" })
1820
+ ] }),
1821
+ stats.received > 0 && /* @__PURE__ */ jsxs14("div", { className: "hl-runstats-cell is-wide", children: [
1822
+ /* @__PURE__ */ jsx25("dt", { children: "Last in" }),
1823
+ /* @__PURE__ */ jsx25("dd", { className: "hl-mono", children: previewValue(stats.lastReceived) })
1824
+ ] }),
1825
+ stats.sent > 0 && /* @__PURE__ */ jsxs14("div", { className: "hl-runstats-cell is-wide", children: [
1826
+ /* @__PURE__ */ jsx25("dt", { children: "Last out" }),
1827
+ /* @__PURE__ */ jsx25("dd", { className: "hl-mono", children: previewValue(stats.lastSent) })
1828
+ ] })
1829
+ ] })
1830
+ ] });
1831
+ }
1832
+
1122
1833
  // src/panels/InspectorPanel.tsx
1123
- import { jsx as jsx20, jsxs as jsxs12 } from "react/jsx-runtime";
1834
+ import { jsx as jsx26, jsxs as jsxs15 } from "react/jsx-runtime";
1124
1835
  function InspectorPanel({ core, emptyState }) {
1125
1836
  const selection = useSelection(core);
1837
+ const readOnly = useReadOnly2(core);
1126
1838
  const selectedNodes2 = selection.selectedNodes;
1127
1839
  const singleId = selectedNodes2.length === 1 ? selectedNodes2[0] : void 0;
1128
1840
  const params = useNodeParams(core, singleId);
1129
1841
  const node = params.node;
1842
+ const singleEdgeId = selectedNodes2.length === 0 && selection.selectedEdges.length === 1 ? selection.selectedEdges[0] : void 0;
1843
+ if (node === void 0 && singleEdgeId !== void 0) {
1844
+ return /* @__PURE__ */ jsx26(ConnectionInspector, { core, edgeId: singleEdgeId });
1845
+ }
1130
1846
  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." })
1847
+ return /* @__PURE__ */ jsx26("div", { className: "hl-inspector hl-inspector-empty", children: emptyState != null ? emptyState : /* @__PURE__ */ jsxs15("div", { className: "hl-inspector-blank", children: [
1848
+ /* @__PURE__ */ jsx26("span", { className: "hl-inspector-blank-badge", "aria-hidden": true }),
1849
+ /* @__PURE__ */ jsx26("p", { className: "hl-inspector-blank-title", children: selectedNodes2.length > 1 ? `${String(selectedNodes2.length)} nodes selected` : "Nothing selected" }),
1850
+ /* @__PURE__ */ jsx26("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
1851
  ] }) });
1136
1852
  }
1137
1853
  const descriptor = core.registry.get(node.code);
1138
1854
  const fields = descriptor === void 0 ? [] : deriveParamSchema(descriptor, node);
1139
1855
  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: [
1856
+ return /* @__PURE__ */ jsxs15("div", { className: "hl-inspector", children: [
1857
+ /* @__PURE__ */ jsx26("div", { className: `hl-inspector-preview hl-${node.kind.toLowerCase()}`, children: imageUrl !== void 0 ? /* @__PURE__ */ jsx26("img", { className: "hl-inspector-preview-img", src: imageUrl, alt: node.name }) : /* @__PURE__ */ jsx26("span", { className: "hl-inspector-preview-badge hl-mono", children: node.kind }) }),
1858
+ /* @__PURE__ */ jsxs15("header", { className: "hl-inspector-head", children: [
1859
+ /* @__PURE__ */ jsx26("span", { className: `hl-inspector-kind hl-${node.kind.toLowerCase()}`, children: node.kind }),
1860
+ /* @__PURE__ */ jsxs15("div", { className: "hl-inspector-titles", children: [
1861
+ /* @__PURE__ */ jsx26("span", { className: "hl-inspector-name", children: node.name }),
1862
+ /* @__PURE__ */ jsxs15("span", { className: "hl-inspector-role hl-mono", children: [
1147
1863
  node.code,
1148
1864
  " \xB7 ",
1149
1865
  KIND_ROLE[node.kind]
1150
1866
  ] })
1151
1867
  ] })
1152
1868
  ] }),
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(
1869
+ /* @__PURE__ */ jsxs15("section", { className: "hl-inspector-section", children: [
1870
+ /* @__PURE__ */ jsx26("h3", { className: "hl-inspector-section-title", children: "Parameters" }),
1871
+ fields.length === 0 ? /* @__PURE__ */ jsx26("p", { className: "hl-inspector-hint", children: "This node has no parameters." }) : /* @__PURE__ */ jsx26(
1156
1872
  ParamForm,
1157
1873
  {
1158
1874
  fields,
1875
+ disabled: readOnly,
1159
1876
  onCommit: (key, value) => params.setParam(key, value)
1160
1877
  }
1161
1878
  )
1162
1879
  ] }),
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(
1880
+ /* @__PURE__ */ jsx26(NodeRunStats, { nodeId: node.id }),
1881
+ /* @__PURE__ */ jsxs15("section", { className: "hl-inspector-section", children: [
1882
+ /* @__PURE__ */ jsx26("h3", { className: "hl-inspector-section-title", children: "Ports" }),
1883
+ /* @__PURE__ */ jsx26("ul", { className: "hl-inspector-ports", children: node.ports.map((port) => /* @__PURE__ */ jsxs15(
1166
1884
  "li",
1167
1885
  {
1168
1886
  className: `hl-inspector-port${port.isError ? " is-error" : ""}`,
1169
1887
  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 })
1888
+ /* @__PURE__ */ jsx26("span", { className: `hl-port-dot hl-dir-${port.direction.toLowerCase()}` }),
1889
+ /* @__PURE__ */ jsx26("span", { className: "hl-mono", children: port.name }),
1890
+ /* @__PURE__ */ jsx26("span", { className: "hl-inspector-port-dir hl-mono", children: port.direction })
1173
1891
  ]
1174
1892
  },
1175
1893
  port.id
@@ -1179,14 +1897,14 @@ function InspectorPanel({ core, emptyState }) {
1179
1897
  }
1180
1898
 
1181
1899
  // src/panels/OutlineTree.tsx
1182
- import { useCallback as useCallback3, useEffect as useEffect2, useMemo as useMemo3, useState as useState3 } from "react";
1900
+ import { useCallback as useCallback5, useEffect as useEffect3, useMemo as useMemo4, useState as useState4 } from "react";
1183
1901
  import { GraphTopology } from "@hlang-tech/editor-core";
1184
1902
  import { useSelection as useSelection2, useViewState, useDiagnostics } from "@hlang-tech/editor-react";
1185
- import { jsx as jsx21, jsxs as jsxs13 } from "react/jsx-runtime";
1903
+ import { jsx as jsx27, jsxs as jsxs16 } from "react/jsx-runtime";
1186
1904
  function useTopologyComponents(core) {
1187
- const [tick, setTick] = useState3(0);
1188
- const bump = useCallback3(() => setTick((t) => t + 1), []);
1189
- useEffect2(() => {
1905
+ const [tick, setTick] = useState4(0);
1906
+ const bump = useCallback5(() => setTick((t) => t + 1), []);
1907
+ useEffect3(() => {
1190
1908
  const kinds = ["nodeAdded", "nodeRemoved", "nodeChanged", "edgeAdded", "edgeRemoved"];
1191
1909
  const disposers = kinds.map((k) => core.events.on(k, bump));
1192
1910
  bump();
@@ -1196,7 +1914,7 @@ function useTopologyComponents(core) {
1196
1914
  }
1197
1915
  };
1198
1916
  }, [core, bump]);
1199
- return useMemo3(() => {
1917
+ return useMemo4(() => {
1200
1918
  void tick;
1201
1919
  const topo = new GraphTopology();
1202
1920
  topo.reset(core.model.getNodes(), core.model.getEdges());
@@ -1208,15 +1926,15 @@ function OutlineTree({ core, onFocus }) {
1208
1926
  const selection = useSelection2(core);
1209
1927
  const viewState = useViewState(core);
1210
1928
  const diagnostics = useDiagnostics(core);
1211
- const [collapsed, setCollapsed] = useState3(/* @__PURE__ */ new Set());
1212
- const nodeById = useMemo3(() => {
1929
+ const [collapsed, setCollapsed] = useState4(/* @__PURE__ */ new Set());
1930
+ const nodeById = useMemo4(() => {
1213
1931
  const m = /* @__PURE__ */ new Map();
1214
1932
  for (const n of core.model.getNodes()) {
1215
1933
  m.set(n.id, n);
1216
1934
  }
1217
1935
  return m;
1218
1936
  }, [core, components]);
1219
- const toggleGroup = useCallback3((id) => {
1937
+ const toggleGroup = useCallback5((id) => {
1220
1938
  setCollapsed((prev) => {
1221
1939
  const next = new Set(prev);
1222
1940
  if (next.has(id)) {
@@ -1227,7 +1945,7 @@ function OutlineTree({ core, onFocus }) {
1227
1945
  return next;
1228
1946
  });
1229
1947
  }, []);
1230
- const select = useCallback3(
1948
+ const select = useCallback5(
1231
1949
  (id) => {
1232
1950
  selection.selectNodes([id], "replace");
1233
1951
  onFocus == null ? void 0 : onFocus(id);
@@ -1235,80 +1953,80 @@ function OutlineTree({ core, onFocus }) {
1235
1953
  [selection, onFocus]
1236
1954
  );
1237
1955
  if (components.length === 0) {
1238
- return /* @__PURE__ */ jsx21("p", { className: "hl-outline-empty hl-mono", children: "no layers yet" });
1956
+ return /* @__PURE__ */ jsx27("p", { className: "hl-outline-empty hl-mono", children: "no layers yet" });
1239
1957
  }
1240
1958
  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) => {
1959
+ return /* @__PURE__ */ jsx27("div", { className: "hl-outline", role: "tree", "aria-label": "Outline", children: groups.map((group, gi) => {
1242
1960
  const members = [...group.nodeIds].map((id) => nodeById.get(id)).filter((n) => n !== void 0);
1243
1961
  const open = !collapsed.has(group.id);
1244
1962
  const groupName = `Flow ${String(gi + 1)}`;
1245
- return /* @__PURE__ */ jsxs13("div", { className: "hl-outline-group", role: "treeitem", "aria-expanded": open, children: [
1246
- /* @__PURE__ */ jsxs13(
1963
+ return /* @__PURE__ */ jsxs16("div", { className: "hl-outline-group", role: "treeitem", "aria-expanded": open, children: [
1964
+ /* @__PURE__ */ jsxs16(
1247
1965
  "button",
1248
1966
  {
1249
1967
  className: "hl-outline-grouphead",
1250
1968
  onClick: () => toggleGroup(group.id),
1251
1969
  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 })
1970
+ /* @__PURE__ */ jsx27("span", { className: "hl-outline-caret" + (open ? " is-open" : ""), "aria-hidden": true, children: "\u25B8" }),
1971
+ /* @__PURE__ */ jsx27("span", { className: "hl-outline-groupname", children: groupName }),
1972
+ /* @__PURE__ */ jsx27("span", { className: "hl-outline-count hl-mono", children: group.nodeIds.size })
1255
1973
  ]
1256
1974
  }
1257
1975
  ),
1258
- open && /* @__PURE__ */ jsx21("ul", { className: "hl-outline-members", children: members.map((node) => {
1976
+ open && /* @__PURE__ */ jsx27("ul", { className: "hl-outline-members", children: members.map((node) => {
1259
1977
  const active = selection.isSelected(node.id);
1260
1978
  const isHidden = viewState.isHidden(node.id);
1261
1979
  const isLocked = viewState.isLocked(node.id);
1262
1980
  const { imageUrl } = node.params;
1263
1981
  const thumbnail = typeof imageUrl === "string" && imageUrl.length > 0 ? imageUrl : void 0;
1264
- return /* @__PURE__ */ jsx21("li", { children: /* @__PURE__ */ jsxs13(
1982
+ return /* @__PURE__ */ jsx27("li", { children: /* @__PURE__ */ jsxs16(
1265
1983
  "div",
1266
1984
  {
1267
1985
  className: "hl-outline-row" + (active ? " is-active" : "") + (isHidden ? " is-hidden" : "") + (isLocked ? " is-locked" : ""),
1268
1986
  children: [
1269
- /* @__PURE__ */ jsxs13("button", { className: "hl-outline-main", onClick: () => select(node.id), children: [
1270
- /* @__PURE__ */ jsx21(
1987
+ /* @__PURE__ */ jsxs16("button", { className: "hl-outline-main", onClick: () => select(node.id), children: [
1988
+ /* @__PURE__ */ jsx27(
1271
1989
  "span",
1272
1990
  {
1273
1991
  className: `hl-outline-icon hl-kind-${node.kind.toLowerCase()}`,
1274
1992
  "aria-hidden": true,
1275
- children: thumbnail !== void 0 ? /* @__PURE__ */ jsx21(
1993
+ children: thumbnail !== void 0 ? /* @__PURE__ */ jsx27(
1276
1994
  "img",
1277
1995
  {
1278
1996
  className: "hl-outline-thumb",
1279
1997
  src: thumbnail,
1280
1998
  alt: ""
1281
1999
  }
1282
- ) : /* @__PURE__ */ jsx21(NodeIcon, { code: node.code, kind: node.kind, size: 13 })
2000
+ ) : /* @__PURE__ */ jsx27(NodeIcon, { code: node.code, kind: node.kind, size: 13 })
1283
2001
  }
1284
2002
  ),
1285
- /* @__PURE__ */ jsx21("span", { className: "hl-outline-name", children: node.name }),
2003
+ /* @__PURE__ */ jsx27("span", { className: "hl-outline-name", children: node.name }),
1286
2004
  (() => {
1287
2005
  const diag = diagnostics.worstOf(node.id);
1288
- return diag === void 0 ? null : /* @__PURE__ */ jsx21(DiagnosticBadge, { diagnostic: diag, variant: "hl-outline-diag", size: 11 });
2006
+ return diag === void 0 ? null : /* @__PURE__ */ jsx27(DiagnosticBadge, { diagnostic: diag, variant: "hl-outline-diag", size: 11 });
1289
2007
  })(),
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 }) })
2008
+ isLocked && /* @__PURE__ */ jsx27("span", { className: "hl-outline-badge", "aria-hidden": true, children: /* @__PURE__ */ jsx27(LockIcon, { size: 11 }) }),
2009
+ isHidden && /* @__PURE__ */ jsx27("span", { className: "hl-outline-badge", "aria-hidden": true, children: /* @__PURE__ */ jsx27(EyeOffIcon, { size: 11 }) })
1292
2010
  ] }),
1293
- /* @__PURE__ */ jsxs13("span", { className: "hl-outline-controls", children: [
1294
- /* @__PURE__ */ jsx21(
2011
+ /* @__PURE__ */ jsxs16("span", { className: "hl-outline-controls", children: [
2012
+ /* @__PURE__ */ jsx27(
1295
2013
  "button",
1296
2014
  {
1297
2015
  className: "hl-outline-ctl" + (isHidden ? " is-active-ctl" : ""),
1298
2016
  onClick: () => viewState.toggleHidden(node.id),
1299
2017
  "aria-label": isHidden ? "Show layer" : "Hide layer",
1300
2018
  title: isHidden ? "Show" : "Hide",
1301
- children: isHidden ? /* @__PURE__ */ jsx21(EyeOffIcon, {}) : /* @__PURE__ */ jsx21(EyeIcon, {})
2019
+ children: isHidden ? /* @__PURE__ */ jsx27(EyeOffIcon, {}) : /* @__PURE__ */ jsx27(EyeIcon, {})
1302
2020
  }
1303
2021
  ),
1304
- /* @__PURE__ */ jsx21(
2022
+ /* @__PURE__ */ jsx27(
1305
2023
  "button",
1306
2024
  {
1307
2025
  className: "hl-outline-ctl" + (isLocked ? " is-active-ctl" : ""),
1308
2026
  onClick: () => viewState.toggleLocked(node.id),
1309
2027
  "aria-label": isLocked ? "Unlock layer" : "Lock layer",
1310
2028
  title: isLocked ? "Unlock" : "Lock",
1311
- children: isLocked ? /* @__PURE__ */ jsx21(LockIcon, {}) : /* @__PURE__ */ jsx21(UnlockIcon, {})
2029
+ children: isLocked ? /* @__PURE__ */ jsx27(LockIcon, {}) : /* @__PURE__ */ jsx27(UnlockIcon, {})
1312
2030
  }
1313
2031
  )
1314
2032
  ] })
@@ -1321,21 +2039,48 @@ function OutlineTree({ core, onFocus }) {
1321
2039
  }
1322
2040
 
1323
2041
  // src/panels/PreviewPanel.tsx
1324
- import { jsx as jsx22, jsxs as jsxs14 } from "react/jsx-runtime";
2042
+ import { jsx as jsx28, jsxs as jsxs17 } from "react/jsx-runtime";
1325
2043
  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 })
2044
+ return /* @__PURE__ */ jsxs17("section", { className: "hl-preview", "aria-label": "Preview", children: [
2045
+ /* @__PURE__ */ jsxs17("div", { className: "hl-preview-head", children: [
2046
+ /* @__PURE__ */ jsx28("span", { className: "hl-preview-title", children: title }),
2047
+ status !== void 0 && /* @__PURE__ */ jsx28("span", { className: "hl-preview-status hl-mono", children: status })
1330
2048
  ] }),
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" }) })
2049
+ /* @__PURE__ */ jsx28("div", { className: "hl-preview-frame", children: imageUrl !== void 0 && imageUrl.length > 0 ? /* @__PURE__ */ jsx28("img", { className: "hl-preview-img", src: imageUrl, alt: title, draggable: false }) : /* @__PURE__ */ jsx28("div", { className: "hl-preview-placeholder hl-mono", children: "no output yet" }) })
1332
2050
  ] });
1333
2051
  }
1334
2052
 
2053
+ // src/panels/PanelHead.tsx
2054
+ import { jsx as jsx29, jsxs as jsxs18 } from "react/jsx-runtime";
2055
+ function PanelHead({ leading, meta, children }) {
2056
+ return /* @__PURE__ */ jsxs18("header", { className: "hl-panel-head", children: [
2057
+ leading,
2058
+ meta === void 0 ? null : /* @__PURE__ */ jsx29("span", { className: "hl-panel-meta hl-mono", children: meta }),
2059
+ children === void 0 ? null : /* @__PURE__ */ jsx29("span", { className: "hl-panel-actions", children })
2060
+ ] });
2061
+ }
2062
+ function PanelAction({ icon, label, onClick, pressed, disabled = false, title }) {
2063
+ return /* @__PURE__ */ jsxs18(
2064
+ "button",
2065
+ {
2066
+ type: "button",
2067
+ className: `hl-panel-action${pressed === true ? " is-on" : ""}`,
2068
+ "aria-pressed": pressed,
2069
+ disabled,
2070
+ title: title != null ? title : label,
2071
+ onClick,
2072
+ children: [
2073
+ /* @__PURE__ */ jsx29("span", { className: "hl-panel-action-icon", "aria-hidden": true, children: icon }),
2074
+ /* @__PURE__ */ jsx29("span", { className: "hl-panel-action-label", children: label })
2075
+ ]
2076
+ }
2077
+ );
2078
+ }
2079
+
1335
2080
  // src/panels/RailTabs.tsx
1336
- import { jsx as jsx23 } from "react/jsx-runtime";
2081
+ import { jsx as jsx30 } from "react/jsx-runtime";
1337
2082
  function RailTabs({ tabs, active, onChange }) {
1338
- return /* @__PURE__ */ jsx23("div", { className: "hl-railtabs", role: "tablist", children: tabs.map((tab) => /* @__PURE__ */ jsx23(
2083
+ return /* @__PURE__ */ jsx30("div", { className: "hl-railtabs", role: "tablist", children: tabs.map((tab) => /* @__PURE__ */ jsx30(
1339
2084
  "button",
1340
2085
  {
1341
2086
  role: "tab",
@@ -1349,20 +2094,20 @@ function RailTabs({ tabs, active, onChange }) {
1349
2094
  }
1350
2095
 
1351
2096
  // src/panels/AssetGrid.tsx
1352
- import { jsx as jsx24 } from "react/jsx-runtime";
2097
+ import { jsx as jsx31 } from "react/jsx-runtime";
1353
2098
  function AssetGrid({ items, onSelect, emptyHint = "No assets yet" }) {
1354
2099
  if (items.length === 0) {
1355
- return /* @__PURE__ */ jsx24("p", { className: "hl-assetgrid-empty hl-mono", children: emptyHint });
2100
+ return /* @__PURE__ */ jsx31("p", { className: "hl-assetgrid-empty hl-mono", children: emptyHint });
1356
2101
  }
1357
- return /* @__PURE__ */ jsx24("div", { className: "hl-assetgrid", children: items.map((item) => {
2102
+ return /* @__PURE__ */ jsx31("div", { className: "hl-assetgrid", children: items.map((item) => {
1358
2103
  var _a;
1359
- return /* @__PURE__ */ jsx24(
2104
+ return /* @__PURE__ */ jsx31(
1360
2105
  "button",
1361
2106
  {
1362
2107
  className: "hl-assetgrid-cell",
1363
2108
  onClick: () => onSelect == null ? void 0 : onSelect(item.id),
1364
2109
  title: item.label,
1365
- children: /* @__PURE__ */ jsx24("img", { className: "hl-assetgrid-img", src: item.url, alt: (_a = item.label) != null ? _a : "", loading: "lazy" })
2110
+ children: /* @__PURE__ */ jsx31("img", { className: "hl-assetgrid-img", src: item.url, alt: (_a = item.label) != null ? _a : "", loading: "lazy" })
1366
2111
  },
1367
2112
  item.id
1368
2113
  );
@@ -1370,10 +2115,10 @@ function AssetGrid({ items, onSelect, emptyHint = "No assets yet" }) {
1370
2115
  }
1371
2116
 
1372
2117
  // src/panels/PositionPanel.tsx
1373
- import { useCallback as useCallback4, useEffect as useEffect3, useReducer } from "react";
2118
+ import { useCallback as useCallback6, useEffect as useEffect4, useReducer } from "react";
1374
2119
  import { MoveNodeCommand } from "@hlang-tech/editor-core";
1375
- import { useSelection as useSelection3 } from "@hlang-tech/editor-react";
1376
- import { jsx as jsx25, jsxs as jsxs15 } from "react/jsx-runtime";
2120
+ import { useReadOnly as useReadOnly3, useSelection as useSelection3 } from "@hlang-tech/editor-react";
2121
+ import { jsx as jsx32, jsxs as jsxs19 } from "react/jsx-runtime";
1377
2122
  function selectedNodes(core) {
1378
2123
  const ids = core.selection.getSelectedNodes();
1379
2124
  const out = [];
@@ -1388,8 +2133,9 @@ function selectedNodes(core) {
1388
2133
  function PositionPanel({ core, livePosition }) {
1389
2134
  var _a;
1390
2135
  useSelection3(core);
2136
+ const readOnly = useReadOnly3(core);
1391
2137
  const [, bump] = useReducer((n) => n + 1, 0);
1392
- useEffect3(() => {
2138
+ useEffect4(() => {
1393
2139
  const kinds = ["nodeChanged", "nodeAdded", "nodeRemoved"];
1394
2140
  const disposers = kinds.map((k) => core.events.on(k, bump));
1395
2141
  return () => {
@@ -1401,7 +2147,7 @@ function PositionPanel({ core, livePosition }) {
1401
2147
  const nodes = selectedNodes(core);
1402
2148
  const single = nodes.length === 1 ? nodes[0] : void 0;
1403
2149
  const shownPos = single !== void 0 ? (_a = livePosition == null ? void 0 : livePosition(single.id)) != null ? _a : single.position : void 0;
1404
- const moveAll = useCallback4(
2150
+ const moveAll = useCallback6(
1405
2151
  (fn) => {
1406
2152
  const targets = selectedNodes(core);
1407
2153
  if (targets.length === 0) {
@@ -1415,7 +2161,7 @@ function PositionPanel({ core, livePosition }) {
1415
2161
  },
1416
2162
  [core]
1417
2163
  );
1418
- const align = useCallback4(
2164
+ const align = useCallback6(
1419
2165
  (kind) => {
1420
2166
  const targets = selectedNodes(core);
1421
2167
  if (targets.length === 0) {
@@ -1448,7 +2194,7 @@ function PositionPanel({ core, livePosition }) {
1448
2194
  },
1449
2195
  [core, moveAll]
1450
2196
  );
1451
- const setCoord = useCallback4(
2197
+ const setCoord = useCallback6(
1452
2198
  (axis, value) => {
1453
2199
  if (single === void 0 || Number.isNaN(value)) {
1454
2200
  return;
@@ -1458,7 +2204,7 @@ function PositionPanel({ core, livePosition }) {
1458
2204
  },
1459
2205
  [core, single]
1460
2206
  );
1461
- const disabled = nodes.length === 0;
2207
+ const disabled = readOnly || nodes.length === 0;
1462
2208
  const alignButtons = [
1463
2209
  { kind: "left", label: "Align left", d: "M5 4v16M9 8h8M9 16h5" },
1464
2210
  { kind: "hcenter", label: "Align centre", d: "M12 4v16M8 9h8M9 15h6" },
@@ -1467,9 +2213,9 @@ function PositionPanel({ core, livePosition }) {
1467
2213
  { kind: "vcenter", label: "Align middle", d: "M4 12h16M9 8v8M15 9v6" },
1468
2214
  { kind: "bottom", label: "Align bottom", d: "M4 19h16M8 7v8M16 12v3" }
1469
2215
  ];
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(
2216
+ return /* @__PURE__ */ jsxs19("section", { className: "hl-position", children: [
2217
+ /* @__PURE__ */ jsx32("h3", { className: "hl-position-title hl-mono", children: "Position" }),
2218
+ /* @__PURE__ */ jsx32("div", { className: "hl-position-align", role: "group", "aria-label": "Align", children: alignButtons.map((b) => /* @__PURE__ */ jsx32(
1473
2219
  "button",
1474
2220
  {
1475
2221
  className: "hl-position-alignbtn",
@@ -1477,55 +2223,55 @@ function PositionPanel({ core, livePosition }) {
1477
2223
  disabled,
1478
2224
  title: b.label,
1479
2225
  "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 }) })
2226
+ children: /* @__PURE__ */ jsx32("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__ */ jsx32("path", { d: b.d }) })
1481
2227
  },
1482
2228
  b.kind
1483
2229
  )) }),
1484
- /* @__PURE__ */ jsxs15("div", { className: "hl-position-coords", children: [
1485
- /* @__PURE__ */ jsxs15("label", { className: "hl-position-field", children: [
1486
- /* @__PURE__ */ jsx25(
2230
+ /* @__PURE__ */ jsxs19("div", { className: "hl-position-coords", children: [
2231
+ /* @__PURE__ */ jsxs19("label", { className: "hl-position-field", children: [
2232
+ /* @__PURE__ */ jsx32(
1487
2233
  "input",
1488
2234
  {
1489
2235
  type: "number",
1490
2236
  className: "hl-position-input hl-mono",
1491
2237
  value: shownPos !== void 0 ? Math.round(shownPos.x) : "",
1492
- disabled: single === void 0,
2238
+ disabled: readOnly || single === void 0,
1493
2239
  onChange: (e) => setCoord("x", Number(e.target.value)),
1494
2240
  "aria-label": "X position"
1495
2241
  }
1496
2242
  ),
1497
- /* @__PURE__ */ jsx25("span", { className: "hl-position-axis hl-mono", children: "X" })
2243
+ /* @__PURE__ */ jsx32("span", { className: "hl-position-axis hl-mono", children: "X" })
1498
2244
  ] }),
1499
- /* @__PURE__ */ jsxs15("label", { className: "hl-position-field", children: [
1500
- /* @__PURE__ */ jsx25(
2245
+ /* @__PURE__ */ jsxs19("label", { className: "hl-position-field", children: [
2246
+ /* @__PURE__ */ jsx32(
1501
2247
  "input",
1502
2248
  {
1503
2249
  type: "number",
1504
2250
  className: "hl-position-input hl-mono",
1505
2251
  value: shownPos !== void 0 ? Math.round(shownPos.y) : "",
1506
- disabled: single === void 0,
2252
+ disabled: readOnly || single === void 0,
1507
2253
  onChange: (e) => setCoord("y", Number(e.target.value)),
1508
2254
  "aria-label": "Y position"
1509
2255
  }
1510
2256
  ),
1511
- /* @__PURE__ */ jsx25("span", { className: "hl-position-axis hl-mono", children: "Y" })
2257
+ /* @__PURE__ */ jsx32("span", { className: "hl-position-axis hl-mono", children: "Y" })
1512
2258
  ] })
1513
2259
  ] })
1514
2260
  ] });
1515
2261
  }
1516
2262
 
1517
2263
  // 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";
2264
+ import { useCallback as useCallback7, useEffect as useEffect5, useMemo as useMemo5, useRef as useRef5, useState as useState5 } from "react";
2265
+ import { Fragment as Fragment5, jsx as jsx33, jsxs as jsxs20 } from "react/jsx-runtime";
1520
2266
  var PANEL_WIDTH = 320;
1521
2267
  var PANEL_MAX_HEIGHT = 360;
1522
2268
  function NodeSearch({ controller }) {
1523
2269
  const { state } = controller;
1524
- const [query, setQuery] = useState4("");
1525
- const [active, setActive] = useState4(0);
1526
- const inputRef = useRef3(null);
2270
+ const [query, setQuery] = useState5("");
2271
+ const [active, setActive] = useState5(0);
2272
+ const inputRef = useRef5(null);
1527
2273
  const isOpen = state !== void 0;
1528
- useEffect4(() => {
2274
+ useEffect5(() => {
1529
2275
  var _a;
1530
2276
  if (isOpen) {
1531
2277
  setQuery("");
@@ -1533,12 +2279,12 @@ function NodeSearch({ controller }) {
1533
2279
  (_a = inputRef.current) == null ? void 0 : _a.focus();
1534
2280
  }
1535
2281
  }, [isOpen]);
1536
- const results = useMemo4(
2282
+ const results = useMemo5(
1537
2283
  () => state !== void 0 ? controller.results(query) : [],
1538
2284
  [controller, state, query]
1539
2285
  );
1540
2286
  const clampedActive = results.length === 0 ? 0 : Math.min(active, results.length - 1);
1541
- const choose = useCallback5(
2287
+ const choose = useCallback7(
1542
2288
  (view) => {
1543
2289
  if (view !== void 0) {
1544
2290
  controller.commit(view);
@@ -1546,7 +2292,7 @@ function NodeSearch({ controller }) {
1546
2292
  },
1547
2293
  [controller]
1548
2294
  );
1549
- const onKeyDown = useCallback5(
2295
+ const onKeyDown = useCallback7(
1550
2296
  (event) => {
1551
2297
  switch (event.key) {
1552
2298
  case "ArrowDown":
@@ -1576,8 +2322,8 @@ function NodeSearch({ controller }) {
1576
2322
  }
1577
2323
  const style = clampToViewport(state.screen.x, state.screen.y);
1578
2324
  const connectMode = state.pending !== void 0;
1579
- return /* @__PURE__ */ jsxs16(Fragment4, { children: [
1580
- /* @__PURE__ */ jsx26(
2325
+ return /* @__PURE__ */ jsxs20(Fragment5, { children: [
2326
+ /* @__PURE__ */ jsx33(
1581
2327
  "div",
1582
2328
  {
1583
2329
  className: "hl-node-search-scrim",
@@ -1586,10 +2332,10 @@ function NodeSearch({ controller }) {
1586
2332
  }
1587
2333
  }
1588
2334
  ),
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(
2335
+ /* @__PURE__ */ jsxs20("div", { className: "hl-node-search", style, role: "dialog", "aria-label": "Create node", children: [
2336
+ /* @__PURE__ */ jsxs20("div", { className: "hl-node-search-field", children: [
2337
+ /* @__PURE__ */ jsx33("span", { className: "hl-node-search-glyph", "aria-hidden": true, children: connectMode ? "\u2933" : "\uFF0B" }),
2338
+ /* @__PURE__ */ jsx33(
1593
2339
  "input",
1594
2340
  {
1595
2341
  ref: inputRef,
@@ -1607,12 +2353,12 @@ function NodeSearch({ controller }) {
1607
2353
  }
1608
2354
  )
1609
2355
  ] }),
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" }),
2356
+ /* @__PURE__ */ jsxs20("ul", { className: "hl-node-search-list", role: "listbox", children: [
2357
+ results.length === 0 && /* @__PURE__ */ jsx33("li", { className: "hl-node-search-empty hl-mono", children: "no matching nodes" }),
1612
2358
  results.map((view, index) => {
1613
2359
  const descriptor = view.result.value;
1614
2360
  const kind = descriptor.kind;
1615
- return /* @__PURE__ */ jsxs16(
2361
+ return /* @__PURE__ */ jsxs20(
1616
2362
  "li",
1617
2363
  {
1618
2364
  role: "option",
@@ -1624,15 +2370,15 @@ function NodeSearch({ controller }) {
1624
2370
  choose(view);
1625
2371
  },
1626
2372
  children: [
1627
- /* @__PURE__ */ jsx26(
2373
+ /* @__PURE__ */ jsx33(
1628
2374
  "span",
1629
2375
  {
1630
2376
  className: `hl-node-search-kind hl-mono hl-kind-${kind.toLowerCase()}`,
1631
2377
  children: kind
1632
2378
  }
1633
2379
  ),
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 })
2380
+ /* @__PURE__ */ jsx33("span", { className: "hl-node-search-label", children: highlight(descriptor.displayName, view.result.match) }),
2381
+ view.connectPortId !== void 0 && /* @__PURE__ */ jsx33("span", { className: "hl-node-search-port hl-mono", children: view.connectPortId })
1636
2382
  ]
1637
2383
  },
1638
2384
  descriptor.code
@@ -1646,7 +2392,7 @@ function highlight(text, match) {
1646
2392
  const hit = new Set(match.positions);
1647
2393
  const chars = Array.from(text);
1648
2394
  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)
2395
+ (ch, i) => hit.has(i) ? /* @__PURE__ */ jsx33("mark", { className: "hl-node-search-hit", children: ch }, i) : /* @__PURE__ */ jsx33("span", { children: ch }, i)
1650
2396
  );
1651
2397
  }
1652
2398
  function clampToViewport(x, y) {
@@ -1659,7 +2405,7 @@ function clampToViewport(x, y) {
1659
2405
  }
1660
2406
 
1661
2407
  // src/command-palette/useNodeSearch.ts
1662
- import { useCallback as useCallback6, useMemo as useMemo5, useState as useState5 } from "react";
2408
+ import { useCallback as useCallback8, useMemo as useMemo6, useState as useState6 } from "react";
1663
2409
  import { AddNodeCommand, ConnectCommand } from "@hlang-tech/editor-core";
1664
2410
 
1665
2411
  // src/command-palette/fuzzy.ts
@@ -1750,21 +2496,21 @@ function firstCompletingPort(ports, pending, validator) {
1750
2496
 
1751
2497
  // src/command-palette/useNodeSearch.ts
1752
2498
  function useNodeSearch(core) {
1753
- const [state, setState] = useState5(void 0);
1754
- const openBlank = useCallback6((screen, flow) => {
2499
+ const [state, setState] = useState6(void 0);
2500
+ const openBlank = useCallback8((screen, flow) => {
1755
2501
  setState({ screen, flow });
1756
2502
  }, []);
1757
- const openFromConnection = useCallback6(
2503
+ const openFromConnection = useCallback8(
1758
2504
  (screen, flow, pending) => {
1759
2505
  setState({ screen, flow, pending });
1760
2506
  },
1761
2507
  []
1762
2508
  );
1763
- const close = useCallback6(() => {
2509
+ const close = useCallback8(() => {
1764
2510
  setState(void 0);
1765
2511
  }, []);
1766
2512
  const descriptors = core.registry.list();
1767
- const results = useCallback6(
2513
+ const results = useCallback8(
1768
2514
  (query) => {
1769
2515
  if ((state == null ? void 0 : state.pending) !== void 0) {
1770
2516
  return connectableCandidates(
@@ -1778,7 +2524,7 @@ function useNodeSearch(core) {
1778
2524
  },
1779
2525
  [state, descriptors, core]
1780
2526
  );
1781
- const commit = useCallback6(
2527
+ const commit = useCallback8(
1782
2528
  (view) => {
1783
2529
  if (state === void 0) {
1784
2530
  return;
@@ -1802,14 +2548,14 @@ function useNodeSearch(core) {
1802
2548
  },
1803
2549
  [state, core]
1804
2550
  );
1805
- return useMemo5(
2551
+ return useMemo6(
1806
2552
  () => ({ state, openBlank, openFromConnection, close, results, commit }),
1807
2553
  [state, openBlank, openFromConnection, close, results, commit]
1808
2554
  );
1809
2555
  }
1810
2556
 
1811
2557
  // src/command-palette/useNodeSearchTriggers.ts
1812
- import { useCallback as useCallback7, useRef as useRef4 } from "react";
2558
+ import { useCallback as useCallback9, useRef as useRef6 } from "react";
1813
2559
  import { useReactFlow } from "@xyflow/react";
1814
2560
  var DEFAULT_DOUBLE_CLICK_MS = 350;
1815
2561
  function useNodeSearchTriggers(core, search, options = {}) {
@@ -1817,16 +2563,16 @@ function useNodeSearchTriggers(core, search, options = {}) {
1817
2563
  const flow = useReactFlow();
1818
2564
  const doubleClickMs = (_a = options.doubleClickMs) != null ? _a : DEFAULT_DOUBLE_CLICK_MS;
1819
2565
  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) => {
2566
+ const startRef = useRef6(null);
2567
+ const lastPaneClickRef = useRef6(Number.NEGATIVE_INFINITY);
2568
+ const onConnectStart = useCallback9((_event, params) => {
1823
2569
  startRef.current = params;
1824
2570
  }, []);
1825
- const onConnectEnd = useCallback7(
2571
+ const onConnectEnd = useCallback9(
1826
2572
  (event, connectionState) => {
1827
2573
  const start = startRef.current;
1828
2574
  startRef.current = null;
1829
- if (connectionState.toNode !== null || start === null) {
2575
+ if (core.mode.readOnly || connectionState.toNode !== null || start === null) {
1830
2576
  return;
1831
2577
  }
1832
2578
  if (start.nodeId === null || start.handleId === null || start.handleType === null) {
@@ -1855,8 +2601,11 @@ function useNodeSearchTriggers(core, search, options = {}) {
1855
2601
  },
1856
2602
  [core, flow, search]
1857
2603
  );
1858
- const onPaneClick = useCallback7(
2604
+ const onPaneClick = useCallback9(
1859
2605
  (event) => {
2606
+ if (core.mode.readOnly) {
2607
+ return;
2608
+ }
1860
2609
  const t = now();
1861
2610
  const isDouble = t - lastPaneClickRef.current <= doubleClickMs;
1862
2611
  lastPaneClickRef.current = t;
@@ -1867,7 +2616,7 @@ function useNodeSearchTriggers(core, search, options = {}) {
1867
2616
  const screen = { x: event.clientX, y: event.clientY };
1868
2617
  search.openBlank(screen, flow.screenToFlowPosition(screen));
1869
2618
  },
1870
- [flow, search, doubleClickMs, now]
2619
+ [core, flow, search, doubleClickMs, now]
1871
2620
  );
1872
2621
  return { onConnectStart, onConnectEnd, onPaneClick };
1873
2622
  }
@@ -1887,7 +2636,7 @@ function pointerOf(event) {
1887
2636
  }
1888
2637
 
1889
2638
  // src/hooks/useEditorHotkeys.ts
1890
- import { useEffect as useEffect5 } from "react";
2639
+ import { useEffect as useEffect6 } from "react";
1891
2640
  import { useClipboard } from "@hlang-tech/editor-react";
1892
2641
  function isEditableTarget(target) {
1893
2642
  if (!(target instanceof HTMLElement)) {
@@ -1906,7 +2655,7 @@ function withinScope(target, scope) {
1906
2655
  function useEditorHotkeys(core, options = {}) {
1907
2656
  const clipboard = useClipboard(core);
1908
2657
  const { scope } = options;
1909
- useEffect5(() => {
2658
+ useEffect6(() => {
1910
2659
  const onKeyDown = (event) => {
1911
2660
  if (!withinScope(event.target, scope) || isEditableTarget(event.target)) {
1912
2661
  return;
@@ -1969,33 +2718,33 @@ function useEditorHotkeys(core, options = {}) {
1969
2718
  }
1970
2719
 
1971
2720
  // 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";
2721
+ import { useCallback as useCallback10, useEffect as useEffect7, useMemo as useMemo7, useRef as useRef7, useState as useState7 } from "react";
2722
+ import { Fragment as Fragment6, jsx as jsx34, jsxs as jsxs21 } from "react/jsx-runtime";
1974
2723
  var PANEL_WIDTH2 = 208;
1975
2724
  var ITEM_HEIGHT = 34;
1976
2725
  var VIEWPORT_MARGIN = 8;
1977
2726
  var MENU_VPAD = 12;
1978
2727
  function NodeContextMenu({ controller, items }) {
1979
2728
  const { state } = controller;
1980
- const [active, setActive] = useState6(0);
1981
- const menuRef = useRef5(null);
2729
+ const [active, setActive] = useState7(0);
2730
+ const menuRef = useRef7(null);
1982
2731
  const isOpen = state !== void 0;
1983
- useEffect6(() => {
2732
+ useEffect7(() => {
1984
2733
  var _a;
1985
2734
  if (isOpen) {
1986
2735
  setActive(0);
1987
2736
  (_a = menuRef.current) == null ? void 0 : _a.focus();
1988
2737
  }
1989
2738
  }, [isOpen]);
1990
- const rows = useMemo6(
2739
+ const rows = useMemo7(
1991
2740
  () => state !== void 0 ? items(state.nodeId) : [],
1992
2741
  [items, state]
1993
2742
  );
1994
- const enabledIndices = useMemo6(
2743
+ const enabledIndices = useMemo7(
1995
2744
  () => rows.map((r, i) => r.disabled ? -1 : i).filter((i) => i >= 0),
1996
2745
  [rows]
1997
2746
  );
1998
- const move = useCallback8(
2747
+ const move = useCallback10(
1999
2748
  (delta) => {
2000
2749
  if (enabledIndices.length === 0) {
2001
2750
  return;
@@ -2009,7 +2758,7 @@ function NodeContextMenu({ controller, items }) {
2009
2758
  },
2010
2759
  [enabledIndices]
2011
2760
  );
2012
- const choose = useCallback8(
2761
+ const choose = useCallback10(
2013
2762
  (item) => {
2014
2763
  if (item === void 0 || item.disabled) {
2015
2764
  return;
@@ -2019,7 +2768,7 @@ function NodeContextMenu({ controller, items }) {
2019
2768
  },
2020
2769
  [controller]
2021
2770
  );
2022
- const onKeyDown = useCallback8(
2771
+ const onKeyDown = useCallback10(
2023
2772
  (event) => {
2024
2773
  switch (event.key) {
2025
2774
  case "ArrowDown":
@@ -2048,8 +2797,8 @@ function NodeContextMenu({ controller, items }) {
2048
2797
  return null;
2049
2798
  }
2050
2799
  const style = clampToViewport2(state.screen.x, state.screen.y, rows.length);
2051
- return /* @__PURE__ */ jsxs17(Fragment5, { children: [
2052
- /* @__PURE__ */ jsx27(
2800
+ return /* @__PURE__ */ jsxs21(Fragment6, { children: [
2801
+ /* @__PURE__ */ jsx34(
2053
2802
  "div",
2054
2803
  {
2055
2804
  className: "hl-ctx-scrim",
@@ -2062,7 +2811,7 @@ function NodeContextMenu({ controller, items }) {
2062
2811
  }
2063
2812
  }
2064
2813
  ),
2065
- /* @__PURE__ */ jsx27(
2814
+ /* @__PURE__ */ jsx34(
2066
2815
  "div",
2067
2816
  {
2068
2817
  ref: menuRef,
@@ -2072,7 +2821,7 @@ function NodeContextMenu({ controller, items }) {
2072
2821
  "aria-label": "Node actions",
2073
2822
  tabIndex: -1,
2074
2823
  onKeyDown,
2075
- children: rows.map((item, index) => /* @__PURE__ */ jsxs17(
2824
+ children: rows.map((item, index) => /* @__PURE__ */ jsxs21(
2076
2825
  "button",
2077
2826
  {
2078
2827
  type: "button",
@@ -2085,9 +2834,9 @@ function NodeContextMenu({ controller, items }) {
2085
2834
  choose(item);
2086
2835
  },
2087
2836
  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 })
2837
+ /* @__PURE__ */ jsx34("span", { className: "hl-ctx-item-icon", "aria-hidden": true, children: item.icon }),
2838
+ /* @__PURE__ */ jsx34("span", { className: "hl-ctx-item-label", children: item.label }),
2839
+ item.shortcut !== void 0 && /* @__PURE__ */ jsx34("span", { className: "hl-ctx-item-key hl-mono", children: item.shortcut })
2091
2840
  ]
2092
2841
  },
2093
2842
  item.id
@@ -2107,21 +2856,21 @@ function clampToViewport2(x, y, rowCount) {
2107
2856
  }
2108
2857
 
2109
2858
  // src/context-menu/useNodeContextMenu.ts
2110
- import { useCallback as useCallback9, useMemo as useMemo7, useState as useState7 } from "react";
2859
+ import { useCallback as useCallback11, useMemo as useMemo8, useState as useState8 } from "react";
2111
2860
  function useNodeContextMenu() {
2112
- const [state, setState] = useState7(void 0);
2113
- const openForNode = useCallback9((screen, nodeId) => {
2861
+ const [state, setState] = useState8(void 0);
2862
+ const openForNode = useCallback11((screen, nodeId) => {
2114
2863
  setState({ screen, nodeId });
2115
2864
  }, []);
2116
- const close = useCallback9(() => {
2865
+ const close = useCallback11(() => {
2117
2866
  setState(void 0);
2118
2867
  }, []);
2119
- return useMemo7(() => ({ state, openForNode, close }), [state, openForNode, close]);
2868
+ return useMemo8(() => ({ state, openForNode, close }), [state, openForNode, close]);
2120
2869
  }
2121
2870
 
2122
2871
  // 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";
2872
+ import { useEffect as useEffect8, useRef as useRef8 } from "react";
2873
+ import { Fragment as Fragment7, jsx as jsx35, jsxs as jsxs22 } from "react/jsx-runtime";
2125
2874
  var DEFAULT_GROUPS = [
2126
2875
  {
2127
2876
  title: "Edit",
@@ -2157,8 +2906,8 @@ var DEFAULT_GROUPS = [
2157
2906
  }
2158
2907
  ];
2159
2908
  function Cheatsheet({ open, onClose, groups = DEFAULT_GROUPS }) {
2160
- const dialogRef = useRef6(null);
2161
- useEffect7(() => {
2909
+ const dialogRef = useRef8(null);
2910
+ useEffect8(() => {
2162
2911
  var _a;
2163
2912
  if (!open) {
2164
2913
  return;
@@ -2175,9 +2924,9 @@ function Cheatsheet({ open, onClose, groups = DEFAULT_GROUPS }) {
2175
2924
  if (!open) {
2176
2925
  return null;
2177
2926
  }
2178
- return /* @__PURE__ */ jsxs18(Fragment6, { children: [
2179
- /* @__PURE__ */ jsx28("div", { className: "hl-cheat-scrim", onPointerDown: onClose }),
2180
- /* @__PURE__ */ jsxs18(
2927
+ return /* @__PURE__ */ jsxs22(Fragment7, { children: [
2928
+ /* @__PURE__ */ jsx35("div", { className: "hl-cheat-scrim", onPointerDown: onClose }),
2929
+ /* @__PURE__ */ jsxs22(
2181
2930
  "div",
2182
2931
  {
2183
2932
  ref: dialogRef,
@@ -2187,18 +2936,18 @@ function Cheatsheet({ open, onClose, groups = DEFAULT_GROUPS }) {
2187
2936
  "aria-label": "Keyboard shortcuts",
2188
2937
  tabIndex: -1,
2189
2938
  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" })
2939
+ /* @__PURE__ */ jsxs22("div", { className: "hl-cheat-head", children: [
2940
+ /* @__PURE__ */ jsx35("span", { className: "hl-cheat-title", children: "Shortcuts" }),
2941
+ /* @__PURE__ */ jsx35("button", { className: "hl-cheat-close", onClick: onClose, "aria-label": "Close", children: "\u2715" })
2193
2942
  ] }),
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) => (
2943
+ /* @__PURE__ */ jsx35("div", { className: "hl-cheat-grid", children: groups.map((group) => /* @__PURE__ */ jsxs22("section", { className: "hl-cheat-group", children: [
2944
+ /* @__PURE__ */ jsx35("h3", { className: "hl-cheat-group-title hl-mono", children: group.title }),
2945
+ /* @__PURE__ */ jsx35("ul", { className: "hl-cheat-rows", children: group.rows.map((row) => (
2197
2946
  // A row is a binding: one action can have several (two ways to pan),
2198
2947
  // 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 })
2948
+ /* @__PURE__ */ jsxs22("li", { className: "hl-cheat-row", children: [
2949
+ /* @__PURE__ */ jsx35("span", { className: "hl-cheat-label", children: row.label }),
2950
+ /* @__PURE__ */ jsx35("kbd", { className: "hl-cheat-keys hl-mono", children: row.keys })
2202
2951
  ] }, `${row.keys}\0${row.label}`)
2203
2952
  )) })
2204
2953
  ] }, group.title)) })
@@ -2209,13 +2958,13 @@ function Cheatsheet({ open, onClose, groups = DEFAULT_GROUPS }) {
2209
2958
  }
2210
2959
 
2211
2960
  // 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 });
2961
+ import { createContext as createContext4, useCallback as useCallback12, useContext as useContext4, useMemo as useMemo9, useRef as useRef9, useState as useState9 } from "react";
2962
+ import { jsx as jsx36, jsxs as jsxs23 } from "react/jsx-runtime";
2963
+ var ToastCtx = createContext4({ show: () => void 0 });
2215
2964
  function ToastProvider({ children, duration = 2200 }) {
2216
- const [items, setItems] = useState8([]);
2217
- const seq = useRef7(0);
2218
- const show = useCallback10(
2965
+ const [items, setItems] = useState9([]);
2966
+ const seq = useRef9(0);
2967
+ const show = useCallback12(
2219
2968
  (message) => {
2220
2969
  seq.current += 1;
2221
2970
  const id = seq.current;
@@ -2226,22 +2975,22 @@ function ToastProvider({ children, duration = 2200 }) {
2226
2975
  },
2227
2976
  [duration]
2228
2977
  );
2229
- const api = useMemo8(() => ({ show }), [show]);
2230
- return /* @__PURE__ */ jsxs19(ToastCtx.Provider, { value: api, children: [
2978
+ const api = useMemo9(() => ({ show }), [show]);
2979
+ return /* @__PURE__ */ jsxs23(ToastCtx.Provider, { value: api, children: [
2231
2980
  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)) })
2981
+ /* @__PURE__ */ jsx36("div", { className: "hl-toast-stack", "aria-live": "polite", children: items.map((t) => /* @__PURE__ */ jsx36("div", { className: "hl-toast", children: t.message }, t.id)) })
2233
2982
  ] });
2234
2983
  }
2235
2984
  function useToast() {
2236
- return useContext3(ToastCtx);
2985
+ return useContext4(ToastCtx);
2237
2986
  }
2238
2987
 
2239
2988
  // src/hooks/useLiveSignals.ts
2240
- import { useEffect as useEffect8 } from "react";
2989
+ import { useEffect as useEffect9 } from "react";
2241
2990
  function useLiveSignals(core, handle, explicitStore) {
2242
2991
  const contextStore = useLiveSignalStore();
2243
2992
  const store = explicitStore != null ? explicitStore : contextStore;
2244
- useEffect8(() => {
2993
+ useEffect9(() => {
2245
2994
  if (store === null) {
2246
2995
  return;
2247
2996
  }
@@ -2250,6 +2999,8 @@ function useLiveSignals(core, handle, explicitStore) {
2250
2999
  id: edge.id,
2251
3000
  sourceNodeId: edge.source.nodeId,
2252
3001
  sourcePortId: edge.source.portId,
3002
+ targetNodeId: edge.target.nodeId,
3003
+ targetPortId: edge.target.portId,
2253
3004
  isError: edge.source.portId === "@node/error"
2254
3005
  }));
2255
3006
  store.setEdgeIndex(entries);
@@ -2262,7 +3013,8 @@ function useLiveSignals(core, handle, explicitStore) {
2262
3013
  removed.dispose();
2263
3014
  };
2264
3015
  }, [core, store]);
2265
- useEffect8(() => {
3016
+ useEffect9(() => {
3017
+ var _a, _b, _c;
2266
3018
  if (store === null) {
2267
3019
  return;
2268
3020
  }
@@ -2270,45 +3022,457 @@ function useLiveSignals(core, handle, explicitStore) {
2270
3022
  store.reset();
2271
3023
  return;
2272
3024
  }
2273
- const activitySub = handle.onActivity((e) => store.markActivity(e.nodeId, e.portId));
3025
+ const signalSub = (_b = (_a = handle.onDelivery) == null ? void 0 : _a.call(handle, (event) => {
3026
+ store.markDelivery(event);
3027
+ })) != null ? _b : handle.onActivity((e) => store.markActivity(e.nodeId, e.portId));
2274
3028
  const errorSub = handle.onError((e) => store.markError(e.nodeId));
3029
+ const finishedSub = (_c = handle.onFinished) == null ? void 0 : _c.call(handle, (e) => {
3030
+ store.markFinished(e.nodeId);
3031
+ });
2275
3032
  return () => {
2276
- activitySub.dispose();
3033
+ signalSub.dispose();
2277
3034
  errorSub.dispose();
3035
+ finishedSub == null ? void 0 : finishedSub.dispose();
2278
3036
  store.reset();
2279
3037
  };
2280
3038
  }, [store, handle]);
2281
3039
  }
3040
+
3041
+ // src/live/run-trace.ts
3042
+ var RUN_LOG_CAPACITY = 500;
3043
+ var RunTrace = class {
3044
+ constructor(options = {}) {
3045
+ /** The log as a ring: `ring[(head + i) % capacity]` is the i-th oldest entry. */
3046
+ this.ring = [];
3047
+ this.head = 0;
3048
+ this.droppedCount = 0;
3049
+ this.seq = 0;
3050
+ this.startedAt = 0;
3051
+ this.stats = /* @__PURE__ */ new Map();
3052
+ this.listeners = /* @__PURE__ */ new Set();
3053
+ /** Bumped by every change; the entries cache follows it. */
3054
+ this.revision = 0;
3055
+ /** Bumped once per notified frame; what subscribers compare. */
3056
+ this.version = 0;
3057
+ this.frame = null;
3058
+ /** The entries as an array, rebuilt only after a change. */
3059
+ this.entriesCache = [];
3060
+ this.entriesRevision = -1;
3061
+ var _a, _b;
3062
+ const capacity = (_a = options.capacity) != null ? _a : RUN_LOG_CAPACITY;
3063
+ if (!Number.isInteger(capacity) || capacity < 1) {
3064
+ throw new RangeError("RunTrace capacity must be a positive integer.");
3065
+ }
3066
+ this.capacity = capacity;
3067
+ this.deps = (_b = options.deps) != null ? _b : defaultLiveDeps;
3068
+ }
3069
+ /** Begins a run: clears everything and times entries from now. */
3070
+ start() {
3071
+ this.clear();
3072
+ this.startedAt = this.deps.now();
3073
+ }
3074
+ /** Forgets the log and the statistics. */
3075
+ clear() {
3076
+ this.ring.length = 0;
3077
+ this.head = 0;
3078
+ this.droppedCount = 0;
3079
+ this.stats.clear();
3080
+ this.changed();
3081
+ }
3082
+ recordDelivery(event) {
3083
+ this.append({
3084
+ kind: "delivery",
3085
+ seq: this.nextSeq(),
3086
+ at: this.elapsed(),
3087
+ sourceNodeId: event.sourceNodeId,
3088
+ sourcePortId: event.sourcePortId,
3089
+ targetNodeId: event.targetNodeId,
3090
+ targetPortId: event.targetPortId,
3091
+ message: event.message,
3092
+ duration: event.duration
3093
+ });
3094
+ const source = this.statsFor(event.sourceNodeId);
3095
+ source.sent += 1;
3096
+ source.lastSent = event.message;
3097
+ const target = this.statsFor(event.targetNodeId);
3098
+ target.received += 1;
3099
+ target.totalMs += event.duration;
3100
+ target.maxMs = Math.max(target.maxMs, event.duration);
3101
+ target.lastReceived = event.message;
3102
+ this.changed();
3103
+ }
3104
+ recordError(event) {
3105
+ this.append({ kind: "error", seq: this.nextSeq(), at: this.elapsed(), nodeId: event.nodeId, error: event.error });
3106
+ this.statsFor(event.nodeId).errors += 1;
3107
+ this.changed();
3108
+ }
3109
+ recordEnded(event) {
3110
+ this.append({ kind: "ended", seq: this.nextSeq(), at: this.elapsed(), nodeId: event.nodeId, portId: event.portId });
3111
+ this.statsFor(event.nodeId);
3112
+ this.changed();
3113
+ }
3114
+ recordFinished(event) {
3115
+ this.append({ kind: "finished", seq: this.nextSeq(), at: this.elapsed(), nodeId: event.nodeId });
3116
+ this.statsFor(event.nodeId).finished = true;
3117
+ this.changed();
3118
+ }
3119
+ recordDropped(event) {
3120
+ this.append({
3121
+ kind: "dropped",
3122
+ seq: this.nextSeq(),
3123
+ at: this.elapsed(),
3124
+ sourceNodeId: event.sourceNodeId,
3125
+ sourcePortId: event.sourcePortId,
3126
+ targetNodeId: event.targetNodeId,
3127
+ targetPortId: event.targetPortId,
3128
+ message: event.message
3129
+ });
3130
+ this.statsFor(event.sourceNodeId);
3131
+ this.statsFor(event.targetNodeId).dropped += 1;
3132
+ this.changed();
3133
+ }
3134
+ /** The log, oldest first. The same array is returned until something changes. */
3135
+ entries() {
3136
+ if (this.entriesRevision !== this.revision) {
3137
+ const count = this.ring.length;
3138
+ const ordered = [];
3139
+ for (let i = 0; i < count; i += 1) {
3140
+ const entry = this.ring[(this.head + i) % count];
3141
+ if (entry !== void 0) {
3142
+ ordered.push(entry);
3143
+ }
3144
+ }
3145
+ this.entriesCache = ordered;
3146
+ this.entriesRevision = this.revision;
3147
+ }
3148
+ return this.entriesCache;
3149
+ }
3150
+ /** A node's statistics, or `undefined` when it has done nothing this run. */
3151
+ statsOf(nodeId) {
3152
+ const stats = this.stats.get(nodeId);
3153
+ return stats === void 0 ? void 0 : { ...stats };
3154
+ }
3155
+ /** How many entries the log has let go of to stay within its capacity. */
3156
+ get dropped() {
3157
+ return this.droppedCount;
3158
+ }
3159
+ /** Changes once per notified frame; a snapshot for `useSyncExternalStore`. */
3160
+ getVersion() {
3161
+ return this.version;
3162
+ }
3163
+ subscribe(listener) {
3164
+ this.listeners.add(listener);
3165
+ return () => {
3166
+ this.listeners.delete(listener);
3167
+ };
3168
+ }
3169
+ append(entry) {
3170
+ if (this.ring.length < this.capacity) {
3171
+ this.ring.push(entry);
3172
+ return;
3173
+ }
3174
+ this.ring[this.head] = entry;
3175
+ this.head = (this.head + 1) % this.capacity;
3176
+ this.droppedCount += 1;
3177
+ }
3178
+ nextSeq() {
3179
+ this.seq += 1;
3180
+ return this.seq;
3181
+ }
3182
+ elapsed() {
3183
+ return this.deps.now() - this.startedAt;
3184
+ }
3185
+ statsFor(nodeId) {
3186
+ let stats = this.stats.get(nodeId);
3187
+ if (stats === void 0) {
3188
+ stats = {
3189
+ received: 0,
3190
+ sent: 0,
3191
+ errors: 0,
3192
+ totalMs: 0,
3193
+ maxMs: 0,
3194
+ lastReceived: void 0,
3195
+ lastSent: void 0,
3196
+ dropped: 0,
3197
+ finished: false
3198
+ };
3199
+ this.stats.set(nodeId, stats);
3200
+ }
3201
+ return stats;
3202
+ }
3203
+ /** Schedules one notification for the next frame, however many changes come before it. */
3204
+ changed() {
3205
+ this.revision += 1;
3206
+ if (this.frame !== null) {
3207
+ return;
3208
+ }
3209
+ this.frame = this.deps.schedule(() => {
3210
+ this.frame = null;
3211
+ this.version += 1;
3212
+ for (const listener of [...this.listeners]) {
3213
+ listener();
3214
+ }
3215
+ });
3216
+ }
3217
+ };
3218
+ function involves(entry, nodeId) {
3219
+ switch (entry.kind) {
3220
+ case "delivery":
3221
+ case "dropped":
3222
+ return entry.sourceNodeId === nodeId || entry.targetNodeId === nodeId;
3223
+ case "error":
3224
+ case "ended":
3225
+ case "finished":
3226
+ return entry.nodeId === nodeId;
3227
+ }
3228
+ }
3229
+
3230
+ // src/hooks/useRunTrace.ts
3231
+ import { useEffect as useEffect10 } from "react";
3232
+ function useRunTrace(trace, handle) {
3233
+ useEffect10(() => {
3234
+ var _a, _b, _c, _d;
3235
+ if (handle === void 0) {
3236
+ return;
3237
+ }
3238
+ trace.start();
3239
+ const deliveries = (_a = handle.onDelivery) == null ? void 0 : _a.call(handle, (event) => {
3240
+ trace.recordDelivery(event);
3241
+ });
3242
+ const errors = handle.onError((event) => {
3243
+ trace.recordError(event);
3244
+ });
3245
+ const ended = (_b = handle.onEnded) == null ? void 0 : _b.call(handle, (event) => {
3246
+ trace.recordEnded(event);
3247
+ });
3248
+ const finished = (_c = handle.onFinished) == null ? void 0 : _c.call(handle, (event) => {
3249
+ trace.recordFinished(event);
3250
+ });
3251
+ const dropped = (_d = handle.onDropped) == null ? void 0 : _d.call(handle, (event) => {
3252
+ trace.recordDropped(event);
3253
+ });
3254
+ return () => {
3255
+ deliveries == null ? void 0 : deliveries.dispose();
3256
+ errors.dispose();
3257
+ ended == null ? void 0 : ended.dispose();
3258
+ finished == null ? void 0 : finished.dispose();
3259
+ dropped == null ? void 0 : dropped.dispose();
3260
+ };
3261
+ }, [trace, handle]);
3262
+ }
3263
+
3264
+ // src/hooks/useFollowScroll.ts
3265
+ import { useCallback as useCallback13, useLayoutEffect, useRef as useRef10, useState as useState10 } from "react";
3266
+ var FOLLOW_SLACK_PX = 24;
3267
+ function useFollowScroll() {
3268
+ const ref = useRef10(null);
3269
+ const [follow, setFollow] = useState10(true);
3270
+ useLayoutEffect(() => {
3271
+ const element = ref.current;
3272
+ if (follow && element !== null) {
3273
+ element.scrollTop = element.scrollHeight;
3274
+ }
3275
+ });
3276
+ const onScroll = useCallback13((event) => {
3277
+ const element = event.currentTarget;
3278
+ setFollow(element.scrollHeight - element.scrollTop - element.clientHeight <= FOLLOW_SLACK_PX);
3279
+ }, []);
3280
+ return { ref, onScroll, follow, setFollow };
3281
+ }
3282
+
3283
+ // src/panels/RunLog.tsx
3284
+ import { useState as useState11 } from "react";
3285
+ import { useSelection as useSelection4 } from "@hlang-tech/editor-react";
3286
+ import { jsx as jsx37, jsxs as jsxs24 } from "react/jsx-runtime";
3287
+ function RunLog({ core, onSelectNode, title = "Run log", leading }) {
3288
+ const trace = useRunTraceStore();
3289
+ useRunTraceVersion(trace);
3290
+ const selection = useSelection4(core);
3291
+ const [onlySelected, setOnlySelected] = useState11(false);
3292
+ const { ref: bodyRef, onScroll, follow, setFollow } = useFollowScroll();
3293
+ const all = trace === null ? [] : trace.entries();
3294
+ const selectedId = selection.selectedNodes.length === 1 ? selection.selectedNodes[0] : void 0;
3295
+ const filterId = onlySelected ? selectedId : void 0;
3296
+ const entries = filterId === void 0 ? all : all.filter((entry) => involves(entry, filterId));
3297
+ if (trace === null) {
3298
+ return null;
3299
+ }
3300
+ const label = (nodeId) => {
3301
+ var _a, _b;
3302
+ return (_b = (_a = core.model.getNode(nodeId)) == null ? void 0 : _a.name) != null ? _b : nodeId;
3303
+ };
3304
+ const choose = (nodeId) => {
3305
+ core.selection.selectNodes([nodeId], "replace");
3306
+ onSelectNode == null ? void 0 : onSelectNode(nodeId);
3307
+ };
3308
+ return /* @__PURE__ */ jsxs24("section", { className: "hl-runlog", "aria-label": title, children: [
3309
+ /* @__PURE__ */ jsxs24(
3310
+ PanelHead,
3311
+ {
3312
+ leading: leading != null ? leading : /* @__PURE__ */ jsx37("span", { className: "hl-panel-title", children: title }),
3313
+ meta: (entries.length === all.length ? `${String(all.length)} events` : `${String(entries.length)} of ${String(all.length)} events`) + (trace.dropped > 0 ? ` \xB7 ${String(trace.dropped)} earlier not kept` : ""),
3314
+ children: [
3315
+ /* @__PURE__ */ jsx37(
3316
+ PanelAction,
3317
+ {
3318
+ icon: /* @__PURE__ */ jsx37(FilterIcon, {}),
3319
+ label: onlySelected && selectedId !== void 0 ? `Only ${label(selectedId)}` : "Selected node",
3320
+ title: selectedId === void 0 ? "Select a node to filter by it" : `Only ${label(selectedId)}`,
3321
+ pressed: onlySelected,
3322
+ disabled: selectedId === void 0 && !onlySelected,
3323
+ onClick: () => {
3324
+ setOnlySelected((value) => !value);
3325
+ }
3326
+ }
3327
+ ),
3328
+ /* @__PURE__ */ jsx37(
3329
+ PanelAction,
3330
+ {
3331
+ icon: /* @__PURE__ */ jsx37(FollowIcon, {}),
3332
+ label: "Follow",
3333
+ title: "Follow the newest event",
3334
+ pressed: follow,
3335
+ onClick: () => {
3336
+ setFollow(!follow);
3337
+ }
3338
+ }
3339
+ ),
3340
+ /* @__PURE__ */ jsx37(
3341
+ PanelAction,
3342
+ {
3343
+ icon: /* @__PURE__ */ jsx37(ClearIcon, {}),
3344
+ label: "Clear",
3345
+ title: "Clear the log",
3346
+ disabled: all.length === 0,
3347
+ onClick: () => {
3348
+ trace.clear();
3349
+ }
3350
+ }
3351
+ )
3352
+ ]
3353
+ }
3354
+ ),
3355
+ entries.length === 0 ? /* @__PURE__ */ jsx37("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__ */ jsx37("ol", { className: "hl-runlog-body hl-mono", ref: bodyRef, onScroll, children: entries.map((entry) => /* @__PURE__ */ jsx37(Row, { entry, label, onChoose: choose }, entry.seq)) })
3356
+ ] });
3357
+ }
3358
+ function Row({
3359
+ entry,
3360
+ label,
3361
+ onChoose
3362
+ }) {
3363
+ switch (entry.kind) {
3364
+ case "error":
3365
+ return /* @__PURE__ */ jsxs24(Event, { className: "is-error", at: entry.at, onChoose: () => onChoose(entry.nodeId), ms: "", children: [
3366
+ /* @__PURE__ */ jsx37(NodePort, { node: label(entry.nodeId), port: "error" }),
3367
+ /* @__PURE__ */ jsx37("span", { className: "hl-runlog-value", children: previewValue(entry.error) })
3368
+ ] });
3369
+ case "ended":
3370
+ return /* @__PURE__ */ jsxs24(Event, { className: "is-end", at: entry.at, onChoose: () => onChoose(entry.nodeId), ms: "", children: [
3371
+ /* @__PURE__ */ jsx37(NodePort, { node: label(entry.nodeId), port: entry.portId }),
3372
+ /* @__PURE__ */ jsx37("span", { className: "hl-runlog-value", children: "ended" })
3373
+ ] });
3374
+ case "finished":
3375
+ return /* @__PURE__ */ jsxs24(Event, { className: "is-end", at: entry.at, onChoose: () => onChoose(entry.nodeId), ms: "", children: [
3376
+ /* @__PURE__ */ jsx37(NodePort, { node: label(entry.nodeId), port: "" }),
3377
+ /* @__PURE__ */ jsx37("span", { className: "hl-runlog-value", children: "finished" })
3378
+ ] });
3379
+ case "dropped":
3380
+ return /* @__PURE__ */ jsxs24(Event, { className: "is-dropped", at: entry.at, onChoose: () => onChoose(entry.targetNodeId), ms: "", children: [
3381
+ /* @__PURE__ */ jsx37(Route, { entry, label }),
3382
+ /* @__PURE__ */ jsxs24("span", { className: "hl-runlog-value", children: [
3383
+ "dropped ",
3384
+ previewValue(entry.message)
3385
+ ] })
3386
+ ] });
3387
+ case "delivery":
3388
+ return /* @__PURE__ */ jsxs24(Event, { className: "", at: entry.at, onChoose: () => onChoose(entry.targetNodeId), ms: formatDuration(entry.duration), children: [
3389
+ /* @__PURE__ */ jsx37(Route, { entry, label }),
3390
+ /* @__PURE__ */ jsx37("span", { className: "hl-runlog-value", children: previewValue(entry.message) })
3391
+ ] });
3392
+ }
3393
+ }
3394
+ function Event({
3395
+ className: className2,
3396
+ at,
3397
+ ms,
3398
+ onChoose,
3399
+ children
3400
+ }) {
3401
+ return /* @__PURE__ */ jsx37("li", { className: `hl-runlog-row ${className2}`.trim(), children: /* @__PURE__ */ jsxs24("button", { type: "button", className: "hl-runlog-hit", onClick: onChoose, children: [
3402
+ /* @__PURE__ */ jsx37("span", { className: "hl-runlog-at", children: formatElapsed(at) }),
3403
+ children,
3404
+ /* @__PURE__ */ jsx37("span", { className: "hl-runlog-ms", children: ms })
3405
+ ] }) });
3406
+ }
3407
+ function NodePort({ node, port }) {
3408
+ return /* @__PURE__ */ jsxs24("span", { className: "hl-runlog-route", children: [
3409
+ /* @__PURE__ */ jsx37("span", { className: "hl-runlog-node", children: node }),
3410
+ port === "" ? null : /* @__PURE__ */ jsx37("span", { className: "hl-runlog-port", children: port })
3411
+ ] });
3412
+ }
3413
+ function Route({
3414
+ entry,
3415
+ label
3416
+ }) {
3417
+ return /* @__PURE__ */ jsxs24("span", { className: "hl-runlog-route", children: [
3418
+ /* @__PURE__ */ jsx37("span", { className: "hl-runlog-node", children: label(entry.sourceNodeId) }),
3419
+ /* @__PURE__ */ jsx37("span", { className: "hl-runlog-port", children: entry.sourcePortId }),
3420
+ /* @__PURE__ */ jsx37("span", { className: "hl-runlog-arrow", "aria-label": "to", children: "\u2192" }),
3421
+ /* @__PURE__ */ jsx37("span", { className: "hl-runlog-node", children: label(entry.targetNodeId) }),
3422
+ /* @__PURE__ */ jsx37("span", { className: "hl-runlog-port", children: entry.targetPortId })
3423
+ ] });
3424
+ }
2282
3425
  export {
2283
3426
  AssetGrid,
2284
3427
  AssetNodeChrome,
2285
3428
  CanvasChrome,
2286
3429
  CanvasDock,
2287
3430
  Cheatsheet,
3431
+ ClearIcon,
3432
+ ConnectionInspector,
3433
+ DEFAULT_EDGE_CAPACITY,
3434
+ DEFAULT_MIN_CANVAS_HEIGHT,
2288
3435
  DiagnosticBadge,
2289
3436
  EditorShell,
2290
3437
  EditorUIThemeProvider,
2291
3438
  EmptyState,
3439
+ FOLLOW_SLACK_PX,
3440
+ FOOTER_KEY_STEP,
3441
+ FilterIcon,
2292
3442
  FlowEdge,
3443
+ FollowIcon,
3444
+ FooterResizer,
2293
3445
  HlangNode,
2294
3446
  InspectorPanel,
2295
3447
  KIND_ROLE,
2296
3448
  KIND_TOKEN,
3449
+ LivePulse,
2297
3450
  LiveSignalProvider,
2298
3451
  LiveSignalStore,
2299
3452
  NodeBodySlot,
2300
3453
  NodeChrome,
2301
3454
  NodeContextMenu,
2302
3455
  NodeInteractionProvider,
3456
+ NodeRunStats,
2303
3457
  NodeSearch,
2304
3458
  OutlineTree,
3459
+ PREVIEW_LENGTH,
3460
+ PanelAction,
3461
+ PanelHead,
2305
3462
  ParamForm,
2306
3463
  PillNodeChrome,
2307
3464
  PortRow,
2308
3465
  PositionPanel,
2309
3466
  PreviewPanel,
3467
+ RUN_LOG_CAPACITY,
2310
3468
  RailTabs,
3469
+ ReadOnlyBadge,
2311
3470
  RunButton,
3471
+ RunLog,
3472
+ RunTrace,
3473
+ RunTraceProvider,
3474
+ SELF_LOOP_CLEARANCE,
3475
+ SELF_LOOP_RADIUS,
2312
3476
  SignalBar,
2313
3477
  TOKEN_NAMES,
2314
3478
  ToastProvider,
@@ -2317,26 +3481,47 @@ export {
2317
3481
  ToolbarGhostButton,
2318
3482
  ToolbarStat,
2319
3483
  UndoRedo,
3484
+ boundsFields,
3485
+ capacityLabel,
3486
+ clampFooter,
2320
3487
  connectableCandidates,
2321
3488
  createHlangFlowTypes,
2322
3489
  createTheme,
3490
+ defaultLiveDeps,
2323
3491
  deriveParamSchema,
2324
3492
  descriptorCandidates,
3493
+ footerBounds,
3494
+ formatDuration,
3495
+ formatElapsed,
2325
3496
  fuzzyRank,
2326
3497
  fuzzyScore,
3498
+ involves,
2327
3499
  isEditableTarget,
3500
+ nextBounds,
3501
+ previewValue,
3502
+ resizeByKey,
2328
3503
  searchNodes,
3504
+ selfLoopPath,
3505
+ toggleCollapsed,
2329
3506
  tokenVar,
3507
+ useEdgeBounds,
2330
3508
  useEdgeFlowing,
3509
+ useEdgePulse,
2331
3510
  useEditorHotkeys,
3511
+ useFollowScroll,
3512
+ useFooterResize,
2332
3513
  useLiveSignalStore,
2333
3514
  useLiveSignals,
2334
3515
  useNodeContextMenu,
2335
3516
  useNodeInteraction,
2336
3517
  useNodeLiveState,
2337
3518
  useNodeParams,
3519
+ useNodePulse,
2338
3520
  useNodeSearch,
2339
3521
  useNodeSearchTriggers,
3522
+ useRunTrace,
3523
+ useRunTraceStore,
3524
+ useRunTraceVersion,
2340
3525
  useToast
2341
3526
  };
2342
3527
  //# sourceMappingURL=index.mjs.map