@hlang-tech/editor-ui 0.0.0-stage → 0.1.1

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 ADDED
@@ -0,0 +1,2318 @@
1
+ // src/tokens/tokens.ts
2
+ var TOKEN_NAMES = [
3
+ "hl-void",
4
+ "hl-void-2",
5
+ "hl-surface",
6
+ "hl-surface-2",
7
+ "hl-line",
8
+ "hl-line-strong",
9
+ "hl-silver",
10
+ "hl-silver-hi",
11
+ "hl-silver-lo",
12
+ "hl-spark",
13
+ "hl-r",
14
+ "hl-t",
15
+ "hl-w",
16
+ "hl-error",
17
+ "hl-ink",
18
+ "hl-ink-2",
19
+ "hl-ink-3",
20
+ "hl-font-ui",
21
+ "hl-font-mono",
22
+ "hl-r-sm",
23
+ "hl-r-md",
24
+ "hl-r-lg",
25
+ "hl-r-pill",
26
+ "hl-shadow-node",
27
+ "hl-shadow-glass",
28
+ "hl-glow-silver",
29
+ "hl-ease"
30
+ ];
31
+ function tokenVar(name) {
32
+ return `var(--${name})`;
33
+ }
34
+ var KIND_TOKEN = {
35
+ R: "var(--hl-r)",
36
+ T: "var(--hl-t)",
37
+ W: "var(--hl-w)"
38
+ };
39
+ var KIND_ROLE = {
40
+ R: "source",
41
+ T: "transform",
42
+ W: "sink"
43
+ };
44
+
45
+ // src/tokens/theme.ts
46
+ function createTheme(overrides) {
47
+ const style = {};
48
+ for (const [name, value] of Object.entries(overrides)) {
49
+ if (value !== void 0) {
50
+ style[`--${name}`] = value;
51
+ }
52
+ }
53
+ return style;
54
+ }
55
+
56
+ // src/tokens/ThemeProvider.tsx
57
+ import { jsx } from "react/jsx-runtime";
58
+ function EditorUIThemeProvider({
59
+ theme,
60
+ className,
61
+ style,
62
+ children
63
+ }) {
64
+ const themeStyle = theme ? createTheme(theme) : void 0;
65
+ return /* @__PURE__ */ jsx("div", { className, style: { ...themeStyle, ...style }, children });
66
+ }
67
+
68
+ // src/nodes/SignalBar.tsx
69
+ import { jsx as jsx2 } from "react/jsx-runtime";
70
+ function SignalBar() {
71
+ return /* @__PURE__ */ jsx2("div", { className: "hl-signal", "aria-hidden": "true" });
72
+ }
73
+
74
+ // src/nodes/PortRow.tsx
75
+ import { Handle, Position } from "@xyflow/react";
76
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
77
+ function PortRow({ port }) {
78
+ const isInput = port.direction === "I";
79
+ const side = isInput ? "target" : "source";
80
+ const isLive = port.stream !== "value";
81
+ const rowClass = `hl-port${port.isError ? " is-error" : ""}`;
82
+ const handleClass = `hl-handle${isLive ? " is-live" : ""}`;
83
+ return /* @__PURE__ */ jsxs("li", { className: rowClass, children: [
84
+ /* @__PURE__ */ jsx3(
85
+ Handle,
86
+ {
87
+ type: side,
88
+ position: isInput ? Position.Left : Position.Right,
89
+ id: port.id,
90
+ className: handleClass,
91
+ isConnectable: true
92
+ }
93
+ ),
94
+ /* @__PURE__ */ jsx3("span", { className: "hl-port-label", children: port.isError ? "error" : port.name })
95
+ ] });
96
+ }
97
+
98
+ // src/nodes/NodeBodySlot.tsx
99
+ import { jsx as jsx4 } from "react/jsx-runtime";
100
+ function NodeBodySlot({ children }) {
101
+ if (children === void 0 || children === null || children === false) {
102
+ return null;
103
+ }
104
+ return /* @__PURE__ */ jsx4("div", { className: "hl-body", children });
105
+ }
106
+
107
+ // src/live/LiveSignalContext.tsx
108
+ import { createContext, useContext, useMemo, useSyncExternalStore } from "react";
109
+
110
+ // src/live/live-signal-store.ts
111
+ var ACTIVE_WINDOW_MS = 700;
112
+ function portKey(nodeId, portId) {
113
+ return `${nodeId}@${portId}`;
114
+ }
115
+ var defaultDeps = {
116
+ now: () => typeof performance !== "undefined" ? performance.now() : Date.now(),
117
+ schedule: (cb) => typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame(cb) : (
118
+ // `window.setTimeout` returns a numeric handle (the DOM overload),
119
+ // unlike the Node overload's `Timeout` object — this store deals in
120
+ // numeric handles because it runs in the browser.
121
+ window.setTimeout(cb, 16)
122
+ ),
123
+ cancel: (h) => typeof cancelAnimationFrame !== "undefined" ? cancelAnimationFrame(h) : window.clearTimeout(h)
124
+ };
125
+ var LiveSignalStore = class {
126
+ constructor(deps = defaultDeps) {
127
+ /** nodeId -> last-active timestamp. */
128
+ this.nodeActiveAt = /* @__PURE__ */ new Map();
129
+ /** nodeId set that errored during this run. */
130
+ this.erroredNodes = /* @__PURE__ */ new Set();
131
+ /** edgeId -> last-flowing timestamp. */
132
+ this.edgeFlowAt = /* @__PURE__ */ new Map();
133
+ /** Per-id listeners. */
134
+ this.nodeListeners = /* @__PURE__ */ new Map();
135
+ this.edgeListeners = /* @__PURE__ */ new Map();
136
+ /** Snapshot of last-emitted state per id, so we only notify on real change. */
137
+ this.lastNodeState = /* @__PURE__ */ new Map();
138
+ this.lastEdgeFlowing = /* @__PURE__ */ new Map();
139
+ /** Edges sourced by a given (nodeId@portId), for pulse -> edge mapping. */
140
+ this.edgesBySourcePort = /* @__PURE__ */ new Map();
141
+ this.rafHandle = null;
142
+ this.running = false;
143
+ this.deps = deps;
144
+ }
145
+ /** Provide the current edge index (call whenever edges change). */
146
+ setEdgeIndex(edges) {
147
+ const next = /* @__PURE__ */ new Map();
148
+ for (const edge of edges) {
149
+ const key = portKey(edge.sourceNodeId, edge.sourcePortId);
150
+ const list = next.get(key);
151
+ if (list === void 0) {
152
+ next.set(key, [edge]);
153
+ } else {
154
+ list.push(edge);
155
+ }
156
+ }
157
+ this.edgesBySourcePort = next;
158
+ }
159
+ /** Record a node emitting on an output port (drives node + edge activity). */
160
+ markActivity(nodeId, portId) {
161
+ const t = this.deps.now();
162
+ this.nodeActiveAt.set(nodeId, t);
163
+ const edges = this.edgesBySourcePort.get(portKey(nodeId, portId));
164
+ if (edges !== void 0) {
165
+ for (const edge of edges) {
166
+ this.edgeFlowAt.set(edge.id, t);
167
+ }
168
+ }
169
+ this.ensureLoop();
170
+ }
171
+ /** Record a node erroring (sticky for the run). */
172
+ markError(nodeId) {
173
+ this.erroredNodes.add(nodeId);
174
+ this.ensureLoop();
175
+ }
176
+ /** Clear all live state (on stop/teardown) and notify everyone. */
177
+ reset() {
178
+ this.nodeActiveAt.clear();
179
+ this.erroredNodes.clear();
180
+ this.edgeFlowAt.clear();
181
+ this.stopLoop();
182
+ this.flushNotifications();
183
+ }
184
+ /** Subscribe to a single node's live state. */
185
+ subscribeNode(nodeId, listener) {
186
+ return this.subscribe(this.nodeListeners, nodeId, listener);
187
+ }
188
+ /** Subscribe to a single edge's flowing state. */
189
+ subscribeEdge(edgeId, listener) {
190
+ return this.subscribe(this.edgeListeners, edgeId, listener);
191
+ }
192
+ /** Current live state of a node. */
193
+ getNodeState(nodeId) {
194
+ if (this.erroredNodes.has(nodeId)) {
195
+ return "error";
196
+ }
197
+ const at = this.nodeActiveAt.get(nodeId);
198
+ if (at !== void 0 && this.deps.now() - at <= ACTIVE_WINDOW_MS) {
199
+ return "active";
200
+ }
201
+ return "idle";
202
+ }
203
+ /** Whether an edge is currently flowing. */
204
+ isEdgeFlowing(edgeId) {
205
+ const at = this.edgeFlowAt.get(edgeId);
206
+ return at !== void 0 && this.deps.now() - at <= ACTIVE_WINDOW_MS;
207
+ }
208
+ subscribe(registry, id, listener) {
209
+ var _a;
210
+ const set = (_a = registry.get(id)) != null ? _a : /* @__PURE__ */ new Set();
211
+ set.add(listener);
212
+ registry.set(id, set);
213
+ return () => {
214
+ set.delete(listener);
215
+ if (set.size === 0) {
216
+ registry.delete(id);
217
+ }
218
+ };
219
+ }
220
+ ensureLoop() {
221
+ if (this.running) {
222
+ this.flushNotifications();
223
+ return;
224
+ }
225
+ this.running = true;
226
+ const step = () => {
227
+ const stillLive = this.flushNotifications();
228
+ if (stillLive) {
229
+ this.rafHandle = this.deps.schedule(step);
230
+ } else {
231
+ this.running = false;
232
+ this.rafHandle = null;
233
+ }
234
+ };
235
+ this.rafHandle = this.deps.schedule(step);
236
+ }
237
+ stopLoop() {
238
+ if (this.rafHandle !== null) {
239
+ this.deps.cancel(this.rafHandle);
240
+ this.rafHandle = null;
241
+ }
242
+ this.running = false;
243
+ }
244
+ /**
245
+ * Recompute every subscribed id's state, notify only those that changed, and
246
+ * report whether anything is still within its active window (so the loop can
247
+ * stop when the graph goes quiet).
248
+ */
249
+ flushNotifications() {
250
+ let anyLive = false;
251
+ for (const [nodeId, listeners] of this.nodeListeners) {
252
+ const state = this.getNodeState(nodeId);
253
+ if (state !== "idle") {
254
+ anyLive = anyLive || state === "active";
255
+ }
256
+ if (this.lastNodeState.get(nodeId) !== state) {
257
+ this.lastNodeState.set(nodeId, state);
258
+ for (const listener of listeners) {
259
+ listener();
260
+ }
261
+ }
262
+ }
263
+ for (const [edgeId, listeners] of this.edgeListeners) {
264
+ const flowing = this.isEdgeFlowing(edgeId);
265
+ anyLive = anyLive || flowing;
266
+ if (this.lastEdgeFlowing.get(edgeId) !== flowing) {
267
+ this.lastEdgeFlowing.set(edgeId, flowing);
268
+ for (const listener of listeners) {
269
+ listener();
270
+ }
271
+ }
272
+ }
273
+ return anyLive;
274
+ }
275
+ };
276
+
277
+ // src/live/LiveSignalContext.tsx
278
+ import { jsx as jsx5 } from "react/jsx-runtime";
279
+ var LiveSignalContext = createContext(null);
280
+ function LiveSignalProvider({ store, children }) {
281
+ const value = useMemo(() => store != null ? store : new LiveSignalStore(), [store]);
282
+ return /* @__PURE__ */ jsx5(LiveSignalContext.Provider, { value, children });
283
+ }
284
+ function useLiveSignalStore() {
285
+ return useContext(LiveSignalContext);
286
+ }
287
+ function useNodeLiveState(nodeId) {
288
+ const store = useContext(LiveSignalContext);
289
+ return useSyncExternalStore(
290
+ (onChange) => store === null ? () => void 0 : store.subscribeNode(nodeId, onChange),
291
+ () => store === null ? "idle" : store.getNodeState(nodeId),
292
+ () => "idle"
293
+ );
294
+ }
295
+ function useEdgeFlowing(edgeId) {
296
+ const store = useContext(LiveSignalContext);
297
+ return useSyncExternalStore(
298
+ (onChange) => store === null ? () => void 0 : store.subscribeEdge(edgeId, onChange),
299
+ () => store === null ? false : store.isEdgeFlowing(edgeId),
300
+ () => false
301
+ );
302
+ }
303
+
304
+ // src/nodes/AssetNode.tsx
305
+ import { jsx as jsx6, jsxs as jsxs2 } from "react/jsx-runtime";
306
+ function stringParam(node, key) {
307
+ const v = node.params[key];
308
+ return typeof v === "string" ? v : void 0;
309
+ }
310
+ function AssetNodeChrome({ node, selected }) {
311
+ var _a, _b;
312
+ const imageUrl = (_a = stringParam(node, "imageUrl")) != null ? _a : "";
313
+ const caption = (_b = stringParam(node, "caption")) != null ? _b : node.name;
314
+ const role = stringParam(node, "role");
315
+ const live = useNodeLiveState(node.id);
316
+ const outputs = node.ports.filter((p) => p.direction === "O" && !p.isError);
317
+ const className = [
318
+ "hl-asset",
319
+ selected ? "is-selected" : "",
320
+ live === "active" ? "is-active" : "",
321
+ live === "error" ? "is-live-error" : ""
322
+ ].filter(Boolean).join(" ");
323
+ return /* @__PURE__ */ jsxs2("div", { className, "data-kind": node.kind, children: [
324
+ /* @__PURE__ */ jsxs2("div", { className: "hl-asset-frame", children: [
325
+ imageUrl.length > 0 ? /* @__PURE__ */ jsx6("img", { className: "hl-asset-img", src: imageUrl, alt: caption, draggable: false }) : /* @__PURE__ */ jsx6("div", { className: "hl-asset-placeholder", "aria-hidden": true }),
326
+ /* @__PURE__ */ jsx6("div", { className: "hl-asset-scrim" }),
327
+ /* @__PURE__ */ jsxs2("div", { className: "hl-asset-caption", children: [
328
+ /* @__PURE__ */ jsx6("span", { className: "hl-asset-name", children: caption }),
329
+ role !== void 0 && /* @__PURE__ */ jsx6("span", { className: "hl-asset-role hl-mono", children: role })
330
+ ] })
331
+ ] }),
332
+ /* @__PURE__ */ jsx6("ul", { className: "hl-asset-ports", children: outputs.map((port) => /* @__PURE__ */ jsx6(PortRow, { port }, port.id)) })
333
+ ] });
334
+ }
335
+
336
+ // src/nodes/PillNode.tsx
337
+ import { useCallback, useEffect, useRef, useState } from "react";
338
+
339
+ // src/icons/node-icons.tsx
340
+ import { jsx as jsx7 } from "react/jsx-runtime";
341
+ function svg(size, children) {
342
+ return /* @__PURE__ */ jsx7(
343
+ "svg",
344
+ {
345
+ width: size,
346
+ height: size,
347
+ viewBox: "0 0 24 24",
348
+ fill: "none",
349
+ stroke: "currentColor",
350
+ strokeWidth: 1.6,
351
+ strokeLinecap: "round",
352
+ strokeLinejoin: "round",
353
+ "aria-hidden": true,
354
+ children
355
+ }
356
+ );
357
+ }
358
+ var BY_CODE = {
359
+ "image-source": (s) => svg(s, [
360
+ /* @__PURE__ */ jsx7("rect", { x: "3", y: "4", width: "18", height: "16", rx: "2.5" }, "r"),
361
+ /* @__PURE__ */ jsx7("circle", { cx: "8.5", cy: "9", r: "1.6" }, "c"),
362
+ /* @__PURE__ */ jsx7("path", { d: "M4 17l5-5 4 4 3-2 4 4" }, "p")
363
+ ]),
364
+ "text-source": (s) => svg(s, [/* @__PURE__ */ jsx7("path", { d: "M5 6h14M12 6v12M9 18h6" }, "p")]),
365
+ "interval-source": (s) => svg(s, [
366
+ /* @__PURE__ */ jsx7("circle", { cx: "12", cy: "12", r: "8" }, "c"),
367
+ /* @__PURE__ */ jsx7("path", { d: "M12 8v4l3 2" }, "h")
368
+ ]),
369
+ "range-source": (s) => svg(s, [
370
+ /* @__PURE__ */ jsx7("path", { d: "M4 12h16" }, "l"),
371
+ /* @__PURE__ */ jsx7("circle", { cx: "6", cy: "12", r: "2" }, "a"),
372
+ /* @__PURE__ */ jsx7("circle", { cx: "18", cy: "12", r: "2" }, "b")
373
+ ]),
374
+ "constant-source": (s) => svg(s, [/* @__PURE__ */ jsx7("path", { d: "M6 12h12M9 8l-3 4 3 4M15 8l3 4-3 4" }, "p")]),
375
+ "map-scale": (s) => svg(s, [
376
+ /* @__PURE__ */ jsx7("path", { d: "M4 20V8l8-4" }, "a"),
377
+ /* @__PURE__ */ jsx7("path", { d: "M4 8l8 4 8-4M12 12v8" }, "b")
378
+ ]),
379
+ uppercase: (s) => svg(s, [/* @__PURE__ */ jsx7("path", { d: "M5 18L9 6l4 12M6.5 14h5M15 10v8M15 10c3 0 3 4 0 4" }, "p")]),
380
+ "filter-threshold": (s) => svg(s, [/* @__PURE__ */ jsx7("path", { d: "M4 5h16l-6 7v6l-4-2v-4z" }, "p")]),
381
+ "scan-sum": (s) => svg(s, [/* @__PURE__ */ jsx7("path", { d: "M17 6H7l6 6-6 6h10" }, "p")]),
382
+ delay: (s) => svg(s, [
383
+ /* @__PURE__ */ jsx7("circle", { cx: "12", cy: "13", r: "7" }, "c"),
384
+ /* @__PURE__ */ jsx7("path", { d: "M12 10v3l2 2M9 3h6" }, "h")
385
+ ]),
386
+ merge: (s) => svg(s, [/* @__PURE__ */ jsx7("path", { d: "M6 5v4c0 3 3 3 6 3s6 0 6-3V5M12 12v7" }, "p")]),
387
+ "console-sink": (s) => svg(s, [
388
+ /* @__PURE__ */ jsx7("rect", { x: "4", y: "5", width: "16", height: "12", rx: "2" }, "r"),
389
+ /* @__PURE__ */ jsx7("path", { d: "M8 10l2.5 2L8 14M13 14h3" }, "p")
390
+ ]),
391
+ "collect-sink": (s) => svg(s, [
392
+ /* @__PURE__ */ jsx7("path", { d: "M4 7h16M4 12h16M4 17h10" }, "p")
393
+ ])
394
+ };
395
+ var BY_KIND = {
396
+ R: (s) => svg(s, [/* @__PURE__ */ jsx7("path", { d: "M8 5l10 7-10 7z" }, "p")]),
397
+ // source ▷ (play)
398
+ T: (s) => svg(s, [/* @__PURE__ */ jsx7("path", { d: "M4 8h6l4 8h6M14 8h6M4 16h6" }, "p")]),
399
+ // transform
400
+ W: (s) => svg(s, [
401
+ /* @__PURE__ */ jsx7("path", { d: "M5 4h11l3 3v13H5z" }, "p"),
402
+ /* @__PURE__ */ jsx7("path", { d: "M9 4v5h6" }, "q")
403
+ ])
404
+ // sink (save)
405
+ };
406
+ function NodeIcon({ code, kind, size = 15 }) {
407
+ var _a;
408
+ const build = (_a = BY_CODE[code]) != null ? _a : BY_KIND[kind];
409
+ return build(size);
410
+ }
411
+ function EyeIcon({ size = 14 }) {
412
+ return svg(size, [
413
+ /* @__PURE__ */ jsx7("path", { d: "M2 12s3.5-6.5 10-6.5S22 12 22 12s-3.5 6.5-10 6.5S2 12 2 12z" }, "p"),
414
+ /* @__PURE__ */ jsx7("circle", { cx: "12", cy: "12", r: "2.6" }, "c")
415
+ ]);
416
+ }
417
+ function EyeOffIcon({ size = 14 }) {
418
+ return svg(size, [
419
+ /* @__PURE__ */ jsx7("path", { d: "M2 12s3.5-6.5 10-6.5c1.7 0 3.2.45 4.5 1.1M22 12s-3.5 6.5-10 6.5c-1.7 0-3.2-.45-4.5-1.1" }, "p"),
420
+ /* @__PURE__ */ jsx7("path", { d: "M9.9 9.9a2.6 2.6 0 003.7 3.7" }, "e"),
421
+ /* @__PURE__ */ jsx7("path", { d: "M3 3l18 18" }, "s")
422
+ ]);
423
+ }
424
+ function UnlockIcon({ size = 14 }) {
425
+ return svg(size, [
426
+ /* @__PURE__ */ jsx7("rect", { x: "5", y: "11", width: "14", height: "9", rx: "2" }, "r"),
427
+ /* @__PURE__ */ jsx7("path", { d: "M8 11V7a4 4 0 017.9-1" }, "s")
428
+ ]);
429
+ }
430
+ function LockIcon({ size = 14 }) {
431
+ return svg(size, [
432
+ /* @__PURE__ */ jsx7("rect", { x: "5", y: "11", width: "14", height: "9", rx: "2" }, "r"),
433
+ /* @__PURE__ */ jsx7("path", { d: "M8 11V7a4 4 0 018 0v4" }, "s")
434
+ ]);
435
+ }
436
+ function WarningIcon({ size = 13 }) {
437
+ return svg(size, [
438
+ /* @__PURE__ */ jsx7("path", { d: "M12 4l9 15H3z" }, "t"),
439
+ /* @__PURE__ */ jsx7("path", { d: "M12 10v4" }, "b"),
440
+ /* @__PURE__ */ jsx7("path", { d: "M12 17v.5" }, "d")
441
+ ]);
442
+ }
443
+ function ErrorIcon({ size = 13 }) {
444
+ return svg(size, [
445
+ /* @__PURE__ */ jsx7("circle", { cx: "12", cy: "12", r: "9" }, "c"),
446
+ /* @__PURE__ */ jsx7("path", { d: "M12 7v6" }, "b"),
447
+ /* @__PURE__ */ jsx7("path", { d: "M12 16v.5" }, "d")
448
+ ]);
449
+ }
450
+
451
+ // src/panels/DiagnosticBadge.tsx
452
+ import { jsx as jsx8 } from "react/jsx-runtime";
453
+ function DiagnosticBadge({ diagnostic, variant, size = 12 }) {
454
+ return /* @__PURE__ */ jsx8(
455
+ "span",
456
+ {
457
+ className: `${variant} hl-diag-${diagnostic.severity}`,
458
+ title: diagnostic.message,
459
+ "aria-label": `${diagnostic.severity}: ${diagnostic.message}`,
460
+ children: diagnostic.severity === "error" ? /* @__PURE__ */ jsx8(ErrorIcon, { size }) : /* @__PURE__ */ jsx8(WarningIcon, { size })
461
+ }
462
+ );
463
+ }
464
+
465
+ // src/nodes/NodeInteractionContext.tsx
466
+ import { createContext as createContext2, useContext as useContext2 } from "react";
467
+ import { jsx as jsx9 } from "react/jsx-runtime";
468
+ var NoopInteraction = {
469
+ diagnosticFor: () => void 0,
470
+ rename: () => void 0
471
+ };
472
+ var NodeInteractionCtx = createContext2(NoopInteraction);
473
+ function NodeInteractionProvider({
474
+ value,
475
+ children
476
+ }) {
477
+ return /* @__PURE__ */ jsx9(NodeInteractionCtx.Provider, { value, children });
478
+ }
479
+ function useNodeInteraction() {
480
+ return useContext2(NodeInteractionCtx);
481
+ }
482
+
483
+ // src/nodes/PillNode.tsx
484
+ import { jsx as jsx10, jsxs as jsxs3 } from "react/jsx-runtime";
485
+ var DOUBLE_CLICK_MS = 320;
486
+ function PillNodeChrome({ node, selected }) {
487
+ const live = useNodeLiveState(node.id);
488
+ const interaction = useNodeInteraction();
489
+ const diagnostic = interaction.diagnosticFor(node.id);
490
+ const [editing, setEditing] = useState(false);
491
+ const [draft, setDraft] = useState(node.name);
492
+ const inputRef = useRef(null);
493
+ useEffect(() => {
494
+ if (editing) {
495
+ setDraft(node.name);
496
+ requestAnimationFrame(() => {
497
+ var _a;
498
+ return (_a = inputRef.current) == null ? void 0 : _a.select();
499
+ });
500
+ }
501
+ }, [editing, node.name]);
502
+ const commit = useCallback(() => {
503
+ setEditing(false);
504
+ interaction.rename(node.id, draft);
505
+ }, [interaction, node.id, draft]);
506
+ const onKeyDown = useCallback(
507
+ (e) => {
508
+ if (e.key === "Enter") {
509
+ e.preventDefault();
510
+ commit();
511
+ } else if (e.key === "Escape") {
512
+ e.preventDefault();
513
+ setEditing(false);
514
+ }
515
+ },
516
+ [commit]
517
+ );
518
+ const lastClick = useRef(0);
519
+ const onNameClick = useCallback(() => {
520
+ const now = performance.now();
521
+ if (now - lastClick.current < DOUBLE_CLICK_MS) {
522
+ setEditing(true);
523
+ lastClick.current = 0;
524
+ } else {
525
+ lastClick.current = now;
526
+ }
527
+ }, []);
528
+ const inputs = node.ports.filter((p) => p.direction === "I" && !p.isError);
529
+ const outputs = node.ports.filter((p) => p.direction === "O" && !p.isError);
530
+ const className = [
531
+ "hl-pill",
532
+ `hl-pill-${node.kind.toLowerCase()}`,
533
+ selected ? "is-selected" : "",
534
+ live === "active" ? "is-active" : "",
535
+ live === "error" ? "is-live-error" : "",
536
+ (diagnostic == null ? void 0 : diagnostic.severity) === "error" ? "has-diag-error" : "",
537
+ (diagnostic == null ? void 0 : diagnostic.severity) === "warning" ? "has-diag-warning" : ""
538
+ ].filter(Boolean).join(" ");
539
+ return /* @__PURE__ */ jsxs3("div", { className, "data-kind": node.kind, children: [
540
+ /* @__PURE__ */ jsx10("span", { className: "hl-pill-dot", "aria-hidden": true, children: /* @__PURE__ */ jsx10(NodeIcon, { code: node.code, kind: node.kind, size: 13 }) }),
541
+ editing ? /* @__PURE__ */ jsx10(
542
+ "input",
543
+ {
544
+ ref: inputRef,
545
+ className: "hl-pill-rename nodrag",
546
+ value: draft,
547
+ onChange: (e) => setDraft(e.target.value),
548
+ onKeyDown,
549
+ onBlur: commit,
550
+ spellCheck: false,
551
+ "aria-label": "Rename node"
552
+ }
553
+ ) : /* @__PURE__ */ jsx10(
554
+ "span",
555
+ {
556
+ className: "hl-pill-name nodrag",
557
+ onClick: (e) => {
558
+ e.stopPropagation();
559
+ onNameClick();
560
+ },
561
+ title: "Double-click to rename",
562
+ children: node.name
563
+ }
564
+ ),
565
+ diagnostic !== void 0 && /* @__PURE__ */ jsx10(DiagnosticBadge, { diagnostic, variant: "hl-pill-diag", size: 12 }),
566
+ /* @__PURE__ */ jsx10("span", { className: "hl-pill-status", "aria-hidden": true }),
567
+ /* @__PURE__ */ jsx10("ul", { className: "hl-pill-ports hl-in", children: inputs.map((port) => /* @__PURE__ */ jsx10(PortRow, { port }, port.id)) }),
568
+ /* @__PURE__ */ jsx10("ul", { className: "hl-pill-ports hl-out", children: outputs.map((port) => /* @__PURE__ */ jsx10(PortRow, { port }, port.id)) })
569
+ ] });
570
+ }
571
+
572
+ // src/nodes/HlangNode.tsx
573
+ import { jsx as jsx11, jsxs as jsxs4 } from "react/jsx-runtime";
574
+ function NodeChrome({ node, selected, body }) {
575
+ const inputs = node.ports.filter((p) => p.direction === "I");
576
+ const outputs = node.ports.filter((p) => p.direction === "O");
577
+ const live = useNodeLiveState(node.id);
578
+ const className = [
579
+ "hl-node",
580
+ `hl-${node.kind.toLowerCase()}`,
581
+ selected ? "is-selected" : "",
582
+ live === "active" ? "is-active" : "",
583
+ live === "error" ? "is-live-error" : ""
584
+ ].filter(Boolean).join(" ");
585
+ return /* @__PURE__ */ jsxs4("div", { className, "data-kind": node.kind, children: [
586
+ /* @__PURE__ */ jsx11(SignalBar, {}),
587
+ /* @__PURE__ */ jsxs4("header", { className: "hl-head", children: [
588
+ /* @__PURE__ */ jsx11("span", { className: "hl-kind", children: node.kind }),
589
+ /* @__PURE__ */ jsx11("span", { className: "hl-name", children: node.name }),
590
+ /* @__PURE__ */ jsx11("span", { className: "hl-role", children: KIND_ROLE[node.kind] })
591
+ ] }),
592
+ /* @__PURE__ */ jsx11(NodeBodySlot, { node, children: body }),
593
+ /* @__PURE__ */ jsxs4("div", { className: "hl-ports", children: [
594
+ /* @__PURE__ */ jsx11("ul", { className: "hl-col hl-in", children: inputs.map((port) => /* @__PURE__ */ jsx11(PortRow, { port }, port.id)) }),
595
+ /* @__PURE__ */ jsx11("ul", { className: "hl-col hl-out", children: outputs.map((port) => /* @__PURE__ */ jsx11(PortRow, { port }, port.id)) })
596
+ ] })
597
+ ] });
598
+ }
599
+ function HlangNode({ data, selected }) {
600
+ const node = data.editorNode;
601
+ if (typeof node.params.imageUrl === "string" && node.params.imageUrl.length > 0) {
602
+ return /* @__PURE__ */ jsx11(AssetNodeChrome, { node, selected });
603
+ }
604
+ return /* @__PURE__ */ jsx11(PillNodeChrome, { node, selected });
605
+ }
606
+
607
+ // src/edges/FlowEdge.tsx
608
+ import { BaseEdge, EdgeLabelRenderer, getBezierPath, useInternalNode } from "@xyflow/react";
609
+ import { Fragment, jsx as jsx12, jsxs as jsxs5 } from "react/jsx-runtime";
610
+ function useSourceRole(sourceId) {
611
+ var _a, _b;
612
+ const node = useInternalNode(sourceId);
613
+ const editorNode = (_a = node == null ? void 0 : node.data) == null ? void 0 : _a.editorNode;
614
+ const role = (_b = editorNode == null ? void 0 : editorNode.params) == null ? void 0 : _b.role;
615
+ return typeof role === "string" && role.length > 0 ? role : void 0;
616
+ }
617
+ function FlowEdge(props) {
618
+ var _a;
619
+ const { id, source, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, data } = props;
620
+ const [path] = getBezierPath({
621
+ sourceX,
622
+ sourceY,
623
+ sourcePosition,
624
+ targetX,
625
+ targetY,
626
+ targetPosition
627
+ });
628
+ const t = 0.62;
629
+ const labelX = sourceX + (targetX - sourceX) * t;
630
+ const labelY = sourceY + (targetY - sourceY) * t;
631
+ const isError = (_a = data == null ? void 0 : data.isError) != null ? _a : false;
632
+ const isValue = (data == null ? void 0 : data.stream) === "value";
633
+ const flowing = useEdgeFlowing(id);
634
+ const role = useSourceRole(source);
635
+ const className = [
636
+ "hl-edge",
637
+ isError ? "is-error" : "",
638
+ isValue ? "is-value" : "is-live",
639
+ flowing ? "is-flowing" : ""
640
+ ].filter(Boolean).join(" ");
641
+ return /* @__PURE__ */ jsxs5(Fragment, { children: [
642
+ /* @__PURE__ */ jsxs5("g", { className, children: [
643
+ /* @__PURE__ */ jsx12(BaseEdge, { id, path, className: "hl-edge-base" }),
644
+ /* @__PURE__ */ jsx12("path", { d: path, className: "hl-edge-flow" })
645
+ ] }),
646
+ role !== void 0 && /* @__PURE__ */ jsx12(EdgeLabelRenderer, { children: /* @__PURE__ */ jsx12(
647
+ "div",
648
+ {
649
+ className: "hl-edge-label hl-mono",
650
+ style: { transform: `translate(-50%, -50%) translate(${String(labelX)}px, ${String(labelY)}px)` },
651
+ children: role
652
+ }
653
+ ) })
654
+ ] });
655
+ }
656
+
657
+ // src/nodes/flow-types.ts
658
+ var nodeComponent = HlangNode;
659
+ var edgeComponent = FlowEdge;
660
+ function createHlangFlowTypes() {
661
+ return {
662
+ nodeTypes: { R: nodeComponent, T: nodeComponent, W: nodeComponent },
663
+ edgeTypes: { default: edgeComponent }
664
+ };
665
+ }
666
+
667
+ // src/shell/EditorShell.tsx
668
+ import { jsx as jsx13, jsxs as jsxs6 } from "react/jsx-runtime";
669
+ function EditorShell({
670
+ toolbar,
671
+ left,
672
+ right,
673
+ canvas,
674
+ footer,
675
+ leftCollapsed = false,
676
+ rightCollapsed = false
677
+ }) {
678
+ const className = [
679
+ "hl-shell",
680
+ leftCollapsed ? "hl-left-collapsed" : "",
681
+ rightCollapsed ? "hl-right-collapsed" : ""
682
+ ].filter(Boolean).join(" ");
683
+ return /* @__PURE__ */ jsxs6("div", { className, children: [
684
+ toolbar !== void 0 && /* @__PURE__ */ jsx13("div", { className: "hl-region-toolbar", children: toolbar }),
685
+ /* @__PURE__ */ jsxs6("div", { className: "hl-region-body", children: [
686
+ left !== void 0 && !leftCollapsed && /* @__PURE__ */ jsx13("aside", { className: "hl-region-left", children: left }),
687
+ /* @__PURE__ */ jsx13("main", { className: "hl-region-canvas", children: canvas }),
688
+ right !== void 0 && !rightCollapsed && /* @__PURE__ */ jsx13("aside", { className: "hl-region-right", children: right })
689
+ ] }),
690
+ footer !== void 0 && /* @__PURE__ */ jsx13("div", { className: "hl-region-footer", children: footer })
691
+ ] });
692
+ }
693
+
694
+ // src/toolbar/Toolbar.tsx
695
+ import { useHistory } from "@hlang-tech/editor-react";
696
+ import { Fragment as Fragment2, jsx as jsx14, jsxs as jsxs7 } from "react/jsx-runtime";
697
+ function Toolbar({ title, center, actions }) {
698
+ return /* @__PURE__ */ jsxs7("header", { className: "hl-toolbar", children: [
699
+ /* @__PURE__ */ jsx14("div", { className: "hl-toolbar-brand", children: title != null ? title : /* @__PURE__ */ jsxs7(Fragment2, { children: [
700
+ /* @__PURE__ */ jsx14("span", { className: "hl-toolbar-glyph", "aria-hidden": "true" }),
701
+ /* @__PURE__ */ jsx14("span", { className: "hl-toolbar-wordmark", children: "hlang" })
702
+ ] }) }),
703
+ center !== void 0 && /* @__PURE__ */ jsx14("div", { className: "hl-toolbar-center", children: center }),
704
+ /* @__PURE__ */ jsx14("div", { className: "hl-toolbar-actions", children: actions })
705
+ ] });
706
+ }
707
+ function UndoRedo({ core }) {
708
+ const history = useHistory(core);
709
+ return /* @__PURE__ */ jsxs7(Fragment2, { children: [
710
+ /* @__PURE__ */ jsx14(
711
+ "button",
712
+ {
713
+ type: "button",
714
+ className: "hl-toolbar-btn",
715
+ onClick: () => history.undo(),
716
+ disabled: !history.canUndo,
717
+ title: "Undo (\u2318Z)",
718
+ children: "Undo"
719
+ }
720
+ ),
721
+ /* @__PURE__ */ jsx14(
722
+ "button",
723
+ {
724
+ type: "button",
725
+ className: "hl-toolbar-btn",
726
+ onClick: () => history.redo(),
727
+ disabled: !history.canRedo,
728
+ title: "Redo (\u2318\u21E7Z)",
729
+ children: "Redo"
730
+ }
731
+ )
732
+ ] });
733
+ }
734
+ function RunButton({
735
+ onRun,
736
+ running = false,
737
+ runLabel = "Run",
738
+ stopLabel = "Stop"
739
+ }) {
740
+ return /* @__PURE__ */ jsxs7(
741
+ "button",
742
+ {
743
+ type: "button",
744
+ className: `hl-toolbar-btn hl-toolbar-run${running ? " is-running" : ""}`,
745
+ onClick: onRun,
746
+ "aria-pressed": running,
747
+ children: [
748
+ /* @__PURE__ */ jsx14(
749
+ "span",
750
+ {
751
+ className: `hl-toolbar-run-icon${running ? " is-stop" : ""}`,
752
+ "aria-hidden": "true"
753
+ }
754
+ ),
755
+ running ? stopLabel : runLabel
756
+ ]
757
+ }
758
+ );
759
+ }
760
+ function ToolbarStat({ label, value }) {
761
+ return /* @__PURE__ */ jsx14("span", { className: "hl-toolbar-stat hl-mono", title: label, children: value });
762
+ }
763
+ function ToolbarGhostButton({
764
+ children,
765
+ onClick,
766
+ title
767
+ }) {
768
+ return /* @__PURE__ */ jsx14("button", { type: "button", className: "hl-toolbar-btn", onClick, title, children });
769
+ }
770
+
771
+ // src/toolbar/ToolCluster.tsx
772
+ import { jsx as jsx15, jsxs as jsxs8 } from "react/jsx-runtime";
773
+ function ToolCluster({ items }) {
774
+ 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: [
775
+ item.separatorBefore === true && /* @__PURE__ */ jsx15("span", { className: "hl-toolcluster-sep", "aria-hidden": true }),
776
+ /* @__PURE__ */ jsx15(
777
+ "button",
778
+ {
779
+ type: "button",
780
+ className: "hl-toolcluster-btn" + (item.active === true ? " is-active" : ""),
781
+ onClick: item.onClick,
782
+ disabled: item.disabled,
783
+ title: item.title,
784
+ "aria-label": item.title,
785
+ "aria-pressed": item.active,
786
+ children: item.icon
787
+ }
788
+ )
789
+ ] }, item.id)) });
790
+ }
791
+
792
+ // src/canvas/CanvasChrome.tsx
793
+ import { Background, BackgroundVariant } from "@xyflow/react";
794
+ import { Fragment as Fragment3, jsx as jsx16 } from "react/jsx-runtime";
795
+ function CanvasChrome({ background = true }) {
796
+ return /* @__PURE__ */ jsx16(Fragment3, { children: background && /* @__PURE__ */ jsx16(Background, { variant: BackgroundVariant.Dots, gap: 26, size: 1, color: "#242830" }) });
797
+ }
798
+
799
+ // src/canvas/CanvasDock.tsx
800
+ import { jsx as jsx17, jsxs as jsxs9 } from "react/jsx-runtime";
801
+ function CursorIcon() {
802
+ 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" }) });
803
+ }
804
+ function HandIcon() {
805
+ 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" }) });
806
+ }
807
+ function PlusIcon() {
808
+ 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" }) });
809
+ }
810
+ function FitIcon() {
811
+ 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" }) });
812
+ }
813
+ function UndoIcon() {
814
+ 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" }) });
815
+ }
816
+ function RedoIcon() {
817
+ 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" }) });
818
+ }
819
+ function CanvasDock({
820
+ zoom,
821
+ onZoomIn,
822
+ onZoomOut,
823
+ onFitView,
824
+ onAdd,
825
+ mode,
826
+ onModeChange,
827
+ onUndo,
828
+ onRedo,
829
+ canUndo,
830
+ canRedo
831
+ }) {
832
+ const pct = `${String(Math.round(zoom * 100))}%`;
833
+ return /* @__PURE__ */ jsxs9("div", { className: "hl-dock", role: "toolbar", "aria-label": "Canvas tools", children: [
834
+ /* @__PURE__ */ jsx17(
835
+ "button",
836
+ {
837
+ className: "hl-dock-btn",
838
+ onClick: onUndo,
839
+ disabled: !canUndo,
840
+ title: "Undo (\u2318Z)",
841
+ "aria-label": "Undo",
842
+ children: /* @__PURE__ */ jsx17(UndoIcon, {})
843
+ }
844
+ ),
845
+ /* @__PURE__ */ jsx17(
846
+ "button",
847
+ {
848
+ className: "hl-dock-btn",
849
+ onClick: onRedo,
850
+ disabled: !canRedo,
851
+ title: "Redo (\u2318\u21E7Z)",
852
+ "aria-label": "Redo",
853
+ children: /* @__PURE__ */ jsx17(RedoIcon, {})
854
+ }
855
+ ),
856
+ /* @__PURE__ */ jsx17("span", { className: "hl-dock-sep", "aria-hidden": true }),
857
+ /* @__PURE__ */ jsx17(
858
+ "button",
859
+ {
860
+ className: "hl-dock-btn" + (mode === "select" ? " is-active" : ""),
861
+ onClick: () => onModeChange("select"),
862
+ title: "Select (V)",
863
+ "aria-pressed": mode === "select",
864
+ children: /* @__PURE__ */ jsx17(CursorIcon, {})
865
+ }
866
+ ),
867
+ /* @__PURE__ */ jsx17(
868
+ "button",
869
+ {
870
+ className: "hl-dock-btn" + (mode === "pan" ? " is-active" : ""),
871
+ onClick: () => onModeChange("pan"),
872
+ title: "Pan (Space)",
873
+ "aria-pressed": mode === "pan",
874
+ children: /* @__PURE__ */ jsx17(HandIcon, {})
875
+ }
876
+ ),
877
+ /* @__PURE__ */ jsx17("button", { className: "hl-dock-btn hl-dock-add", onClick: onAdd, title: "Add node", children: /* @__PURE__ */ jsx17(PlusIcon, {}) }),
878
+ /* @__PURE__ */ jsx17("span", { className: "hl-dock-sep", "aria-hidden": true }),
879
+ /* @__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" }) }),
880
+ /* @__PURE__ */ jsx17("button", { className: "hl-dock-zoom hl-mono", onClick: onFitView, title: "Fit view", children: pct }),
881
+ /* @__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: "+" }) }),
882
+ /* @__PURE__ */ jsx17("button", { className: "hl-dock-btn", onClick: onFitView, title: "Fit view", "aria-label": "Fit view", children: /* @__PURE__ */ jsx17(FitIcon, {}) })
883
+ ] });
884
+ }
885
+
886
+ // src/canvas/EmptyState.tsx
887
+ import { jsx as jsx18, jsxs as jsxs10 } from "react/jsx-runtime";
888
+ function EmptyState({
889
+ title = "An empty canvas.",
890
+ hint = "add a source \xB7 wire it to a sink \xB7 run"
891
+ }) {
892
+ return /* @__PURE__ */ jsxs10("div", { className: "hl-empty", "aria-hidden": "true", children: [
893
+ /* @__PURE__ */ jsx18("p", { className: "hl-empty-title", children: title }),
894
+ /* @__PURE__ */ jsx18("p", { className: "hl-empty-hint", children: hint })
895
+ ] });
896
+ }
897
+
898
+ // src/panels/InspectorPanel.tsx
899
+ import { useSelection } from "@hlang-tech/editor-react";
900
+
901
+ // src/schema/param-field.ts
902
+ function humanizeKey(key) {
903
+ const spaced = key.replace(/[_-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").trim();
904
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
905
+ }
906
+ function fieldFor(key, value) {
907
+ const label = humanizeKey(key);
908
+ if (typeof value === "boolean") {
909
+ return { key, label, type: "toggle", value };
910
+ }
911
+ if (typeof value === "number") {
912
+ return { key, label, type: "number", value };
913
+ }
914
+ if (typeof value === "string") {
915
+ const type = value.includes("\n") ? "textarea" : "text";
916
+ return { key, label, type, value };
917
+ }
918
+ return { key, label, type: "custom", fieldType: "json", value };
919
+ }
920
+ function deriveParamSchema(descriptor, node) {
921
+ const fields = [];
922
+ for (const key of Object.keys(descriptor.defaultParams)) {
923
+ const current = key in node.params ? node.params[key] : descriptor.defaultParams[key];
924
+ fields.push(fieldFor(key, current != null ? current : null));
925
+ }
926
+ return fields;
927
+ }
928
+
929
+ // src/hooks/useNodeParams.ts
930
+ import { useCallback as useCallback2, useMemo as useMemo2, useRef as useRef2, useSyncExternalStore as useSyncExternalStore2 } from "react";
931
+ import { UpdateParamsCommand } from "@hlang-tech/editor-core";
932
+ function useNodeParams(core, nodeId) {
933
+ const versionRef = useRef2(0);
934
+ const subscribe = useCallback2(
935
+ (onStoreChange) => {
936
+ const disposable = core.events.on("nodeChanged", (event) => {
937
+ if (nodeId !== void 0 && event.node.id === nodeId) {
938
+ versionRef.current += 1;
939
+ onStoreChange();
940
+ }
941
+ });
942
+ return () => {
943
+ disposable.dispose();
944
+ };
945
+ },
946
+ [core, nodeId]
947
+ );
948
+ const getSnapshot = useCallback2(() => versionRef.current, []);
949
+ const version = useSyncExternalStore2(subscribe, getSnapshot, getSnapshot);
950
+ const setParams = useCallback2(
951
+ (next) => {
952
+ if (nodeId === void 0) {
953
+ return;
954
+ }
955
+ core.commands.execute(new UpdateParamsCommand(nodeId, next));
956
+ },
957
+ [core, nodeId]
958
+ );
959
+ const setParam = useCallback2(
960
+ (key, value) => {
961
+ if (nodeId === void 0) {
962
+ return;
963
+ }
964
+ const node = core.model.getNode(nodeId);
965
+ if (node === void 0) {
966
+ return;
967
+ }
968
+ setParams({ ...node.params, [key]: value });
969
+ },
970
+ [core, nodeId, setParams]
971
+ );
972
+ return useMemo2(
973
+ () => ({
974
+ node: nodeId === void 0 ? void 0 : core.model.getNode(nodeId),
975
+ setParam,
976
+ setParams
977
+ }),
978
+ // `version` re-reads the node after each nodeChanged for this id.
979
+ // eslint-disable-next-line react-hooks/exhaustive-deps
980
+ [core, nodeId, version, setParam, setParams]
981
+ );
982
+ }
983
+
984
+ // src/panels/ParamForm.tsx
985
+ import { useState as useState2 } from "react";
986
+ import { jsx as jsx19, jsxs as jsxs11 } from "react/jsx-runtime";
987
+ function ParamForm({ fields, onCommit }) {
988
+ return /* @__PURE__ */ jsx19("div", { className: "hl-form", children: fields.map((field) => /* @__PURE__ */ jsx19(FieldRow, { field, onCommit }, field.key)) });
989
+ }
990
+ function FieldRow({
991
+ field,
992
+ onCommit
993
+ }) {
994
+ return /* @__PURE__ */ jsxs11("label", { className: "hl-field", children: [
995
+ /* @__PURE__ */ jsx19("span", { className: "hl-field-label", children: field.label }),
996
+ /* @__PURE__ */ jsx19(FieldControl, { field, onCommit })
997
+ ] });
998
+ }
999
+ function FieldControl({
1000
+ field,
1001
+ onCommit
1002
+ }) {
1003
+ switch (field.type) {
1004
+ case "toggle":
1005
+ return /* @__PURE__ */ jsx19(
1006
+ "button",
1007
+ {
1008
+ type: "button",
1009
+ role: "switch",
1010
+ "aria-checked": field.value,
1011
+ className: `hl-toggle${field.value ? " is-on" : ""}`,
1012
+ disabled: field.disabled,
1013
+ onClick: () => onCommit(field.key, !field.value),
1014
+ children: /* @__PURE__ */ jsx19("span", { className: "hl-toggle-knob" })
1015
+ }
1016
+ );
1017
+ case "number":
1018
+ return /* @__PURE__ */ jsx19(NumberControl, { field, onCommit });
1019
+ case "textarea":
1020
+ return /* @__PURE__ */ jsx19(TextControl, { field, onCommit, multiline: true });
1021
+ case "text":
1022
+ return /* @__PURE__ */ jsx19(TextControl, { field, onCommit });
1023
+ case "custom":
1024
+ return /* @__PURE__ */ jsx19("span", { className: "hl-field-custom hl-mono", title: field.fieldType, children: describeCustom(field.value) });
1025
+ }
1026
+ }
1027
+ function TextControl({
1028
+ field,
1029
+ onCommit,
1030
+ multiline = false
1031
+ }) {
1032
+ const [draft, setDraft] = useState2(field.value);
1033
+ const commit = () => {
1034
+ if (draft !== field.value) {
1035
+ onCommit(field.key, draft);
1036
+ }
1037
+ };
1038
+ const shared = {
1039
+ className: "hl-input",
1040
+ value: draft,
1041
+ disabled: field.disabled,
1042
+ spellCheck: false,
1043
+ onBlur: commit
1044
+ };
1045
+ if (multiline) {
1046
+ return /* @__PURE__ */ jsx19(
1047
+ "textarea",
1048
+ {
1049
+ ...shared,
1050
+ rows: 4,
1051
+ onChange: (e) => setDraft(e.target.value)
1052
+ }
1053
+ );
1054
+ }
1055
+ return /* @__PURE__ */ jsx19(
1056
+ "input",
1057
+ {
1058
+ ...shared,
1059
+ type: "text",
1060
+ onChange: (e) => setDraft(e.target.value),
1061
+ onKeyDown: (e) => {
1062
+ if (e.key === "Enter") {
1063
+ e.currentTarget.blur();
1064
+ }
1065
+ }
1066
+ }
1067
+ );
1068
+ }
1069
+ function NumberControl({
1070
+ field,
1071
+ onCommit
1072
+ }) {
1073
+ const [draft, setDraft] = useState2(String(field.value));
1074
+ const commit = () => {
1075
+ const parsed = Number(draft);
1076
+ if (!Number.isNaN(parsed) && parsed !== field.value) {
1077
+ onCommit(field.key, parsed);
1078
+ }
1079
+ };
1080
+ return /* @__PURE__ */ jsx19(
1081
+ "input",
1082
+ {
1083
+ className: "hl-input hl-mono",
1084
+ type: "number",
1085
+ inputMode: "decimal",
1086
+ value: draft,
1087
+ disabled: field.disabled,
1088
+ onChange: (e) => setDraft(e.target.value),
1089
+ onBlur: commit,
1090
+ onKeyDown: (e) => {
1091
+ if (e.key === "Enter") {
1092
+ e.currentTarget.blur();
1093
+ }
1094
+ }
1095
+ }
1096
+ );
1097
+ }
1098
+ function describeCustom(value) {
1099
+ if (Array.isArray(value)) {
1100
+ return `[${String(value.length)} items]`;
1101
+ }
1102
+ if (value === null) {
1103
+ return "null";
1104
+ }
1105
+ if (typeof value === "object") {
1106
+ return "{\u2026}";
1107
+ }
1108
+ return String(value);
1109
+ }
1110
+
1111
+ // src/panels/InspectorPanel.tsx
1112
+ import { jsx as jsx20, jsxs as jsxs12 } from "react/jsx-runtime";
1113
+ function InspectorPanel({ core, emptyState }) {
1114
+ const selection = useSelection(core);
1115
+ const selectedNodes2 = selection.selectedNodes;
1116
+ const singleId = selectedNodes2.length === 1 ? selectedNodes2[0] : void 0;
1117
+ const params = useNodeParams(core, singleId);
1118
+ const node = params.node;
1119
+ if (node === void 0) {
1120
+ return /* @__PURE__ */ jsx20("div", { className: "hl-inspector hl-inspector-empty", children: emptyState != null ? emptyState : /* @__PURE__ */ jsxs12("div", { className: "hl-inspector-blank", children: [
1121
+ /* @__PURE__ */ jsx20("span", { className: "hl-inspector-blank-badge", "aria-hidden": true }),
1122
+ /* @__PURE__ */ jsx20("p", { className: "hl-inspector-blank-title", children: selectedNodes2.length > 1 ? `${String(selectedNodes2.length)} nodes selected` : "Nothing selected" }),
1123
+ /* @__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." })
1124
+ ] }) });
1125
+ }
1126
+ const descriptor = core.registry.get(node.code);
1127
+ const fields = descriptor === void 0 ? [] : deriveParamSchema(descriptor, node);
1128
+ const imageUrl = typeof node.params.imageUrl === "string" && node.params.imageUrl.length > 0 ? node.params.imageUrl : void 0;
1129
+ return /* @__PURE__ */ jsxs12("div", { className: "hl-inspector", children: [
1130
+ /* @__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 }) }),
1131
+ /* @__PURE__ */ jsxs12("header", { className: "hl-inspector-head", children: [
1132
+ /* @__PURE__ */ jsx20("span", { className: `hl-inspector-kind hl-${node.kind.toLowerCase()}`, children: node.kind }),
1133
+ /* @__PURE__ */ jsxs12("div", { className: "hl-inspector-titles", children: [
1134
+ /* @__PURE__ */ jsx20("span", { className: "hl-inspector-name", children: node.name }),
1135
+ /* @__PURE__ */ jsxs12("span", { className: "hl-inspector-role hl-mono", children: [
1136
+ node.code,
1137
+ " \xB7 ",
1138
+ KIND_ROLE[node.kind]
1139
+ ] })
1140
+ ] })
1141
+ ] }),
1142
+ /* @__PURE__ */ jsxs12("section", { className: "hl-inspector-section", children: [
1143
+ /* @__PURE__ */ jsx20("h3", { className: "hl-inspector-section-title", children: "Parameters" }),
1144
+ fields.length === 0 ? /* @__PURE__ */ jsx20("p", { className: "hl-inspector-hint", children: "This node has no parameters." }) : /* @__PURE__ */ jsx20(
1145
+ ParamForm,
1146
+ {
1147
+ fields,
1148
+ onCommit: (key, value) => params.setParam(key, value)
1149
+ }
1150
+ )
1151
+ ] }),
1152
+ /* @__PURE__ */ jsxs12("section", { className: "hl-inspector-section", children: [
1153
+ /* @__PURE__ */ jsx20("h3", { className: "hl-inspector-section-title", children: "Ports" }),
1154
+ /* @__PURE__ */ jsx20("ul", { className: "hl-inspector-ports", children: node.ports.map((port) => /* @__PURE__ */ jsxs12(
1155
+ "li",
1156
+ {
1157
+ className: `hl-inspector-port${port.isError ? " is-error" : ""}`,
1158
+ children: [
1159
+ /* @__PURE__ */ jsx20("span", { className: `hl-port-dot hl-dir-${port.direction.toLowerCase()}` }),
1160
+ /* @__PURE__ */ jsx20("span", { className: "hl-mono", children: port.name }),
1161
+ /* @__PURE__ */ jsx20("span", { className: "hl-inspector-port-dir hl-mono", children: port.direction })
1162
+ ]
1163
+ },
1164
+ port.id
1165
+ )) })
1166
+ ] })
1167
+ ] });
1168
+ }
1169
+
1170
+ // src/panels/OutlineTree.tsx
1171
+ import { useCallback as useCallback3, useEffect as useEffect2, useMemo as useMemo3, useState as useState3 } from "react";
1172
+ import { GraphTopology } from "@hlang-tech/editor-core";
1173
+ import { useSelection as useSelection2, useViewState, useDiagnostics } from "@hlang-tech/editor-react";
1174
+ import { jsx as jsx21, jsxs as jsxs13 } from "react/jsx-runtime";
1175
+ function useTopologyComponents(core) {
1176
+ const [tick, setTick] = useState3(0);
1177
+ const bump = useCallback3(() => setTick((t) => t + 1), []);
1178
+ useEffect2(() => {
1179
+ const kinds = ["nodeAdded", "nodeRemoved", "nodeChanged", "edgeAdded", "edgeRemoved"];
1180
+ const disposers = kinds.map((k) => core.events.on(k, bump));
1181
+ bump();
1182
+ return () => {
1183
+ for (const d of disposers) {
1184
+ d.dispose();
1185
+ }
1186
+ };
1187
+ }, [core, bump]);
1188
+ return useMemo3(() => {
1189
+ void tick;
1190
+ const topo = new GraphTopology();
1191
+ topo.reset(core.model.getNodes(), core.model.getEdges());
1192
+ return topo.components();
1193
+ }, [core, tick]);
1194
+ }
1195
+ function OutlineTree({ core, onFocus }) {
1196
+ const components = useTopologyComponents(core);
1197
+ const selection = useSelection2(core);
1198
+ const viewState = useViewState(core);
1199
+ const diagnostics = useDiagnostics(core);
1200
+ const [collapsed, setCollapsed] = useState3(/* @__PURE__ */ new Set());
1201
+ const nodeById = useMemo3(() => {
1202
+ const m = /* @__PURE__ */ new Map();
1203
+ for (const n of core.model.getNodes()) {
1204
+ m.set(n.id, n);
1205
+ }
1206
+ return m;
1207
+ }, [core, components]);
1208
+ const toggleGroup = useCallback3((id) => {
1209
+ setCollapsed((prev) => {
1210
+ const next = new Set(prev);
1211
+ if (next.has(id)) {
1212
+ next.delete(id);
1213
+ } else {
1214
+ next.add(id);
1215
+ }
1216
+ return next;
1217
+ });
1218
+ }, []);
1219
+ const select = useCallback3(
1220
+ (id) => {
1221
+ selection.selectNodes([id], "replace");
1222
+ onFocus == null ? void 0 : onFocus(id);
1223
+ },
1224
+ [selection, onFocus]
1225
+ );
1226
+ if (components.length === 0) {
1227
+ return /* @__PURE__ */ jsx21("p", { className: "hl-outline-empty hl-mono", children: "no layers yet" });
1228
+ }
1229
+ const groups = [...components].sort((a, b) => b.nodeIds.size - a.nodeIds.size);
1230
+ return /* @__PURE__ */ jsx21("div", { className: "hl-outline", role: "tree", "aria-label": "Outline", children: groups.map((group, gi) => {
1231
+ const members = [...group.nodeIds].map((id) => nodeById.get(id)).filter((n) => n !== void 0);
1232
+ const open = !collapsed.has(group.id);
1233
+ const groupName = `Flow ${String(gi + 1)}`;
1234
+ return /* @__PURE__ */ jsxs13("div", { className: "hl-outline-group", role: "treeitem", "aria-expanded": open, children: [
1235
+ /* @__PURE__ */ jsxs13(
1236
+ "button",
1237
+ {
1238
+ className: "hl-outline-grouphead",
1239
+ onClick: () => toggleGroup(group.id),
1240
+ children: [
1241
+ /* @__PURE__ */ jsx21("span", { className: "hl-outline-caret" + (open ? " is-open" : ""), "aria-hidden": true, children: "\u25B8" }),
1242
+ /* @__PURE__ */ jsx21("span", { className: "hl-outline-groupname", children: groupName }),
1243
+ /* @__PURE__ */ jsx21("span", { className: "hl-outline-count hl-mono", children: group.nodeIds.size })
1244
+ ]
1245
+ }
1246
+ ),
1247
+ open && /* @__PURE__ */ jsx21("ul", { className: "hl-outline-members", children: members.map((node) => {
1248
+ const active = selection.isSelected(node.id);
1249
+ const isHidden = viewState.isHidden(node.id);
1250
+ const isLocked = viewState.isLocked(node.id);
1251
+ const hasImage = typeof node.params.imageUrl === "string" && node.params.imageUrl.length > 0;
1252
+ return /* @__PURE__ */ jsx21("li", { children: /* @__PURE__ */ jsxs13(
1253
+ "div",
1254
+ {
1255
+ className: "hl-outline-row" + (active ? " is-active" : "") + (isHidden ? " is-hidden" : "") + (isLocked ? " is-locked" : ""),
1256
+ children: [
1257
+ /* @__PURE__ */ jsxs13("button", { className: "hl-outline-main", onClick: () => select(node.id), children: [
1258
+ /* @__PURE__ */ jsx21(
1259
+ "span",
1260
+ {
1261
+ className: `hl-outline-icon hl-kind-${node.kind.toLowerCase()}`,
1262
+ "aria-hidden": true,
1263
+ children: hasImage ? /* @__PURE__ */ jsx21(
1264
+ "img",
1265
+ {
1266
+ className: "hl-outline-thumb",
1267
+ src: node.params.imageUrl,
1268
+ alt: ""
1269
+ }
1270
+ ) : /* @__PURE__ */ jsx21(NodeIcon, { code: node.code, kind: node.kind, size: 13 })
1271
+ }
1272
+ ),
1273
+ /* @__PURE__ */ jsx21("span", { className: "hl-outline-name", children: node.name }),
1274
+ (() => {
1275
+ const diag = diagnostics.worstOf(node.id);
1276
+ return diag === void 0 ? null : /* @__PURE__ */ jsx21(DiagnosticBadge, { diagnostic: diag, variant: "hl-outline-diag", size: 11 });
1277
+ })(),
1278
+ isLocked && /* @__PURE__ */ jsx21("span", { className: "hl-outline-badge", "aria-hidden": true, children: /* @__PURE__ */ jsx21(LockIcon, { size: 11 }) }),
1279
+ isHidden && /* @__PURE__ */ jsx21("span", { className: "hl-outline-badge", "aria-hidden": true, children: /* @__PURE__ */ jsx21(EyeOffIcon, { size: 11 }) })
1280
+ ] }),
1281
+ /* @__PURE__ */ jsxs13("span", { className: "hl-outline-controls", children: [
1282
+ /* @__PURE__ */ jsx21(
1283
+ "button",
1284
+ {
1285
+ className: "hl-outline-ctl" + (isHidden ? " is-active-ctl" : ""),
1286
+ onClick: () => viewState.toggleHidden(node.id),
1287
+ "aria-label": isHidden ? "Show layer" : "Hide layer",
1288
+ title: isHidden ? "Show" : "Hide",
1289
+ children: isHidden ? /* @__PURE__ */ jsx21(EyeOffIcon, {}) : /* @__PURE__ */ jsx21(EyeIcon, {})
1290
+ }
1291
+ ),
1292
+ /* @__PURE__ */ jsx21(
1293
+ "button",
1294
+ {
1295
+ className: "hl-outline-ctl" + (isLocked ? " is-active-ctl" : ""),
1296
+ onClick: () => viewState.toggleLocked(node.id),
1297
+ "aria-label": isLocked ? "Unlock layer" : "Lock layer",
1298
+ title: isLocked ? "Unlock" : "Lock",
1299
+ children: isLocked ? /* @__PURE__ */ jsx21(LockIcon, {}) : /* @__PURE__ */ jsx21(UnlockIcon, {})
1300
+ }
1301
+ )
1302
+ ] })
1303
+ ]
1304
+ }
1305
+ ) }, node.id);
1306
+ }) })
1307
+ ] }, group.id);
1308
+ }) });
1309
+ }
1310
+
1311
+ // src/panels/PreviewPanel.tsx
1312
+ import { jsx as jsx22, jsxs as jsxs14 } from "react/jsx-runtime";
1313
+ function PreviewPanel({ imageUrl, title = "Preview", status }) {
1314
+ return /* @__PURE__ */ jsxs14("section", { className: "hl-preview", "aria-label": "Preview", children: [
1315
+ /* @__PURE__ */ jsxs14("div", { className: "hl-preview-head", children: [
1316
+ /* @__PURE__ */ jsx22("span", { className: "hl-preview-title", children: title }),
1317
+ status !== void 0 && /* @__PURE__ */ jsx22("span", { className: "hl-preview-status hl-mono", children: status })
1318
+ ] }),
1319
+ /* @__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" }) })
1320
+ ] });
1321
+ }
1322
+
1323
+ // src/panels/RailTabs.tsx
1324
+ import { jsx as jsx23 } from "react/jsx-runtime";
1325
+ function RailTabs({ tabs, active, onChange }) {
1326
+ return /* @__PURE__ */ jsx23("div", { className: "hl-railtabs", role: "tablist", children: tabs.map((tab) => /* @__PURE__ */ jsx23(
1327
+ "button",
1328
+ {
1329
+ role: "tab",
1330
+ "aria-selected": tab.id === active,
1331
+ className: "hl-railtab" + (tab.id === active ? " is-active" : ""),
1332
+ onClick: () => onChange(tab.id),
1333
+ children: tab.label
1334
+ },
1335
+ tab.id
1336
+ )) });
1337
+ }
1338
+
1339
+ // src/panels/AssetGrid.tsx
1340
+ import { jsx as jsx24 } from "react/jsx-runtime";
1341
+ function AssetGrid({ items, onSelect, emptyHint = "No assets yet" }) {
1342
+ if (items.length === 0) {
1343
+ return /* @__PURE__ */ jsx24("p", { className: "hl-assetgrid-empty hl-mono", children: emptyHint });
1344
+ }
1345
+ return /* @__PURE__ */ jsx24("div", { className: "hl-assetgrid", children: items.map((item) => {
1346
+ var _a;
1347
+ return /* @__PURE__ */ jsx24(
1348
+ "button",
1349
+ {
1350
+ className: "hl-assetgrid-cell",
1351
+ onClick: () => onSelect == null ? void 0 : onSelect(item.id),
1352
+ title: item.label,
1353
+ children: /* @__PURE__ */ jsx24("img", { className: "hl-assetgrid-img", src: item.url, alt: (_a = item.label) != null ? _a : "", loading: "lazy" })
1354
+ },
1355
+ item.id
1356
+ );
1357
+ }) });
1358
+ }
1359
+
1360
+ // src/panels/PositionPanel.tsx
1361
+ import { useCallback as useCallback4, useEffect as useEffect3, useReducer } from "react";
1362
+ import { MoveNodeCommand } from "@hlang-tech/editor-core";
1363
+ import { useSelection as useSelection3 } from "@hlang-tech/editor-react";
1364
+ import { jsx as jsx25, jsxs as jsxs15 } from "react/jsx-runtime";
1365
+ function selectedNodes(core) {
1366
+ const ids = core.selection.getSelectedNodes();
1367
+ const out = [];
1368
+ for (const id of ids) {
1369
+ const n = core.model.getNode(id);
1370
+ if (n !== void 0) {
1371
+ out.push(n);
1372
+ }
1373
+ }
1374
+ return out;
1375
+ }
1376
+ function PositionPanel({ core, livePosition }) {
1377
+ var _a;
1378
+ useSelection3(core);
1379
+ const [, bump] = useReducer((n) => n + 1, 0);
1380
+ useEffect3(() => {
1381
+ const kinds = ["nodeChanged", "nodeAdded", "nodeRemoved"];
1382
+ const disposers = kinds.map((k) => core.events.on(k, bump));
1383
+ return () => {
1384
+ for (const d of disposers) {
1385
+ d.dispose();
1386
+ }
1387
+ };
1388
+ }, [core]);
1389
+ const nodes = selectedNodes(core);
1390
+ const single = nodes.length === 1 ? nodes[0] : void 0;
1391
+ const shownPos = single !== void 0 ? (_a = livePosition == null ? void 0 : livePosition(single.id)) != null ? _a : single.position : void 0;
1392
+ const moveAll = useCallback4(
1393
+ (fn) => {
1394
+ const targets = selectedNodes(core);
1395
+ if (targets.length === 0) {
1396
+ return;
1397
+ }
1398
+ core.history.transact("Align", () => {
1399
+ for (const n of targets) {
1400
+ core.commands.execute(new MoveNodeCommand(n.id, fn(n)));
1401
+ }
1402
+ });
1403
+ },
1404
+ [core]
1405
+ );
1406
+ const align = useCallback4(
1407
+ (kind) => {
1408
+ const targets = selectedNodes(core);
1409
+ if (targets.length === 0) {
1410
+ return;
1411
+ }
1412
+ const xs = targets.map((n) => n.position.x);
1413
+ const ys = targets.map((n) => n.position.y);
1414
+ const minX = Math.min(...xs);
1415
+ const maxX = Math.max(...xs);
1416
+ const minY = Math.min(...ys);
1417
+ const maxY = Math.max(...ys);
1418
+ const cx = (minX + maxX) / 2;
1419
+ const cy = (minY + maxY) / 2;
1420
+ moveAll((n) => {
1421
+ switch (kind) {
1422
+ case "left":
1423
+ return { x: minX, y: n.position.y };
1424
+ case "hcenter":
1425
+ return { x: cx, y: n.position.y };
1426
+ case "right":
1427
+ return { x: maxX, y: n.position.y };
1428
+ case "top":
1429
+ return { x: n.position.x, y: minY };
1430
+ case "vcenter":
1431
+ return { x: n.position.x, y: cy };
1432
+ case "bottom":
1433
+ return { x: n.position.x, y: maxY };
1434
+ }
1435
+ });
1436
+ },
1437
+ [core, moveAll]
1438
+ );
1439
+ const setCoord = useCallback4(
1440
+ (axis, value) => {
1441
+ if (single === void 0 || Number.isNaN(value)) {
1442
+ return;
1443
+ }
1444
+ const next = axis === "x" ? { x: value, y: single.position.y } : { x: single.position.x, y: value };
1445
+ core.commands.execute(new MoveNodeCommand(single.id, next));
1446
+ },
1447
+ [core, single]
1448
+ );
1449
+ const disabled = nodes.length === 0;
1450
+ const alignButtons = [
1451
+ { kind: "left", label: "Align left", d: "M5 4v16M9 8h8M9 16h5" },
1452
+ { kind: "hcenter", label: "Align centre", d: "M12 4v16M8 9h8M9 15h6" },
1453
+ { kind: "right", label: "Align right", d: "M19 4v16M7 8h8M12 16h3" },
1454
+ { kind: "top", label: "Align top", d: "M4 5h16M8 9v8M16 9v5" },
1455
+ { kind: "vcenter", label: "Align middle", d: "M4 12h16M9 8v8M15 9v6" },
1456
+ { kind: "bottom", label: "Align bottom", d: "M4 19h16M8 7v8M16 12v3" }
1457
+ ];
1458
+ return /* @__PURE__ */ jsxs15("section", { className: "hl-position", children: [
1459
+ /* @__PURE__ */ jsx25("h3", { className: "hl-position-title hl-mono", children: "Position" }),
1460
+ /* @__PURE__ */ jsx25("div", { className: "hl-position-align", role: "group", "aria-label": "Align", children: alignButtons.map((b) => /* @__PURE__ */ jsx25(
1461
+ "button",
1462
+ {
1463
+ className: "hl-position-alignbtn",
1464
+ onClick: () => align(b.kind),
1465
+ disabled,
1466
+ title: b.label,
1467
+ "aria-label": b.label,
1468
+ 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 }) })
1469
+ },
1470
+ b.kind
1471
+ )) }),
1472
+ /* @__PURE__ */ jsxs15("div", { className: "hl-position-coords", children: [
1473
+ /* @__PURE__ */ jsxs15("label", { className: "hl-position-field", children: [
1474
+ /* @__PURE__ */ jsx25(
1475
+ "input",
1476
+ {
1477
+ type: "number",
1478
+ className: "hl-position-input hl-mono",
1479
+ value: shownPos !== void 0 ? Math.round(shownPos.x) : "",
1480
+ disabled: single === void 0,
1481
+ onChange: (e) => setCoord("x", Number(e.target.value)),
1482
+ "aria-label": "X position"
1483
+ }
1484
+ ),
1485
+ /* @__PURE__ */ jsx25("span", { className: "hl-position-axis hl-mono", children: "X" })
1486
+ ] }),
1487
+ /* @__PURE__ */ jsxs15("label", { className: "hl-position-field", children: [
1488
+ /* @__PURE__ */ jsx25(
1489
+ "input",
1490
+ {
1491
+ type: "number",
1492
+ className: "hl-position-input hl-mono",
1493
+ value: shownPos !== void 0 ? Math.round(shownPos.y) : "",
1494
+ disabled: single === void 0,
1495
+ onChange: (e) => setCoord("y", Number(e.target.value)),
1496
+ "aria-label": "Y position"
1497
+ }
1498
+ ),
1499
+ /* @__PURE__ */ jsx25("span", { className: "hl-position-axis hl-mono", children: "Y" })
1500
+ ] })
1501
+ ] })
1502
+ ] });
1503
+ }
1504
+
1505
+ // src/command-palette/NodeSearch.tsx
1506
+ import { useCallback as useCallback5, useEffect as useEffect4, useMemo as useMemo4, useRef as useRef3, useState as useState4 } from "react";
1507
+ import { Fragment as Fragment4, jsx as jsx26, jsxs as jsxs16 } from "react/jsx-runtime";
1508
+ var PANEL_WIDTH = 320;
1509
+ var PANEL_MAX_HEIGHT = 360;
1510
+ function NodeSearch({ controller }) {
1511
+ const { state } = controller;
1512
+ const [query, setQuery] = useState4("");
1513
+ const [active, setActive] = useState4(0);
1514
+ const inputRef = useRef3(null);
1515
+ const isOpen = state !== void 0;
1516
+ useEffect4(() => {
1517
+ var _a;
1518
+ if (isOpen) {
1519
+ setQuery("");
1520
+ setActive(0);
1521
+ (_a = inputRef.current) == null ? void 0 : _a.focus();
1522
+ }
1523
+ }, [isOpen]);
1524
+ const results = useMemo4(
1525
+ () => state !== void 0 ? controller.results(query) : [],
1526
+ [controller, state, query]
1527
+ );
1528
+ const clampedActive = results.length === 0 ? 0 : Math.min(active, results.length - 1);
1529
+ const choose = useCallback5(
1530
+ (view) => {
1531
+ if (view !== void 0) {
1532
+ controller.commit(view);
1533
+ }
1534
+ },
1535
+ [controller]
1536
+ );
1537
+ const onKeyDown = useCallback5(
1538
+ (event) => {
1539
+ switch (event.key) {
1540
+ case "ArrowDown":
1541
+ event.preventDefault();
1542
+ setActive((i) => results.length === 0 ? 0 : (i + 1) % results.length);
1543
+ break;
1544
+ case "ArrowUp":
1545
+ event.preventDefault();
1546
+ setActive((i) => results.length === 0 ? 0 : (i - 1 + results.length) % results.length);
1547
+ break;
1548
+ case "Enter":
1549
+ event.preventDefault();
1550
+ choose(results[clampedActive]);
1551
+ break;
1552
+ case "Escape":
1553
+ event.preventDefault();
1554
+ controller.close();
1555
+ break;
1556
+ default:
1557
+ break;
1558
+ }
1559
+ },
1560
+ [results, clampedActive, choose, controller]
1561
+ );
1562
+ if (state === void 0) {
1563
+ return null;
1564
+ }
1565
+ const style = clampToViewport(state.screen.x, state.screen.y);
1566
+ const connectMode = state.pending !== void 0;
1567
+ return /* @__PURE__ */ jsxs16(Fragment4, { children: [
1568
+ /* @__PURE__ */ jsx26(
1569
+ "div",
1570
+ {
1571
+ className: "hl-node-search-scrim",
1572
+ onPointerDown: () => {
1573
+ controller.close();
1574
+ }
1575
+ }
1576
+ ),
1577
+ /* @__PURE__ */ jsxs16("div", { className: "hl-node-search", style, role: "dialog", "aria-label": "Create node", children: [
1578
+ /* @__PURE__ */ jsxs16("div", { className: "hl-node-search-field", children: [
1579
+ /* @__PURE__ */ jsx26("span", { className: "hl-node-search-glyph", "aria-hidden": true, children: connectMode ? "\u2933" : "\uFF0B" }),
1580
+ /* @__PURE__ */ jsx26(
1581
+ "input",
1582
+ {
1583
+ ref: inputRef,
1584
+ className: "hl-node-search-input",
1585
+ value: query,
1586
+ placeholder: connectMode ? "connect to\u2026" : "search nodes\u2026",
1587
+ onChange: (e) => {
1588
+ setQuery(e.target.value);
1589
+ setActive(0);
1590
+ },
1591
+ onKeyDown,
1592
+ "aria-label": "Search nodes",
1593
+ autoComplete: "off",
1594
+ spellCheck: false
1595
+ }
1596
+ )
1597
+ ] }),
1598
+ /* @__PURE__ */ jsxs16("ul", { className: "hl-node-search-list", role: "listbox", children: [
1599
+ results.length === 0 && /* @__PURE__ */ jsx26("li", { className: "hl-node-search-empty hl-mono", children: "no matching nodes" }),
1600
+ results.map((view, index) => {
1601
+ const descriptor = view.result.value;
1602
+ const kind = descriptor.kind;
1603
+ return /* @__PURE__ */ jsxs16(
1604
+ "li",
1605
+ {
1606
+ role: "option",
1607
+ "aria-selected": index === clampedActive,
1608
+ className: "hl-node-search-item" + (index === clampedActive ? " is-active" : ""),
1609
+ onPointerEnter: () => setActive(index),
1610
+ onPointerDown: (e) => {
1611
+ e.preventDefault();
1612
+ choose(view);
1613
+ },
1614
+ children: [
1615
+ /* @__PURE__ */ jsx26(
1616
+ "span",
1617
+ {
1618
+ className: `hl-node-search-kind hl-mono hl-kind-${kind.toLowerCase()}`,
1619
+ children: kind
1620
+ }
1621
+ ),
1622
+ /* @__PURE__ */ jsx26("span", { className: "hl-node-search-label", children: highlight(descriptor.displayName, view.result.match) }),
1623
+ view.connectPortId !== void 0 && /* @__PURE__ */ jsx26("span", { className: "hl-node-search-port hl-mono", children: view.connectPortId })
1624
+ ]
1625
+ },
1626
+ descriptor.code
1627
+ );
1628
+ })
1629
+ ] })
1630
+ ] })
1631
+ ] });
1632
+ }
1633
+ function highlight(text, match) {
1634
+ const hit = new Set(match.positions);
1635
+ const chars = Array.from(text);
1636
+ return chars.map(
1637
+ (ch, i) => hit.has(i) ? /* @__PURE__ */ jsx26("mark", { className: "hl-node-search-hit", children: ch }, i) : /* @__PURE__ */ jsx26("span", { children: ch }, i)
1638
+ );
1639
+ }
1640
+ function clampToViewport(x, y) {
1641
+ const maxLeft = Math.max(8, window.innerWidth - PANEL_WIDTH - 8);
1642
+ const maxTop = Math.max(8, window.innerHeight - PANEL_MAX_HEIGHT - 8);
1643
+ return {
1644
+ left: Math.min(Math.max(8, x), maxLeft),
1645
+ top: Math.min(Math.max(8, y), maxTop)
1646
+ };
1647
+ }
1648
+
1649
+ // src/command-palette/useNodeSearch.ts
1650
+ import { useCallback as useCallback6, useMemo as useMemo5, useState as useState5 } from "react";
1651
+ import { AddNodeCommand, ConnectCommand } from "@hlang-tech/editor-core";
1652
+
1653
+ // src/command-palette/fuzzy.ts
1654
+ function fuzzyScore(query, target) {
1655
+ if (query.length === 0) {
1656
+ return { score: 0, positions: [] };
1657
+ }
1658
+ const q = query.toLowerCase();
1659
+ const t = target.toLowerCase();
1660
+ const positions = [];
1661
+ let score = 0;
1662
+ let qi = 0;
1663
+ let prevMatch = -2;
1664
+ for (let ti = 0; ti < t.length && qi < q.length; ti += 1) {
1665
+ if (t[ti] !== q[qi]) {
1666
+ continue;
1667
+ }
1668
+ positions.push(ti);
1669
+ let point = 1;
1670
+ if (ti === prevMatch + 1) {
1671
+ point += 3;
1672
+ }
1673
+ if (ti === 0 || isBoundary(target, ti)) {
1674
+ point += 2;
1675
+ }
1676
+ point += Math.max(0, 2 - ti * 0.05);
1677
+ score += point;
1678
+ prevMatch = ti;
1679
+ qi += 1;
1680
+ }
1681
+ if (qi < q.length) {
1682
+ return null;
1683
+ }
1684
+ score -= target.length * 0.01;
1685
+ return { score, positions };
1686
+ }
1687
+ function isBoundary(target, index) {
1688
+ const prev = target[index - 1];
1689
+ const cur = target[index];
1690
+ if (prev === void 0 || cur === void 0) {
1691
+ return false;
1692
+ }
1693
+ const isSep = /[\s\-_./]/.test(prev);
1694
+ const isCamel = prev === prev.toLowerCase() && cur === cur.toUpperCase();
1695
+ return isSep || isCamel;
1696
+ }
1697
+ function fuzzyRank(query, candidates) {
1698
+ const results = [];
1699
+ for (const candidate of candidates) {
1700
+ const match = fuzzyScore(query, candidate.text);
1701
+ if (match !== null) {
1702
+ results.push({ value: candidate.value, text: candidate.text, match });
1703
+ }
1704
+ }
1705
+ if (query.length === 0) {
1706
+ return results;
1707
+ }
1708
+ return results.sort((a, b) => b.match.score - a.match.score);
1709
+ }
1710
+
1711
+ // src/command-palette/node-search-model.ts
1712
+ function descriptorCandidates(descriptors) {
1713
+ return descriptors.map((descriptor) => ({ text: descriptor.displayName, value: descriptor }));
1714
+ }
1715
+ function searchNodes(query, descriptors) {
1716
+ return fuzzyRank(query, descriptorCandidates(descriptors));
1717
+ }
1718
+ function connectableCandidates(query, descriptors, pending, validator) {
1719
+ const ranked = searchNodes(query, descriptors);
1720
+ const out = [];
1721
+ for (const result of ranked) {
1722
+ const port = firstCompletingPort(result.value.ports, pending, validator);
1723
+ if (port !== void 0) {
1724
+ out.push({ candidate: result, connectPortId: port });
1725
+ }
1726
+ }
1727
+ return out;
1728
+ }
1729
+ function firstCompletingPort(ports, pending, validator) {
1730
+ for (const port of ports) {
1731
+ const ok = pending.direction === "from-source" ? validator.canLink(pending.fromPort, port).ok : validator.canLink(port, pending.fromPort).ok;
1732
+ if (ok) {
1733
+ return port.id;
1734
+ }
1735
+ }
1736
+ return void 0;
1737
+ }
1738
+
1739
+ // src/command-palette/useNodeSearch.ts
1740
+ function useNodeSearch(core) {
1741
+ const [state, setState] = useState5(void 0);
1742
+ const openBlank = useCallback6((screen, flow) => {
1743
+ setState({ screen, flow });
1744
+ }, []);
1745
+ const openFromConnection = useCallback6(
1746
+ (screen, flow, pending) => {
1747
+ setState({ screen, flow, pending });
1748
+ },
1749
+ []
1750
+ );
1751
+ const close = useCallback6(() => {
1752
+ setState(void 0);
1753
+ }, []);
1754
+ const descriptors = core.registry.list();
1755
+ const results = useCallback6(
1756
+ (query) => {
1757
+ if ((state == null ? void 0 : state.pending) !== void 0) {
1758
+ return connectableCandidates(
1759
+ query,
1760
+ descriptors,
1761
+ state.pending,
1762
+ core.validator
1763
+ ).map((entry) => ({ result: entry.candidate, connectPortId: entry.connectPortId }));
1764
+ }
1765
+ return searchNodes(query, descriptors).map((result) => ({ result }));
1766
+ },
1767
+ [state, descriptors, core]
1768
+ );
1769
+ const commit = useCallback6(
1770
+ (view) => {
1771
+ if (state === void 0) {
1772
+ return;
1773
+ }
1774
+ const descriptor = view.result.value;
1775
+ const node = core.registry.createNode(descriptor.code, state.flow);
1776
+ if (state.pending !== void 0 && view.connectPortId !== void 0) {
1777
+ const pending = state.pending;
1778
+ const connectPortId = view.connectPortId;
1779
+ core.history.transact("Create & Connect Node", () => {
1780
+ core.commands.execute(new AddNodeCommand(node));
1781
+ const edgeId = core.idGenerator.nextEdgeId();
1782
+ const newEndpoint = { nodeId: node.id, portId: connectPortId };
1783
+ const [source, target] = pending.direction === "from-source" ? [pending.fromEndpoint, newEndpoint] : [newEndpoint, pending.fromEndpoint];
1784
+ core.commands.execute(new ConnectCommand(edgeId, source, target));
1785
+ });
1786
+ } else {
1787
+ core.commands.execute(new AddNodeCommand(node));
1788
+ }
1789
+ setState(void 0);
1790
+ },
1791
+ [state, core]
1792
+ );
1793
+ return useMemo5(
1794
+ () => ({ state, openBlank, openFromConnection, close, results, commit }),
1795
+ [state, openBlank, openFromConnection, close, results, commit]
1796
+ );
1797
+ }
1798
+
1799
+ // src/command-palette/useNodeSearchTriggers.ts
1800
+ import { useCallback as useCallback7, useRef as useRef4 } from "react";
1801
+ import { useReactFlow } from "@xyflow/react";
1802
+ var DEFAULT_DOUBLE_CLICK_MS = 350;
1803
+ function useNodeSearchTriggers(core, search, options = {}) {
1804
+ var _a, _b;
1805
+ const flow = useReactFlow();
1806
+ const doubleClickMs = (_a = options.doubleClickMs) != null ? _a : DEFAULT_DOUBLE_CLICK_MS;
1807
+ const now = (_b = options.now) != null ? _b : defaultNow;
1808
+ const startRef = useRef4(null);
1809
+ const lastPaneClickRef = useRef4(Number.NEGATIVE_INFINITY);
1810
+ const onConnectStart = useCallback7((_event, params) => {
1811
+ startRef.current = params;
1812
+ }, []);
1813
+ const onConnectEnd = useCallback7(
1814
+ (event, connectionState) => {
1815
+ const start = startRef.current;
1816
+ startRef.current = null;
1817
+ if (connectionState.toNode !== null || start === null) {
1818
+ return;
1819
+ }
1820
+ if (start.nodeId === null || start.handleId === null || start.handleType === null) {
1821
+ return;
1822
+ }
1823
+ const point = pointerOf(event);
1824
+ if (point === null) {
1825
+ return;
1826
+ }
1827
+ const fromPort = resolvePort(core, start.nodeId, start.handleId);
1828
+ if (fromPort === void 0) {
1829
+ return;
1830
+ }
1831
+ const pending = {
1832
+ fromEndpoint: { nodeId: start.nodeId, portId: start.handleId },
1833
+ fromPort,
1834
+ // A drag from a 'source' (output) handle needs a target node; a drag
1835
+ // from a 'target' (input) handle needs a source node.
1836
+ direction: start.handleType === "source" ? "from-source" : "from-target"
1837
+ };
1838
+ search.openFromConnection(
1839
+ point,
1840
+ flow.screenToFlowPosition(point),
1841
+ pending
1842
+ );
1843
+ },
1844
+ [core, flow, search]
1845
+ );
1846
+ const onPaneClick = useCallback7(
1847
+ (event) => {
1848
+ const t = now();
1849
+ const isDouble = t - lastPaneClickRef.current <= doubleClickMs;
1850
+ lastPaneClickRef.current = t;
1851
+ if (!isDouble) {
1852
+ return;
1853
+ }
1854
+ lastPaneClickRef.current = Number.NEGATIVE_INFINITY;
1855
+ const screen = { x: event.clientX, y: event.clientY };
1856
+ search.openBlank(screen, flow.screenToFlowPosition(screen));
1857
+ },
1858
+ [flow, search, doubleClickMs, now]
1859
+ );
1860
+ return { onConnectStart, onConnectEnd, onPaneClick };
1861
+ }
1862
+ function defaultNow() {
1863
+ return performance.now();
1864
+ }
1865
+ function resolvePort(core, nodeId, portId) {
1866
+ const node = core.model.getNode(nodeId);
1867
+ return node == null ? void 0 : node.ports.find((port) => port.id === portId);
1868
+ }
1869
+ function pointerOf(event) {
1870
+ if ("clientX" in event) {
1871
+ return { x: event.clientX, y: event.clientY };
1872
+ }
1873
+ const touch = event.changedTouches[0];
1874
+ return touch ? { x: touch.clientX, y: touch.clientY } : null;
1875
+ }
1876
+
1877
+ // src/hooks/useEditorHotkeys.ts
1878
+ import { useEffect as useEffect5 } from "react";
1879
+ import { useClipboard } from "@hlang-tech/editor-react";
1880
+ function isEditableTarget(target) {
1881
+ if (!(target instanceof HTMLElement)) {
1882
+ return false;
1883
+ }
1884
+ const tag = target.tagName;
1885
+ return tag === "INPUT" || tag === "TEXTAREA" || target.isContentEditable;
1886
+ }
1887
+ function useEditorHotkeys(core) {
1888
+ const clipboard = useClipboard(core);
1889
+ useEffect5(() => {
1890
+ const onKeyDown = (event) => {
1891
+ if (isEditableTarget(event.target)) {
1892
+ return;
1893
+ }
1894
+ const mod = event.metaKey || event.ctrlKey;
1895
+ if (mod && !event.altKey) {
1896
+ const key = event.key.toLowerCase();
1897
+ if (key === "z" && !event.shiftKey) {
1898
+ event.preventDefault();
1899
+ core.history.undo();
1900
+ return;
1901
+ }
1902
+ if (key === "z" && event.shiftKey || key === "y") {
1903
+ event.preventDefault();
1904
+ core.history.redo();
1905
+ return;
1906
+ }
1907
+ }
1908
+ if (mod && !event.shiftKey && !event.altKey) {
1909
+ const key = event.key.toLowerCase();
1910
+ if (key === "c") {
1911
+ event.preventDefault();
1912
+ clipboard.copy();
1913
+ return;
1914
+ }
1915
+ if (key === "x") {
1916
+ event.preventDefault();
1917
+ clipboard.cut();
1918
+ return;
1919
+ }
1920
+ if (key === "v") {
1921
+ event.preventDefault();
1922
+ clipboard.paste();
1923
+ return;
1924
+ }
1925
+ if (key === "d") {
1926
+ event.preventDefault();
1927
+ clipboard.duplicate();
1928
+ return;
1929
+ }
1930
+ if (key === "a") {
1931
+ event.preventDefault();
1932
+ core.selection.selectNodes(
1933
+ core.model.getNodes().map((n) => n.id),
1934
+ "replace"
1935
+ );
1936
+ return;
1937
+ }
1938
+ }
1939
+ if (event.key === "Delete" || event.key === "Backspace") {
1940
+ event.preventDefault();
1941
+ clipboard.remove();
1942
+ }
1943
+ };
1944
+ window.addEventListener("keydown", onKeyDown);
1945
+ return () => {
1946
+ window.removeEventListener("keydown", onKeyDown);
1947
+ };
1948
+ }, [core, clipboard]);
1949
+ }
1950
+
1951
+ // src/context-menu/NodeContextMenu.tsx
1952
+ import { useCallback as useCallback8, useEffect as useEffect6, useMemo as useMemo6, useRef as useRef5, useState as useState6 } from "react";
1953
+ import { Fragment as Fragment5, jsx as jsx27, jsxs as jsxs17 } from "react/jsx-runtime";
1954
+ var PANEL_WIDTH2 = 208;
1955
+ var ITEM_HEIGHT = 34;
1956
+ var VIEWPORT_MARGIN = 8;
1957
+ var MENU_VPAD = 12;
1958
+ function NodeContextMenu({ controller, items }) {
1959
+ const { state } = controller;
1960
+ const [active, setActive] = useState6(0);
1961
+ const menuRef = useRef5(null);
1962
+ const isOpen = state !== void 0;
1963
+ useEffect6(() => {
1964
+ var _a;
1965
+ if (isOpen) {
1966
+ setActive(0);
1967
+ (_a = menuRef.current) == null ? void 0 : _a.focus();
1968
+ }
1969
+ }, [isOpen]);
1970
+ const rows = useMemo6(
1971
+ () => state !== void 0 ? items(state.nodeId) : [],
1972
+ [items, state]
1973
+ );
1974
+ const enabledIndices = useMemo6(
1975
+ () => rows.map((r, i) => r.disabled ? -1 : i).filter((i) => i >= 0),
1976
+ [rows]
1977
+ );
1978
+ const move = useCallback8(
1979
+ (delta) => {
1980
+ if (enabledIndices.length === 0) {
1981
+ return;
1982
+ }
1983
+ setActive((current) => {
1984
+ var _a;
1985
+ const pos = enabledIndices.indexOf(current);
1986
+ const nextPos = pos === -1 ? 0 : (pos + delta + enabledIndices.length) % enabledIndices.length;
1987
+ return (_a = enabledIndices[nextPos]) != null ? _a : current;
1988
+ });
1989
+ },
1990
+ [enabledIndices]
1991
+ );
1992
+ const choose = useCallback8(
1993
+ (item) => {
1994
+ if (item === void 0 || item.disabled) {
1995
+ return;
1996
+ }
1997
+ item.onSelect();
1998
+ controller.close();
1999
+ },
2000
+ [controller]
2001
+ );
2002
+ const onKeyDown = useCallback8(
2003
+ (event) => {
2004
+ switch (event.key) {
2005
+ case "ArrowDown":
2006
+ event.preventDefault();
2007
+ move(1);
2008
+ break;
2009
+ case "ArrowUp":
2010
+ event.preventDefault();
2011
+ move(-1);
2012
+ break;
2013
+ case "Enter":
2014
+ event.preventDefault();
2015
+ choose(rows[active]);
2016
+ break;
2017
+ case "Escape":
2018
+ event.preventDefault();
2019
+ controller.close();
2020
+ break;
2021
+ default:
2022
+ break;
2023
+ }
2024
+ },
2025
+ [move, choose, rows, active, controller]
2026
+ );
2027
+ if (state === void 0) {
2028
+ return null;
2029
+ }
2030
+ const style = clampToViewport2(state.screen.x, state.screen.y, rows.length);
2031
+ return /* @__PURE__ */ jsxs17(Fragment5, { children: [
2032
+ /* @__PURE__ */ jsx27(
2033
+ "div",
2034
+ {
2035
+ className: "hl-ctx-scrim",
2036
+ onPointerDown: () => {
2037
+ controller.close();
2038
+ },
2039
+ onContextMenu: (e) => {
2040
+ e.preventDefault();
2041
+ controller.close();
2042
+ }
2043
+ }
2044
+ ),
2045
+ /* @__PURE__ */ jsx27(
2046
+ "div",
2047
+ {
2048
+ ref: menuRef,
2049
+ className: "hl-ctx-menu",
2050
+ style,
2051
+ role: "menu",
2052
+ "aria-label": "Node actions",
2053
+ tabIndex: -1,
2054
+ onKeyDown,
2055
+ children: rows.map((item, index) => /* @__PURE__ */ jsxs17(
2056
+ "button",
2057
+ {
2058
+ type: "button",
2059
+ role: "menuitem",
2060
+ disabled: item.disabled,
2061
+ className: "hl-ctx-item" + (item.danger === true ? " is-danger" : "") + (index === active ? " is-active" : ""),
2062
+ onPointerEnter: () => !item.disabled && setActive(index),
2063
+ onPointerDown: (e) => {
2064
+ e.preventDefault();
2065
+ choose(item);
2066
+ },
2067
+ children: [
2068
+ /* @__PURE__ */ jsx27("span", { className: "hl-ctx-item-icon", "aria-hidden": true, children: item.icon }),
2069
+ /* @__PURE__ */ jsx27("span", { className: "hl-ctx-item-label", children: item.label }),
2070
+ item.shortcut !== void 0 && /* @__PURE__ */ jsx27("span", { className: "hl-ctx-item-key hl-mono", children: item.shortcut })
2071
+ ]
2072
+ },
2073
+ item.id
2074
+ ))
2075
+ }
2076
+ )
2077
+ ] });
2078
+ }
2079
+ function clampToViewport2(x, y, rowCount) {
2080
+ const height = Math.max(ITEM_HEIGHT, rowCount * ITEM_HEIGHT + MENU_VPAD);
2081
+ const maxLeft = Math.max(VIEWPORT_MARGIN, window.innerWidth - PANEL_WIDTH2 - VIEWPORT_MARGIN);
2082
+ const maxTop = Math.max(VIEWPORT_MARGIN, window.innerHeight - height - VIEWPORT_MARGIN);
2083
+ return {
2084
+ left: Math.min(Math.max(VIEWPORT_MARGIN, x), maxLeft),
2085
+ top: Math.min(Math.max(VIEWPORT_MARGIN, y), maxTop)
2086
+ };
2087
+ }
2088
+
2089
+ // src/context-menu/useNodeContextMenu.ts
2090
+ import { useCallback as useCallback9, useMemo as useMemo7, useState as useState7 } from "react";
2091
+ function useNodeContextMenu() {
2092
+ const [state, setState] = useState7(void 0);
2093
+ const openForNode = useCallback9((screen, nodeId) => {
2094
+ setState({ screen, nodeId });
2095
+ }, []);
2096
+ const close = useCallback9(() => {
2097
+ setState(void 0);
2098
+ }, []);
2099
+ return useMemo7(() => ({ state, openForNode, close }), [state, openForNode, close]);
2100
+ }
2101
+
2102
+ // src/overlays/Cheatsheet.tsx
2103
+ import { useEffect as useEffect7, useRef as useRef6 } from "react";
2104
+ import { Fragment as Fragment6, jsx as jsx28, jsxs as jsxs18 } from "react/jsx-runtime";
2105
+ var DEFAULT_GROUPS = [
2106
+ {
2107
+ title: "Edit",
2108
+ rows: [
2109
+ { keys: "\u2318Z", label: "Undo" },
2110
+ { keys: "\u2318\u21E7Z / \u2318Y", label: "Redo" },
2111
+ { keys: "\u2318C", label: "Copy" },
2112
+ { keys: "\u2318X", label: "Cut" },
2113
+ { keys: "\u2318V", label: "Paste (at cursor)" },
2114
+ { keys: "\u2318D", label: "Duplicate" },
2115
+ { keys: "\u232B", label: "Delete" }
2116
+ ]
2117
+ },
2118
+ {
2119
+ title: "Select & navigate",
2120
+ rows: [
2121
+ { keys: "\u2318A", label: "Select all" },
2122
+ { keys: "Drag", label: "Box-select (left drag)" },
2123
+ { keys: "Middle / Right drag", label: "Pan" },
2124
+ { keys: "Space + drag", label: "Pan" },
2125
+ { keys: "Double-click name", label: "Rename node" }
2126
+ ]
2127
+ },
2128
+ {
2129
+ title: "Build",
2130
+ rows: [
2131
+ { keys: "Double-click canvas", label: "Search & create node" },
2132
+ { keys: "Drag port to empty", label: "Create & connect" },
2133
+ { keys: "Drop node on wire", label: "Insert inline (if compatible)" },
2134
+ { keys: "Drag edge end", label: "Reconnect" },
2135
+ { keys: "?", label: "This cheatsheet" }
2136
+ ]
2137
+ }
2138
+ ];
2139
+ function Cheatsheet({ open, onClose, groups = DEFAULT_GROUPS }) {
2140
+ const dialogRef = useRef6(null);
2141
+ useEffect7(() => {
2142
+ var _a;
2143
+ if (!open) {
2144
+ return;
2145
+ }
2146
+ (_a = dialogRef.current) == null ? void 0 : _a.focus();
2147
+ const onKey = (e) => {
2148
+ if (e.key === "Escape") {
2149
+ onClose();
2150
+ }
2151
+ };
2152
+ window.addEventListener("keydown", onKey);
2153
+ return () => window.removeEventListener("keydown", onKey);
2154
+ }, [open, onClose]);
2155
+ if (!open) {
2156
+ return null;
2157
+ }
2158
+ return /* @__PURE__ */ jsxs18(Fragment6, { children: [
2159
+ /* @__PURE__ */ jsx28("div", { className: "hl-cheat-scrim", onPointerDown: onClose }),
2160
+ /* @__PURE__ */ jsxs18(
2161
+ "div",
2162
+ {
2163
+ ref: dialogRef,
2164
+ className: "hl-cheat",
2165
+ role: "dialog",
2166
+ "aria-modal": "true",
2167
+ "aria-label": "Keyboard shortcuts",
2168
+ tabIndex: -1,
2169
+ children: [
2170
+ /* @__PURE__ */ jsxs18("div", { className: "hl-cheat-head", children: [
2171
+ /* @__PURE__ */ jsx28("span", { className: "hl-cheat-title", children: "Shortcuts" }),
2172
+ /* @__PURE__ */ jsx28("button", { className: "hl-cheat-close", onClick: onClose, "aria-label": "Close", children: "\u2715" })
2173
+ ] }),
2174
+ /* @__PURE__ */ jsx28("div", { className: "hl-cheat-grid", children: groups.map((group) => /* @__PURE__ */ jsxs18("section", { className: "hl-cheat-group", children: [
2175
+ /* @__PURE__ */ jsx28("h3", { className: "hl-cheat-group-title hl-mono", children: group.title }),
2176
+ /* @__PURE__ */ jsx28("ul", { className: "hl-cheat-rows", children: group.rows.map((row) => /* @__PURE__ */ jsxs18("li", { className: "hl-cheat-row", children: [
2177
+ /* @__PURE__ */ jsx28("span", { className: "hl-cheat-label", children: row.label }),
2178
+ /* @__PURE__ */ jsx28("kbd", { className: "hl-cheat-keys hl-mono", children: row.keys })
2179
+ ] }, row.label)) })
2180
+ ] }, group.title)) })
2181
+ ]
2182
+ }
2183
+ )
2184
+ ] });
2185
+ }
2186
+
2187
+ // src/overlays/Toast.tsx
2188
+ import { createContext as createContext3, useCallback as useCallback10, useContext as useContext3, useMemo as useMemo8, useRef as useRef7, useState as useState8 } from "react";
2189
+ import { jsx as jsx29, jsxs as jsxs19 } from "react/jsx-runtime";
2190
+ var ToastCtx = createContext3({ show: () => void 0 });
2191
+ function ToastProvider({ children, duration = 2200 }) {
2192
+ const [items, setItems] = useState8([]);
2193
+ const seq = useRef7(0);
2194
+ const show = useCallback10(
2195
+ (message) => {
2196
+ seq.current += 1;
2197
+ const id = seq.current;
2198
+ setItems((prev) => [...prev, { id, message }]);
2199
+ setTimeout(() => {
2200
+ setItems((prev) => prev.filter((t) => t.id !== id));
2201
+ }, duration);
2202
+ },
2203
+ [duration]
2204
+ );
2205
+ const api = useMemo8(() => ({ show }), [show]);
2206
+ return /* @__PURE__ */ jsxs19(ToastCtx.Provider, { value: api, children: [
2207
+ children,
2208
+ /* @__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)) })
2209
+ ] });
2210
+ }
2211
+ function useToast() {
2212
+ return useContext3(ToastCtx);
2213
+ }
2214
+
2215
+ // src/hooks/useLiveSignals.ts
2216
+ import { useEffect as useEffect8 } from "react";
2217
+ function useLiveSignals(core, handle, explicitStore) {
2218
+ const contextStore = useLiveSignalStore();
2219
+ const store = explicitStore != null ? explicitStore : contextStore;
2220
+ useEffect8(() => {
2221
+ if (store === null) {
2222
+ return;
2223
+ }
2224
+ const syncEdges = () => {
2225
+ const entries = core.model.getEdges().map((edge) => ({
2226
+ id: edge.id,
2227
+ sourceNodeId: edge.source.nodeId,
2228
+ sourcePortId: edge.source.portId,
2229
+ isError: edge.source.portId === "@node/error"
2230
+ }));
2231
+ store.setEdgeIndex(entries);
2232
+ };
2233
+ syncEdges();
2234
+ const added = core.events.on("edgeAdded", syncEdges);
2235
+ const removed = core.events.on("edgeRemoved", syncEdges);
2236
+ return () => {
2237
+ added.dispose();
2238
+ removed.dispose();
2239
+ };
2240
+ }, [core, store]);
2241
+ useEffect8(() => {
2242
+ if (store === null) {
2243
+ return;
2244
+ }
2245
+ if (handle === void 0) {
2246
+ store.reset();
2247
+ return;
2248
+ }
2249
+ const activitySub = handle.onActivity((e) => store.markActivity(e.nodeId, e.portId));
2250
+ const errorSub = handle.onError((e) => store.markError(e.nodeId));
2251
+ return () => {
2252
+ activitySub.dispose();
2253
+ errorSub.dispose();
2254
+ store.reset();
2255
+ };
2256
+ }, [store, handle]);
2257
+ }
2258
+ export {
2259
+ AssetGrid,
2260
+ AssetNodeChrome,
2261
+ CanvasChrome,
2262
+ CanvasDock,
2263
+ Cheatsheet,
2264
+ DiagnosticBadge,
2265
+ EditorShell,
2266
+ EditorUIThemeProvider,
2267
+ EmptyState,
2268
+ FlowEdge,
2269
+ HlangNode,
2270
+ InspectorPanel,
2271
+ KIND_ROLE,
2272
+ KIND_TOKEN,
2273
+ LiveSignalProvider,
2274
+ LiveSignalStore,
2275
+ NodeBodySlot,
2276
+ NodeChrome,
2277
+ NodeContextMenu,
2278
+ NodeInteractionProvider,
2279
+ NodeSearch,
2280
+ OutlineTree,
2281
+ ParamForm,
2282
+ PillNodeChrome,
2283
+ PortRow,
2284
+ PositionPanel,
2285
+ PreviewPanel,
2286
+ RailTabs,
2287
+ RunButton,
2288
+ SignalBar,
2289
+ TOKEN_NAMES,
2290
+ ToastProvider,
2291
+ ToolCluster,
2292
+ Toolbar,
2293
+ ToolbarGhostButton,
2294
+ ToolbarStat,
2295
+ UndoRedo,
2296
+ connectableCandidates,
2297
+ createHlangFlowTypes,
2298
+ createTheme,
2299
+ deriveParamSchema,
2300
+ descriptorCandidates,
2301
+ fuzzyRank,
2302
+ fuzzyScore,
2303
+ isEditableTarget,
2304
+ searchNodes,
2305
+ tokenVar,
2306
+ useEdgeFlowing,
2307
+ useEditorHotkeys,
2308
+ useLiveSignalStore,
2309
+ useLiveSignals,
2310
+ useNodeContextMenu,
2311
+ useNodeInteraction,
2312
+ useNodeLiveState,
2313
+ useNodeParams,
2314
+ useNodeSearch,
2315
+ useNodeSearchTriggers,
2316
+ useToast
2317
+ };
2318
+ //# sourceMappingURL=index.mjs.map