@3plate/graph-core 0.1.4 → 0.1.6

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/dist/index.cjs CHANGED
@@ -30,6 +30,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ FileSource: () => FileSource,
34
+ FileSystemSource: () => FileSystemSource,
35
+ Ingest: () => Ingest,
36
+ Playground: () => Playground,
37
+ WebSocketSource: () => WebSocketSource,
33
38
  default: () => index_default,
34
39
  graph: () => graph
35
40
  });
@@ -40,6 +45,70 @@ var import_immutable8 = require("immutable");
40
45
 
41
46
  // src/graph/node.ts
42
47
  var import_immutable = require("immutable");
48
+
49
+ // src/log.ts
50
+ var import_pino = __toESM(require("pino"), 1);
51
+ var resolvedLevel = typeof globalThis !== "undefined" && globalThis.__LOG_LEVEL || typeof process !== "undefined" && process.env?.LOG_LEVEL || "debug";
52
+ var browserOpts = { asObject: true };
53
+ browserOpts.transmit = {
54
+ level: resolvedLevel,
55
+ send: (level, log12) => {
56
+ try {
57
+ const endpoint = typeof globalThis !== "undefined" && globalThis.__LOG_INGEST_URL || typeof process !== "undefined" && process.env?.LOG_INGEST_URL || void 0;
58
+ if (!endpoint || typeof window === "undefined") {
59
+ try {
60
+ console.debug("[graph-core] transmit skipped", { endpoint, hasWindow: typeof window !== "undefined", level });
61
+ } catch {
62
+ }
63
+ return;
64
+ }
65
+ const line = JSON.stringify({ level, ...log12, ts: Date.now() }) + "\n";
66
+ try {
67
+ console.debug("[graph-core] transmit sending", { endpoint, level, bytes: line.length });
68
+ } catch {
69
+ }
70
+ fetch(endpoint, {
71
+ method: "POST",
72
+ mode: "no-cors",
73
+ credentials: "omit",
74
+ body: line,
75
+ keepalive: true
76
+ }).then(() => {
77
+ try {
78
+ console.debug("[graph-core] transmit fetch ok");
79
+ } catch {
80
+ }
81
+ }).catch((err) => {
82
+ try {
83
+ console.debug("[graph-core] transmit fetch error", err?.message || err);
84
+ } catch {
85
+ }
86
+ });
87
+ } catch (e) {
88
+ try {
89
+ console.debug("[graph-core] transmit error", e?.message || e);
90
+ } catch {
91
+ }
92
+ }
93
+ }
94
+ };
95
+ var base = (0, import_pino.default)({
96
+ level: resolvedLevel,
97
+ browser: browserOpts
98
+ });
99
+ function logger(module2) {
100
+ const child = base.child({ module: module2 });
101
+ return {
102
+ error: (msg, ...args) => child.error({ args }, msg),
103
+ warn: (msg, ...args) => child.warn({ args }, msg),
104
+ info: (msg, ...args) => child.info({ args }, msg),
105
+ debug: (msg, ...args) => child.debug({ args }, msg)
106
+ };
107
+ }
108
+ var log = logger("core");
109
+
110
+ // src/graph/node.ts
111
+ var log2 = logger("node");
43
112
  var defNodeData = {
44
113
  id: "",
45
114
  data: void 0,
@@ -48,7 +117,7 @@ var defNodeData = {
48
117
  text: void 0,
49
118
  type: void 0,
50
119
  render: void 0,
51
- ports: { in: null, out: null },
120
+ ports: {},
52
121
  aligned: {},
53
122
  edges: { in: (0, import_immutable.Set)(), out: (0, import_immutable.Set)() },
54
123
  segs: { in: (0, import_immutable.Set)(), out: (0, import_immutable.Set)() },
@@ -82,10 +151,7 @@ var Node = class _Node extends (0, import_immutable.Record)(defNodeData) {
82
151
  return this.isDummy ? this.id : _Node.key(this);
83
152
  }
84
153
  static key(node) {
85
- return `${node.id}:${node.version}`;
86
- }
87
- static isDummyId(nodeId) {
88
- return nodeId.startsWith(_Node.dummyPrefix);
154
+ return `k:${node.id}:${node.version}`;
89
155
  }
90
156
  static addNormal(g, data) {
91
157
  const layer = g.layerAt(0);
@@ -95,7 +161,9 @@ var Node = class _Node extends (0, import_immutable.Record)(defNodeData) {
95
161
  segs: { in: (0, import_immutable.Set)(), out: (0, import_immutable.Set)() },
96
162
  aligned: {},
97
163
  edgeIds: [],
98
- layerId: layer.id
164
+ layerId: layer.id,
165
+ lpos: void 0,
166
+ pos: void 0
99
167
  });
100
168
  layer.addNode(g, node.id);
101
169
  g.nodes.set(node.id, node);
@@ -158,6 +226,18 @@ var Node = class _Node extends (0, import_immutable.Record)(defNodeData) {
158
226
  getLayer(g) {
159
227
  return g.getLayer(this.layerId);
160
228
  }
229
+ margin(g) {
230
+ return this.isDummy ? g.options.edgeSpacing - g.options.dummyNodeSize : g.options.nodeMargin;
231
+ }
232
+ marginWith(g, other) {
233
+ return Math.max(this.margin(g), other.margin(g));
234
+ }
235
+ width(g) {
236
+ return this.dims?.[g.w] ?? 0;
237
+ }
238
+ right(g) {
239
+ return this.lpos + this.width(g);
240
+ }
161
241
  setIndex(g, index) {
162
242
  if (this.index == index) return this;
163
243
  return this.mut(g).set("index", index);
@@ -204,9 +284,22 @@ var Node = class _Node extends (0, import_immutable.Record)(defNodeData) {
204
284
  return node;
205
285
  }
206
286
  delSelf(g) {
287
+ if (this.aligned.in)
288
+ g.getNode(this.aligned.in).setAligned(g, "out", void 0);
289
+ if (this.aligned.out)
290
+ g.getNode(this.aligned.out).setAligned(g, "in", void 0);
207
291
  this.getLayer(g).delNode(g, this.id);
208
- for (const rel of this.rels(g))
209
- rel.delSelf(g);
292
+ for (const edge of this.rels(g, "edges", "both"))
293
+ edge.delSelf(g);
294
+ const remainingSegIds = [...this.segs.in, ...this.segs.out];
295
+ if (remainingSegIds.length > 0) {
296
+ for (const segId of remainingSegIds) {
297
+ if (g.segs?.has?.(segId)) {
298
+ g.getSeg(segId).delSelf(g);
299
+ } else {
300
+ }
301
+ }
302
+ }
210
303
  g.nodes.delete(this.id);
211
304
  g.dirtyNodes.delete(this.id);
212
305
  g.delNodes.add(this.id);
@@ -299,30 +392,7 @@ var Node = class _Node extends (0, import_immutable.Record)(defNodeData) {
299
392
 
300
393
  // src/graph/edge.ts
301
394
  var import_immutable2 = require("immutable");
302
-
303
- // src/log.ts
304
- var levels = {
305
- error: 0,
306
- warn: 1,
307
- info: 2,
308
- debug: 3
309
- };
310
- var currentLevel = "debug";
311
- function shouldLog(level) {
312
- return levels[level] <= levels[currentLevel];
313
- }
314
- function logger(module2) {
315
- return {
316
- error: (msg, ...args) => shouldLog("error") && console.error(`[${module2}] ${msg}`, ...args),
317
- warn: (msg, ...args) => shouldLog("warn") && console.warn(`[${module2}] ${msg}`, ...args),
318
- info: (msg, ...args) => shouldLog("info") && console.info(`[${module2}] ${msg}`, ...args),
319
- debug: (msg, ...args) => shouldLog("debug") && console.debug(`[${module2}] ${msg}`, ...args)
320
- };
321
- }
322
- var log = logger("core");
323
-
324
- // src/graph/edge.ts
325
- var log2 = logger("edge");
395
+ var log3 = logger("edge");
326
396
  var defEdgeData = {
327
397
  id: "",
328
398
  data: null,
@@ -330,7 +400,6 @@ var defEdgeData = {
330
400
  source: { id: "" },
331
401
  target: { id: "" },
332
402
  type: void 0,
333
- style: void 0,
334
403
  mutable: false,
335
404
  segIds: []
336
405
  };
@@ -411,7 +480,7 @@ var Edge = class _Edge extends (0, import_immutable2.Record)(defEdgeData) {
411
480
  source = edge.source.id;
412
481
  if (edge.source?.port)
413
482
  source = `${source}.${edge.source.port}`;
414
- const marker = edge.source?.marker ?? edge.style?.marker?.source;
483
+ const marker = edge.source?.marker;
415
484
  if (marker && marker != "none") source += `[${marker}]`;
416
485
  source += "-";
417
486
  }
@@ -421,7 +490,7 @@ var Edge = class _Edge extends (0, import_immutable2.Record)(defEdgeData) {
421
490
  if (edge.target.port)
422
491
  target = `${target}.${edge.target.port}`;
423
492
  target = "-" + target;
424
- const marker = edge.target?.marker ?? edge.style?.marker?.target ?? "arrow";
493
+ const marker = edge.target?.marker ?? "arrow";
425
494
  if (marker && marker != "none") target += `[${marker}]`;
426
495
  }
427
496
  const type = edge.type || "";
@@ -463,7 +532,6 @@ var defSegData = {
463
532
  source: { id: "" },
464
533
  target: { id: "" },
465
534
  type: void 0,
466
- style: void 0,
467
535
  edgeIds: (0, import_immutable3.Set)(),
468
536
  trackPos: void 0,
469
537
  svg: void 0,
@@ -494,7 +562,7 @@ var Seg = class _Seg extends (0, import_immutable3.Record)(defSegData) {
494
562
  sameEnd(other, side) {
495
563
  const mine = this[side];
496
564
  const yours = other[side];
497
- return mine.id === yours.id && mine.port === yours.port && mine.marker === yours.marker;
565
+ return mine.id === yours.id && mine.port === yours.port && mine.marker === yours.marker && this.type === other.type;
498
566
  }
499
567
  setPos(g, source, target) {
500
568
  return this.mut(g).merge({
@@ -566,7 +634,7 @@ var Seg = class _Seg extends (0, import_immutable3.Record)(defSegData) {
566
634
 
567
635
  // src/graph/layer.ts
568
636
  var import_immutable4 = require("immutable");
569
- var log3 = logger("layer");
637
+ var log4 = logger("layer");
570
638
  var defLayerData = {
571
639
  id: "",
572
640
  index: 0,
@@ -591,7 +659,7 @@ var Layer = class extends (0, import_immutable4.Record)(defLayerData) {
591
659
  mutable: false
592
660
  }).asImmutable();
593
661
  }
594
- get size() {
662
+ get nodeCount() {
595
663
  return this.nodeIds.size;
596
664
  }
597
665
  *nodes(g) {
@@ -646,9 +714,8 @@ var Layer = class extends (0, import_immutable4.Record)(defLayerData) {
646
714
  reindex(g, nodeId) {
647
715
  if (!this.isSorted) return void 0;
648
716
  const sorted = this.sorted.filter((id) => id != nodeId);
649
- const idx = this.sorted.findIndex((id) => id == nodeId);
650
- for (let i = idx; i < sorted.length; i++)
651
- g.getNode(sorted[i]).setIndex(g, i);
717
+ for (const [i, id] of this.sorted.entries())
718
+ g.getNode(id).setIndex(g, i);
652
719
  return sorted;
653
720
  }
654
721
  delNode(g, nodeId) {
@@ -820,12 +887,12 @@ var Cycles = class _Cycles {
820
887
 
821
888
  // src/graph/services/dummy.ts
822
889
  var import_immutable5 = require("immutable");
823
- var log4 = logger("dummy");
890
+ var log5 = logger("dummy");
824
891
  var Dummy = class _Dummy {
825
892
  static updateDummies(g) {
826
893
  for (const edgeId of g.dirtyEdges) {
827
894
  const edge = g.getEdge(edgeId);
828
- const { type, style } = edge;
895
+ const { type } = edge;
829
896
  const sourceLayer = edge.sourceNode(g).layerIndex(g);
830
897
  const targetLayer = edge.targetNode(g).layerIndex(g);
831
898
  let segIndex = 0;
@@ -849,7 +916,7 @@ var Dummy = class _Dummy {
849
916
  });
850
917
  target = { id: dummy.id };
851
918
  }
852
- seg = Seg.add(g, { source, target, type, style, edgeIds: (0, import_immutable5.Set)([edgeId]) });
919
+ seg = Seg.add(g, { source, target, type, edgeIds: (0, import_immutable5.Set)([edgeId]) });
853
920
  segs.splice(segIndex, 0, seg.id);
854
921
  changed = true;
855
922
  } else if (segLayer < layerIndex || seg.source.id != source.id || seg.source.port != source.port || layerIndex == targetLayer && (seg.target.id != edge.target.id || seg.target.port != edge.target.port)) {
@@ -888,9 +955,8 @@ var Dummy = class _Dummy {
888
955
  let layer = g.getLayer(layerId);
889
956
  const groups = /* @__PURE__ */ new Map();
890
957
  for (const nodeId of layer.nodeIds) {
891
- if (!Node.isDummyId(nodeId)) continue;
892
958
  const node = g.getNode(nodeId);
893
- if (node.isMerged) continue;
959
+ if (!node.isDummy || node.isMerged) continue;
894
960
  const edge = g.getEdge(node.edgeIds[0]);
895
961
  const key = Edge.key(edge, "k:", side);
896
962
  if (!groups.has(key)) groups.set(key, /* @__PURE__ */ new Set());
@@ -898,7 +964,7 @@ var Dummy = class _Dummy {
898
964
  }
899
965
  for (const [key, group] of groups) {
900
966
  if (group.size == 1) continue;
901
- const edgeIds = [...group].map((node) => node.edgeId);
967
+ const edgeIds = [...group].map((node) => node.edgeIds[0]);
902
968
  const dummy = Node.addDummy(g, { edgeIds, layerId, isMerged: true });
903
969
  let seg;
904
970
  for (const old of group) {
@@ -908,8 +974,9 @@ var Dummy = class _Dummy {
908
974
  const example = g.getSeg(segId);
909
975
  seg = Seg.add(g, {
910
976
  ...example,
911
- edgeIds: (0, import_immutable5.Set)([old.edgeId]),
912
- [altSide]: { ...example[altSide], id: dummy.id }
977
+ edgeIds: (0, import_immutable5.Set)(edgeIds),
978
+ [side]: { ...example[side] },
979
+ [altSide]: { ...example[altSide], id: dummy.id, port: void 0 }
913
980
  });
914
981
  }
915
982
  edge = edge.replaceSegId(g, segId, seg.id);
@@ -921,12 +988,15 @@ var Dummy = class _Dummy {
921
988
  const example = g.getSeg(segId);
922
989
  const seg2 = Seg.add(g, {
923
990
  ...example,
924
- edgeIds: (0, import_immutable5.Set)([old.edgeId]),
925
- [side]: { ...example[side], id: dummy.id }
991
+ edgeIds: (0, import_immutable5.Set)([old.edgeIds[0]]),
992
+ [altSide]: { ...example[altSide] },
993
+ [side]: { ...example[side], id: dummy.id, port: void 0 }
926
994
  });
927
995
  edge = edge.replaceSegId(g, segId, seg2.id);
928
996
  }
929
997
  }
998
+ for (const old of group)
999
+ old.delSelf(g);
930
1000
  }
931
1001
  }
932
1002
  }
@@ -934,7 +1004,7 @@ var Dummy = class _Dummy {
934
1004
 
935
1005
  // src/graph/services/layers.ts
936
1006
  var import_immutable6 = require("immutable");
937
- var log5 = logger("layers");
1007
+ var log6 = logger("layers");
938
1008
  var Layers = class {
939
1009
  static updateLayers(g) {
940
1010
  const stack = [...g.dirtyNodes].map((id) => g.getNode(id)).filter((node) => !node.isDummy).sort((a, b) => b.layerIndex(g) - a.layerIndex(g)).map((node) => node.id);
@@ -997,7 +1067,7 @@ var Layers = class {
997
1067
 
998
1068
  // src/graph/services/layout.ts
999
1069
  var import_immutable7 = require("immutable");
1000
- var log6 = logger("layout");
1070
+ var log7 = logger("layout");
1001
1071
  var Layout = class _Layout {
1002
1072
  static parentIndex(g, node) {
1003
1073
  const parents = (0, import_immutable7.Seq)([...node.adjs(g, "segs", "in")]);
@@ -1014,8 +1084,8 @@ var Layout = class _Layout {
1014
1084
  if (a.isDummy && !b.isDummy) return -1;
1015
1085
  if (!a.isDummy && b.isDummy) return 1;
1016
1086
  if (!a.isDummy) return a.id.localeCompare(b.id);
1017
- const minA = a.edgeId ?? (0, import_immutable7.Seq)(a.edgeIds).min();
1018
- const minB = b.edgeId ?? (0, import_immutable7.Seq)(b.edgeIds).min();
1087
+ const minA = (0, import_immutable7.Seq)(a.edgeIds).min();
1088
+ const minB = (0, import_immutable7.Seq)(b.edgeIds).min();
1019
1089
  return minA.localeCompare(minB);
1020
1090
  }
1021
1091
  static positionNodes(g) {
@@ -1041,7 +1111,12 @@ var Layout = class _Layout {
1041
1111
  let node = g.getNode(sorted[i]);
1042
1112
  node = node.setIndex(g, i).setLayerPos(g, lpos);
1043
1113
  const size = node.dims?.[g.w] ?? 0;
1044
- lpos += size + g.options.nodeMargin;
1114
+ let margin = node.margin(g);
1115
+ if (i + 1 < sorted.length) {
1116
+ const next = g.getNode(sorted[i + 1]);
1117
+ margin = node.marginWith(g, next);
1118
+ }
1119
+ lpos += size + margin;
1045
1120
  }
1046
1121
  }
1047
1122
  }
@@ -1073,7 +1148,7 @@ var Layout = class _Layout {
1073
1148
  let iterations = 0;
1074
1149
  while (true) {
1075
1150
  if (++iterations > 10) {
1076
- log6.error(`alignNodes: infinite loop detected in layer ${layerId}`);
1151
+ log7.error(`alignNodes: infinite loop detected in layer ${layerId}`);
1077
1152
  break;
1078
1153
  }
1079
1154
  let changed = false;
@@ -1133,7 +1208,7 @@ var Layout = class _Layout {
1133
1208
  static anchorPos(g, seg, side) {
1134
1209
  const nodeId = seg[side].id;
1135
1210
  const node = g.getNode(nodeId);
1136
- let p = { x: 0, y: 0, [g.x]: node.lpos, ...node.pos || {} };
1211
+ let p = { [g.x]: node.lpos, [g.y]: node.pos?.[g.y] ?? 0 };
1137
1212
  let w = node.dims?.[g.w] ?? 0;
1138
1213
  let h = node.dims?.[g.h] ?? 0;
1139
1214
  if (node.isDummy)
@@ -1179,22 +1254,18 @@ var Layout = class _Layout {
1179
1254
  }
1180
1255
  static shiftNode(g, nodeId, alignId, dir, lpos, reverseMove, conservative) {
1181
1256
  const node = g.getNode(nodeId);
1182
- log6.debug(`shift ${nodeId} (at ${node.lpos}) to ${alignId} (at ${lpos})`);
1183
1257
  if (!conservative)
1184
1258
  _Layout.markAligned(g, nodeId, alignId, dir, lpos);
1185
- const space = g.options.nodeMargin;
1186
- const nodeWidth = node.dims?.[g.w] ?? 0;
1187
- const aMin = lpos - space, aMax = lpos + nodeWidth + space;
1259
+ const nodeRight = lpos + node.width(g);
1188
1260
  repeat:
1189
1261
  for (const otherId of node.getLayer(g).nodeIds) {
1190
1262
  if (otherId == nodeId) continue;
1191
1263
  const other = g.getNode(otherId);
1192
- const opos = other.lpos;
1193
- const otherWidth = other.dims?.[g.w] ?? 0;
1194
- const bMin = opos, bMax = opos + otherWidth;
1195
- if (aMin < bMax && bMin < aMax) {
1264
+ const margin = node.marginWith(g, other);
1265
+ const gap = lpos < other.lpos ? other.lpos - nodeRight : lpos - other.right(g);
1266
+ if (gap < margin) {
1196
1267
  if (conservative) return false;
1197
- const safePos = reverseMove ? aMin - otherWidth : aMax;
1268
+ const safePos = reverseMove ? lpos - other.width(g) - margin : nodeRight + margin;
1198
1269
  _Layout.shiftNode(g, otherId, void 0, dir, safePos, reverseMove, conservative);
1199
1270
  continue repeat;
1200
1271
  }
@@ -1243,11 +1314,12 @@ var Layout = class _Layout {
1243
1314
  for (const layerId of g.layerList) {
1244
1315
  const layer = g.getLayer(layerId);
1245
1316
  if (layer.sorted.length < 2) continue;
1246
- for (const nodeId of layer.sorted) {
1317
+ for (const [i, nodeId] of layer.sorted.entries()) {
1247
1318
  const node = g.getNode(nodeId);
1248
1319
  if (node.index == 0) continue;
1249
1320
  let minGap = Infinity;
1250
1321
  const stack = [];
1322
+ let maxMargin = 0;
1251
1323
  for (const right of _Layout.aligned(g, nodeId, "both")) {
1252
1324
  stack.push(right);
1253
1325
  const leftId = _Layout.leftOf(g, right);
@@ -1256,8 +1328,10 @@ var Layout = class _Layout {
1256
1328
  const leftWidth = left.dims?.[g.w] ?? 0;
1257
1329
  const gap = right.lpos - left.lpos - leftWidth;
1258
1330
  if (gap < minGap) minGap = gap;
1331
+ const margin = right.marginWith(g, left);
1332
+ if (margin > maxMargin) maxMargin = margin;
1259
1333
  }
1260
- const delta = minGap - g.options.nodeMargin;
1334
+ const delta = minGap - maxMargin;
1261
1335
  if (delta <= 0) continue;
1262
1336
  anyChanged = true;
1263
1337
  for (const right of stack)
@@ -1305,9 +1379,8 @@ var Layout = class _Layout {
1305
1379
  };
1306
1380
 
1307
1381
  // src/canvas/marker.tsx
1308
- var import_marker = __toESM(require("./marker.css?raw"), 1);
1309
1382
  var import_jsx_runtime = require("jsx-dom/jsx-runtime");
1310
- function arrow(size, classPrefix, reverse = false) {
1383
+ function arrow(size, reverse = false) {
1311
1384
  const h = size / 1.5;
1312
1385
  const w = size;
1313
1386
  const ry = h / 2;
@@ -1316,7 +1389,7 @@ function arrow(size, classPrefix, reverse = false) {
1316
1389
  "marker",
1317
1390
  {
1318
1391
  id: `g3p-marker-arrow${suffix}`,
1319
- className: `${classPrefix}-marker ${classPrefix}-marker-arrow`,
1392
+ className: "g3p-marker g3p-marker-arrow",
1320
1393
  markerWidth: size,
1321
1394
  markerHeight: size,
1322
1395
  refX: "2",
@@ -1327,7 +1400,7 @@ function arrow(size, classPrefix, reverse = false) {
1327
1400
  }
1328
1401
  );
1329
1402
  }
1330
- function circle(size, classPrefix, reverse = false) {
1403
+ function circle(size, reverse = false) {
1331
1404
  const r = size / 3;
1332
1405
  const cy = size / 2;
1333
1406
  const suffix = reverse ? "-reverse" : "";
@@ -1335,7 +1408,7 @@ function circle(size, classPrefix, reverse = false) {
1335
1408
  "marker",
1336
1409
  {
1337
1410
  id: `g3p-marker-circle${suffix}`,
1338
- className: `${classPrefix}-marker ${classPrefix}-marker-circle`,
1411
+ className: "g3p-marker g3p-marker-circle",
1339
1412
  markerWidth: size,
1340
1413
  markerHeight: size,
1341
1414
  refX: "2",
@@ -1346,7 +1419,7 @@ function circle(size, classPrefix, reverse = false) {
1346
1419
  }
1347
1420
  );
1348
1421
  }
1349
- function diamond(size, classPrefix, reverse = false) {
1422
+ function diamond(size, reverse = false) {
1350
1423
  const w = size * 0.7;
1351
1424
  const h = size / 2;
1352
1425
  const cy = size / 2;
@@ -1355,7 +1428,7 @@ function diamond(size, classPrefix, reverse = false) {
1355
1428
  "marker",
1356
1429
  {
1357
1430
  id: `g3p-marker-diamond${suffix}`,
1358
- className: `${classPrefix}-marker ${classPrefix}-marker-diamond`,
1431
+ className: "g3p-marker g3p-marker-diamond",
1359
1432
  markerWidth: size,
1360
1433
  markerHeight: size,
1361
1434
  refX: "2",
@@ -1366,7 +1439,7 @@ function diamond(size, classPrefix, reverse = false) {
1366
1439
  }
1367
1440
  );
1368
1441
  }
1369
- function bar(size, classPrefix, reverse = false) {
1442
+ function bar(size, reverse = false) {
1370
1443
  const h = size * 0.6;
1371
1444
  const cy = size / 2;
1372
1445
  const suffix = reverse ? "-reverse" : "";
@@ -1374,7 +1447,7 @@ function bar(size, classPrefix, reverse = false) {
1374
1447
  "marker",
1375
1448
  {
1376
1449
  id: `g3p-marker-bar${suffix}`,
1377
- className: `${classPrefix}-marker ${classPrefix}-marker-bar`,
1450
+ className: "g3p-marker g3p-marker-bar",
1378
1451
  markerWidth: size,
1379
1452
  markerHeight: size,
1380
1453
  refX: "2",
@@ -1385,7 +1458,7 @@ function bar(size, classPrefix, reverse = false) {
1385
1458
  }
1386
1459
  );
1387
1460
  }
1388
- function none(size, classPrefix, reverse = false) {
1461
+ function none(size, reverse = false) {
1389
1462
  return void 0;
1390
1463
  }
1391
1464
  function normalize(data) {
@@ -1404,7 +1477,7 @@ var markerDefs = {
1404
1477
  };
1405
1478
 
1406
1479
  // src/graph/services/lines.ts
1407
- var log7 = logger("lines");
1480
+ var log8 = logger("lines");
1408
1481
  var Lines = class _Lines {
1409
1482
  static layoutSeg(g, seg) {
1410
1483
  const sourcePos = Layout.anchorPos(g, seg, "source");
@@ -1450,7 +1523,11 @@ var Lines = class _Lines {
1450
1523
  validTrack = track;
1451
1524
  break;
1452
1525
  }
1453
- if (other.p1 < seg.p2 && seg.p1 < other.p2) {
1526
+ const minA = Math.min(seg.p1, seg.p2);
1527
+ const maxA = Math.max(seg.p1, seg.p2);
1528
+ const minB = Math.min(other.p1, other.p2);
1529
+ const maxB = Math.max(other.p1, other.p2);
1530
+ if (minA < maxB && minB < maxA) {
1454
1531
  overlap = true;
1455
1532
  break;
1456
1533
  }
@@ -1465,6 +1542,21 @@ var Lines = class _Lines {
1465
1542
  else
1466
1543
  trackSet.push([seg]);
1467
1544
  }
1545
+ const midpoint = (s) => (s.p1 + s.p2) / 2;
1546
+ const sortTracks = (tracks2, goingRight) => {
1547
+ tracks2.sort((a, b) => {
1548
+ const midA = Math.max(...a.map(midpoint));
1549
+ const midB = Math.max(...b.map(midpoint));
1550
+ return goingRight ? midB - midA : midA - midB;
1551
+ });
1552
+ };
1553
+ sortTracks(rightTracks, true);
1554
+ sortTracks(leftTracks, false);
1555
+ allTracks.sort((a, b) => {
1556
+ const avgA = a.reduce((sum, s) => sum + midpoint(s), 0) / a.length;
1557
+ const avgB = b.reduce((sum, s) => sum + midpoint(s), 0) / b.length;
1558
+ return avgB - avgA;
1559
+ });
1468
1560
  const tracks = [];
1469
1561
  const all = leftTracks.concat(rightTracks).concat(allTracks);
1470
1562
  for (const track of all)
@@ -1635,6 +1727,7 @@ var Lines = class _Lines {
1635
1727
  };
1636
1728
 
1637
1729
  // src/graph/graph.ts
1730
+ var log9 = logger("graph");
1638
1731
  var emptyChanges = {
1639
1732
  addedNodes: [],
1640
1733
  removedNodes: [],
@@ -1909,29 +2002,8 @@ var screenPos = (x, y) => ({ x, y });
1909
2002
  var canvasPos = (x, y) => ({ x, y });
1910
2003
  var graphPos = (x, y) => ({ x, y });
1911
2004
 
1912
- // src/canvas/node.tsx
1913
- var import_node3 = __toESM(require("./node.css?raw"), 1);
1914
-
1915
- // src/canvas/styler.ts
1916
- var injected = {};
1917
- function styler(name, styles4, prefix) {
1918
- if (prefix === "g3p" && !injected[name]) {
1919
- const style = document.createElement("style");
1920
- style.textContent = styles4;
1921
- document.head.appendChild(style);
1922
- injected[name] = true;
1923
- }
1924
- return (str, condition) => {
1925
- if (!(condition ?? true)) return "";
1926
- const parts = str.split(/\s+/);
1927
- const fixed = parts.map((p) => `${prefix}-${name}-${p}`);
1928
- return fixed.join(" ");
1929
- };
1930
- }
1931
-
1932
2005
  // src/canvas/node.tsx
1933
2006
  var import_jsx_runtime2 = require("jsx-dom/jsx-runtime");
1934
- var log8 = logger("canvas");
1935
2007
  var Node2 = class {
1936
2008
  selected;
1937
2009
  hovered;
@@ -1939,7 +2011,6 @@ var Node2 = class {
1939
2011
  content;
1940
2012
  canvas;
1941
2013
  data;
1942
- classPrefix;
1943
2014
  isDummy;
1944
2015
  pos;
1945
2016
  constructor(canvas, data, isDummy = false) {
@@ -1947,20 +2018,18 @@ var Node2 = class {
1947
2018
  this.data = data;
1948
2019
  this.selected = false;
1949
2020
  this.hovered = false;
1950
- this.classPrefix = canvas.classPrefix;
1951
2021
  this.isDummy = isDummy;
1952
2022
  if (this.isDummy) {
1953
2023
  const size = canvas.dummyNodeSize;
1954
2024
  } else {
1955
2025
  const render = data.render ?? canvas.renderNode;
1956
- this.content = this.renderContent(render(data.data));
2026
+ this.content = this.renderContent(render(data.data, data));
1957
2027
  }
1958
2028
  }
1959
2029
  remove() {
1960
2030
  this.container.remove();
1961
2031
  }
1962
2032
  append() {
1963
- console.log("append", this);
1964
2033
  this.canvas.group.appendChild(this.container);
1965
2034
  }
1966
2035
  needsContentSize() {
@@ -1969,19 +2038,6 @@ var Node2 = class {
1969
2038
  needsContainerSize() {
1970
2039
  return !this.isDummy;
1971
2040
  }
1972
- handleClick(e) {
1973
- e.stopPropagation();
1974
- }
1975
- handleMouseEnter(e) {
1976
- }
1977
- handleMouseLeave(e) {
1978
- }
1979
- handleContextMenu(e) {
1980
- }
1981
- handleMouseDown(e) {
1982
- }
1983
- handleMouseUp(e) {
1984
- }
1985
2041
  setPos(pos) {
1986
2042
  this.pos = pos;
1987
2043
  const { x, y } = pos;
@@ -1998,7 +2054,6 @@ var Node2 = class {
1998
2054
  return el;
1999
2055
  }
2000
2056
  renderContainer() {
2001
- const c = styler("node", import_node3.default, this.classPrefix);
2002
2057
  const hasPorts = this.hasPorts();
2003
2058
  const inner = this.isDummy ? this.renderDummy() : this.renderForeign();
2004
2059
  const nodeType = this.data?.type;
@@ -2006,14 +2061,8 @@ var Node2 = class {
2006
2061
  this.container = /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2007
2062
  "g",
2008
2063
  {
2009
- className: `${c("container")} ${c("dummy", this.isDummy)} ${typeClass}`.trim(),
2010
- onClick: (e) => this.handleClick(e),
2011
- onMouseEnter: (e) => this.handleMouseEnter(e),
2012
- onMouseLeave: (e) => this.handleMouseLeave(e),
2013
- onContextMenu: (e) => this.handleContextMenu(e),
2014
- onMouseDown: (e) => this.handleMouseDown(e),
2015
- onMouseUp: (e) => this.handleMouseUp(e),
2016
- style: { cursor: "pointer" },
2064
+ className: `g3p-node-container ${this.isDummy ? "g3p-node-dummy" : ""} ${typeClass}`.trim(),
2065
+ "data-node-id": this.data?.id,
2017
2066
  children: inner
2018
2067
  }
2019
2068
  );
@@ -2023,7 +2072,6 @@ var Node2 = class {
2023
2072
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("foreignObject", { width: w, height: h, children: this.content });
2024
2073
  }
2025
2074
  renderDummy() {
2026
- const c = styler("node", import_node3.default, this.classPrefix);
2027
2075
  let w = this.canvas.dummyNodeSize;
2028
2076
  let h = this.canvas.dummyNodeSize;
2029
2077
  w /= 2;
@@ -2036,7 +2084,7 @@ var Node2 = class {
2036
2084
  cy: h,
2037
2085
  rx: w,
2038
2086
  ry: h,
2039
- className: c("background")
2087
+ className: "g3p-node-background"
2040
2088
  }
2041
2089
  ),
2042
2090
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
@@ -2047,7 +2095,7 @@ var Node2 = class {
2047
2095
  rx: w,
2048
2096
  ry: h,
2049
2097
  fill: "none",
2050
- className: c("border")
2098
+ className: "g3p-node-border"
2051
2099
  }
2052
2100
  )
2053
2101
  ] });
@@ -2060,7 +2108,7 @@ var Node2 = class {
2060
2108
  const ports = data.ports?.[dir];
2061
2109
  if (!ports) continue;
2062
2110
  for (const port of ports) {
2063
- const el = this.content.querySelector(`#g3p-port-${data.id}-${port.id}`);
2111
+ const el = this.content.querySelector(`.g3p-node-port[data-node-id="${data.id}"][data-port-id="${port.id}"]`);
2064
2112
  if (!el) continue;
2065
2113
  const portRect = el.getBoundingClientRect();
2066
2114
  if (isVertical) {
@@ -2098,23 +2146,22 @@ var Node2 = class {
2098
2146
  renderPortRow(dir, inout) {
2099
2147
  const ports = this.data?.ports?.[dir];
2100
2148
  if (!ports?.length) return null;
2101
- const c = styler("node", import_node3.default, this.classPrefix);
2102
2149
  const pos = this.getPortPosition(dir);
2103
2150
  const isVertical = this.isVerticalOrientation();
2104
2151
  const layoutClass = isVertical ? "row" : "col";
2105
2152
  const rotateLabels = false;
2106
2153
  const rotateClass = rotateLabels ? `port-rotated-${pos}` : "";
2107
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: `${c("ports")} ${c(`ports-${layoutClass}`)}`, children: ports.map((port) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2154
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: `g3p-node-ports g3p-node-ports-${layoutClass}`, children: ports.map((port) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2108
2155
  "div",
2109
2156
  {
2110
- id: `g3p-port-${this.data.id}-${port.id}`,
2111
- className: `${c("port")} ${c(`port-${inout}-${pos}`)} ${c(rotateClass)}`,
2157
+ className: `g3p-node-port g3p-node-port-${inout}-${pos} ${rotateClass}`,
2158
+ "data-node-id": this.data.id,
2159
+ "data-port-id": port.id,
2112
2160
  children: port.label ?? port.id
2113
2161
  }
2114
2162
  )) });
2115
2163
  }
2116
2164
  renderInsidePorts(el) {
2117
- const c = styler("node", import_node3.default, this.classPrefix);
2118
2165
  const isVertical = this.isVerticalOrientation();
2119
2166
  const isReversed = this.isReversedOrientation();
2120
2167
  let inPorts = this.renderPortRow("in", "in");
@@ -2122,14 +2169,13 @@ var Node2 = class {
2122
2169
  if (!inPorts && !outPorts) return el;
2123
2170
  if (isReversed) [inPorts, outPorts] = [outPorts, inPorts];
2124
2171
  const wrapperClass = isVertical ? "v" : "h";
2125
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `${c("with-ports")} ${c(`with-ports-${wrapperClass}`)}`, children: [
2172
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `g3p-node-with-ports g3p-node-with-ports-${wrapperClass}`, children: [
2126
2173
  inPorts,
2127
2174
  el,
2128
2175
  outPorts
2129
2176
  ] });
2130
2177
  }
2131
2178
  renderOutsidePorts(el) {
2132
- const c = styler("node", import_node3.default, this.classPrefix);
2133
2179
  const isVertical = this.isVerticalOrientation();
2134
2180
  const isReversed = this.isReversedOrientation();
2135
2181
  let inPorts = this.renderPortRow("in", "out");
@@ -2137,71 +2183,59 @@ var Node2 = class {
2137
2183
  if (!inPorts && !outPorts) return el;
2138
2184
  if (isReversed) [inPorts, outPorts] = [outPorts, inPorts];
2139
2185
  const wrapperClass = isVertical ? "v" : "h";
2140
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `${c("with-ports")} ${c(`with-ports-${wrapperClass}`)}`, children: [
2186
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `g3p-node-with-ports g3p-node-with-ports-${wrapperClass}`, children: [
2141
2187
  inPorts,
2142
2188
  el,
2143
2189
  outPorts
2144
2190
  ] });
2145
2191
  }
2146
2192
  renderBorder(el) {
2147
- const c = styler("node", import_node3.default, this.classPrefix);
2148
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: c("border"), children: el });
2193
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "g3p-node-border", children: el });
2149
2194
  }
2150
2195
  };
2151
2196
 
2152
2197
  // src/canvas/seg.tsx
2153
- var import_seg3 = __toESM(require("./seg.css?raw"), 1);
2154
2198
  var import_jsx_runtime3 = require("jsx-dom/jsx-runtime");
2155
- var log9 = logger("canvas");
2156
2199
  var Seg2 = class {
2157
2200
  id;
2158
2201
  selected;
2159
2202
  hovered;
2160
2203
  canvas;
2161
- classPrefix;
2162
2204
  type;
2163
2205
  svg;
2164
2206
  el;
2165
2207
  source;
2166
2208
  target;
2209
+ edgeIds;
2167
2210
  constructor(canvas, data, g) {
2168
2211
  this.id = data.id;
2169
2212
  this.canvas = canvas;
2170
2213
  this.selected = false;
2171
2214
  this.hovered = false;
2172
2215
  this.svg = data.svg;
2173
- this.classPrefix = canvas.classPrefix;
2174
2216
  this.source = { ...data.source, isDummy: data.sourceNode(g).isDummy };
2175
2217
  this.target = { ...data.target, isDummy: data.targetNode(g).isDummy };
2176
2218
  this.type = data.type;
2219
+ this.edgeIds = data.edgeIds.toArray();
2177
2220
  this.el = this.render();
2178
2221
  }
2179
- handleClick(e) {
2180
- e.stopPropagation();
2181
- }
2182
- handleMouseEnter(e) {
2183
- }
2184
- handleMouseLeave(e) {
2185
- }
2186
- handleContextMenu(e) {
2187
- }
2188
2222
  append() {
2189
2223
  this.canvas.group.appendChild(this.el);
2190
2224
  }
2191
2225
  remove() {
2192
2226
  this.el.remove();
2193
2227
  }
2194
- update(data) {
2228
+ update(data, g) {
2195
2229
  this.svg = data.svg;
2196
2230
  this.type = data.type;
2197
- this.source = data.source;
2198
- this.target = data.target;
2231
+ this.source = { ...data.source, isDummy: data.sourceNode(g).isDummy };
2232
+ this.target = { ...data.target, isDummy: data.targetNode(g).isDummy };
2233
+ this.edgeIds = data.edgeIds.toArray();
2199
2234
  this.remove();
2200
2235
  this.el = this.render();
2201
2236
  this.append();
2202
2237
  }
2203
2238
  render() {
2204
- const c = styler("seg", import_seg3.default, this.classPrefix);
2205
2239
  let { source, target } = normalize(this);
2206
2240
  if (this.source.isDummy) source = void 0;
2207
2241
  if (this.target.isDummy) target = void 0;
@@ -2211,18 +2245,15 @@ var Seg2 = class {
2211
2245
  {
2212
2246
  ref: (el) => this.el = el,
2213
2247
  id: `g3p-seg-${this.id}`,
2214
- className: `${c("container")} ${typeClass}`.trim(),
2215
- onClick: this.handleClick.bind(this),
2216
- onMouseEnter: this.handleMouseEnter.bind(this),
2217
- onMouseLeave: this.handleMouseLeave.bind(this),
2218
- onContextMenu: this.handleContextMenu.bind(this),
2248
+ className: `g3p-seg-container ${typeClass}`.trim(),
2249
+ "data-edge-id": this.id,
2219
2250
  children: [
2220
2251
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2221
2252
  "path",
2222
2253
  {
2223
2254
  d: this.svg,
2224
2255
  fill: "none",
2225
- className: c("line"),
2256
+ className: "g3p-seg-line",
2226
2257
  markerStart: source ? `url(#g3p-marker-${source}-reverse)` : void 0,
2227
2258
  markerEnd: target ? `url(#g3p-marker-${target})` : void 0
2228
2259
  }
@@ -2233,7 +2264,7 @@ var Seg2 = class {
2233
2264
  d: this.svg,
2234
2265
  stroke: "transparent",
2235
2266
  fill: "none",
2236
- className: c("hitbox"),
2267
+ className: "g3p-seg-hitbox",
2237
2268
  style: { cursor: "pointer" }
2238
2269
  }
2239
2270
  )
@@ -2243,46 +2274,561 @@ var Seg2 = class {
2243
2274
  }
2244
2275
  };
2245
2276
 
2246
- // src/canvas/canvas.tsx
2247
- var import_canvas = __toESM(require("./canvas.css?raw"), 1);
2248
- var import_zoom = __toESM(require("./zoom.css?raw"), 1);
2277
+ // src/canvas/editMode.ts
2278
+ var EditMode = class {
2279
+ _state = { type: "idle" };
2280
+ _editable = false;
2281
+ get state() {
2282
+ return this._state;
2283
+ }
2284
+ get editable() {
2285
+ return this._editable;
2286
+ }
2287
+ set editable(value) {
2288
+ this._editable = value;
2289
+ if (!value) {
2290
+ this.reset();
2291
+ }
2292
+ }
2293
+ get isIdle() {
2294
+ return this._state.type === "idle";
2295
+ }
2296
+ get isPanning() {
2297
+ return this._state.type === "panning";
2298
+ }
2299
+ get isCreatingEdge() {
2300
+ return this._state.type === "new-edge";
2301
+ }
2302
+ /** Start panning the canvas */
2303
+ startPan(startCanvas, startTransform) {
2304
+ this._state = { type: "panning", startCanvas, startTransform };
2305
+ }
2306
+ /** Start creating a new edge from a node or port */
2307
+ startNewEdge(id, startGraph, port) {
2308
+ if (!this._editable) return;
2309
+ this._state = {
2310
+ type: "new-edge",
2311
+ source: { id, port },
2312
+ startGraph,
2313
+ currentGraph: startGraph,
2314
+ target: null
2315
+ };
2316
+ }
2317
+ /** Update the current position during new-edge mode */
2318
+ updateNewEdgePosition(currentGraph) {
2319
+ if (this._state.type === "new-edge") {
2320
+ this._state = { ...this._state, currentGraph };
2321
+ }
2322
+ }
2323
+ /** Update the hover target during new-edge mode */
2324
+ setHoverTarget(target) {
2325
+ if (this._state.type === "new-edge") {
2326
+ this._state = { ...this._state, target };
2327
+ }
2328
+ }
2329
+ /** Get the new-edge state if active */
2330
+ getNewEdgeState() {
2331
+ if (this._state.type === "new-edge") {
2332
+ return this._state;
2333
+ }
2334
+ return null;
2335
+ }
2336
+ /** Reset to idle state */
2337
+ reset() {
2338
+ this._state = { type: "idle" };
2339
+ }
2340
+ };
2341
+
2342
+ // src/canvas/newEdge.tsx
2249
2343
  var import_jsx_runtime4 = require("jsx-dom/jsx-runtime");
2250
- var log10 = logger("canvas");
2251
- var themeVarMap = {
2252
- // Canvas
2253
- bg: "--g3p-bg",
2254
- shadow: "--g3p-shadow",
2255
- // Node
2256
- border: "--g3p-border",
2257
- borderHover: "--g3p-border-hover",
2258
- borderSelected: "--g3p-border-selected",
2259
- text: "--g3p-text",
2260
- textMuted: "--g3p-text-muted",
2261
- // Port
2262
- bgHover: "--g3p-port-bg-hover",
2263
- // Edge
2264
- color: "--g3p-edge-color"
2344
+ function renderNewEdge({ start, end }) {
2345
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("g", { className: "g3p-new-edge-container", children: [
2346
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2347
+ "circle",
2348
+ {
2349
+ cx: start.x,
2350
+ cy: start.y,
2351
+ r: 4,
2352
+ className: "g3p-new-edge-origin"
2353
+ }
2354
+ ),
2355
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2356
+ "line",
2357
+ {
2358
+ x1: start.x,
2359
+ y1: start.y,
2360
+ x2: end.x,
2361
+ y2: end.y,
2362
+ className: "g3p-new-edge-line"
2363
+ }
2364
+ ),
2365
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2366
+ "circle",
2367
+ {
2368
+ cx: end.x,
2369
+ cy: end.y,
2370
+ r: 3,
2371
+ className: "g3p-new-edge-end"
2372
+ }
2373
+ )
2374
+ ] });
2375
+ }
2376
+
2377
+ // src/canvas/modal.tsx
2378
+ var import_jsx_runtime5 = require("jsx-dom/jsx-runtime");
2379
+ var Modal = class {
2380
+ container;
2381
+ overlay;
2382
+ dialog;
2383
+ onClose;
2384
+ mouseDownOnOverlay = false;
2385
+ constructor(options) {
2386
+ this.onClose = options.onClose;
2387
+ this.overlay = /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2388
+ "div",
2389
+ {
2390
+ className: "g3p-modal-overlay",
2391
+ onMouseDown: (e) => {
2392
+ this.mouseDownOnOverlay = e.target === this.overlay;
2393
+ },
2394
+ onMouseUp: (e) => {
2395
+ if (this.mouseDownOnOverlay && e.target === this.overlay) this.close();
2396
+ this.mouseDownOnOverlay = false;
2397
+ }
2398
+ }
2399
+ );
2400
+ this.dialog = /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-dialog", children: [
2401
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-header", children: [
2402
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "g3p-modal-title", children: options.title }),
2403
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2404
+ "button",
2405
+ {
2406
+ className: "g3p-modal-close",
2407
+ onClick: () => this.close(),
2408
+ children: "\xD7"
2409
+ }
2410
+ )
2411
+ ] }),
2412
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "g3p-modal-body" }),
2413
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "g3p-modal-footer" })
2414
+ ] });
2415
+ if (options.position) {
2416
+ this.dialog.style.position = "absolute";
2417
+ this.dialog.style.left = `${options.position.x}px`;
2418
+ this.dialog.style.top = `${options.position.y}px`;
2419
+ this.dialog.style.transform = "translate(-50%, -50%)";
2420
+ }
2421
+ this.overlay.appendChild(this.dialog);
2422
+ this.container = this.overlay;
2423
+ this.handleKeyDown = this.handleKeyDown.bind(this);
2424
+ document.addEventListener("keydown", this.handleKeyDown);
2425
+ }
2426
+ handleKeyDown(e) {
2427
+ if (e.key === "Escape") {
2428
+ this.close();
2429
+ }
2430
+ }
2431
+ get body() {
2432
+ return this.dialog.querySelector(".g3p-modal-body");
2433
+ }
2434
+ get footer() {
2435
+ return this.dialog.querySelector(".g3p-modal-footer");
2436
+ }
2437
+ show(parent) {
2438
+ parent.appendChild(this.container);
2439
+ const firstInput = this.dialog.querySelector("input, select, button");
2440
+ if (firstInput) firstInput.focus();
2441
+ }
2442
+ close() {
2443
+ document.removeEventListener("keydown", this.handleKeyDown);
2444
+ this.container.remove();
2445
+ this.onClose();
2446
+ }
2265
2447
  };
2266
- function themeToCSS(theme, selector, prefix = "") {
2267
- const entries = Object.entries(theme).filter(([_, v]) => v !== void 0);
2268
- if (!entries.length) return "";
2269
- let css = `${selector} {
2270
- `;
2271
- for (const [key, value] of entries) {
2272
- let cssVar = themeVarMap[key];
2273
- if (key === "bg" && prefix === "node") {
2274
- cssVar = "--g3p-bg-node";
2275
- } else if (key === "bg" && prefix === "port") {
2276
- cssVar = "--g3p-port-bg";
2448
+ var NewNodeModal = class extends Modal {
2449
+ fieldInputs = /* @__PURE__ */ new Map();
2450
+ typeSelect;
2451
+ submitCallback;
2452
+ fields;
2453
+ constructor(options) {
2454
+ super({
2455
+ title: "New Node",
2456
+ onClose: () => options.onCancel?.()
2457
+ });
2458
+ this.submitCallback = options.onSubmit;
2459
+ this.fields = options.fields ?? /* @__PURE__ */ new Map([["title", "string"]]);
2460
+ this.renderBody(options.nodeTypes);
2461
+ this.renderFooter();
2462
+ }
2463
+ renderBody(nodeTypes) {
2464
+ this.body.innerHTML = "";
2465
+ for (const [name, type] of this.fields) {
2466
+ const label = name.charAt(0).toUpperCase() + name.slice(1);
2467
+ const fieldGroup = this.renderField(name, label, type);
2468
+ this.body.appendChild(fieldGroup);
2277
2469
  }
2278
- if (cssVar) {
2279
- css += ` ${cssVar}: ${value};
2280
- `;
2470
+ if (nodeTypes && nodeTypes.length > 0) {
2471
+ const typeGroup = /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-field", children: [
2472
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { className: "g3p-modal-label", children: "Type" }),
2473
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2474
+ "select",
2475
+ {
2476
+ className: "g3p-modal-select",
2477
+ ref: (el) => this.typeSelect = el,
2478
+ children: [
2479
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "", children: "Default" }),
2480
+ nodeTypes.map((type) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: type, children: type }))
2481
+ ]
2482
+ }
2483
+ )
2484
+ ] });
2485
+ this.body.appendChild(typeGroup);
2281
2486
  }
2282
2487
  }
2283
- css += "}\n";
2284
- return css;
2285
- }
2488
+ renderField(name, label, type) {
2489
+ if (type === "boolean") {
2490
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "g3p-modal-field g3p-modal-field-checkbox", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("label", { className: "g3p-modal-label", children: [
2491
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2492
+ "input",
2493
+ {
2494
+ type: "checkbox",
2495
+ className: "g3p-modal-checkbox",
2496
+ ref: (el) => this.fieldInputs.set(name, el)
2497
+ }
2498
+ ),
2499
+ label
2500
+ ] }) });
2501
+ }
2502
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-field", children: [
2503
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { className: "g3p-modal-label", children: label }),
2504
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2505
+ "input",
2506
+ {
2507
+ type: type === "number" ? "number" : "text",
2508
+ className: "g3p-modal-input",
2509
+ placeholder: `Enter ${label.toLowerCase()}`,
2510
+ ref: (el) => this.fieldInputs.set(name, el)
2511
+ }
2512
+ )
2513
+ ] });
2514
+ }
2515
+ renderFooter() {
2516
+ this.footer.innerHTML = "";
2517
+ this.footer.appendChild(
2518
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-buttons", children: [
2519
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2520
+ "button",
2521
+ {
2522
+ className: "g3p-modal-btn g3p-modal-btn-secondary",
2523
+ onClick: () => this.close(),
2524
+ children: "Cancel"
2525
+ }
2526
+ ),
2527
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2528
+ "button",
2529
+ {
2530
+ className: "g3p-modal-btn g3p-modal-btn-primary",
2531
+ onClick: () => this.submit(),
2532
+ children: "Create"
2533
+ }
2534
+ )
2535
+ ] })
2536
+ );
2537
+ }
2538
+ submit() {
2539
+ const data = {};
2540
+ for (const [name, type] of this.fields) {
2541
+ const input = this.fieldInputs.get(name);
2542
+ if (!input) continue;
2543
+ if (type === "boolean") {
2544
+ data[name] = input.checked;
2545
+ } else if (type === "number") {
2546
+ const val = input.value;
2547
+ if (val) data[name] = Number(val);
2548
+ } else {
2549
+ const val = input.value.trim();
2550
+ if (val) data[name] = val;
2551
+ }
2552
+ }
2553
+ if (Object.keys(data).length === 0) {
2554
+ const firstInput = this.fieldInputs.values().next().value;
2555
+ if (firstInput) firstInput.focus();
2556
+ return;
2557
+ }
2558
+ if (this.typeSelect?.value) {
2559
+ data.type = this.typeSelect.value;
2560
+ }
2561
+ document.removeEventListener("keydown", this.handleKeyDown);
2562
+ this.container.remove();
2563
+ this.submitCallback(data);
2564
+ }
2565
+ };
2566
+ var EditNodeModal = class extends Modal {
2567
+ fieldInputs = /* @__PURE__ */ new Map();
2568
+ typeSelect;
2569
+ node;
2570
+ fields;
2571
+ submitCallback;
2572
+ deleteCallback;
2573
+ constructor(options) {
2574
+ super({
2575
+ title: "Edit Node",
2576
+ position: options.position,
2577
+ onClose: () => options.onCancel?.()
2578
+ });
2579
+ this.node = options.node;
2580
+ this.submitCallback = options.onSubmit;
2581
+ this.deleteCallback = options.onDelete;
2582
+ this.fields = options.fields ?? /* @__PURE__ */ new Map([["title", "string"]]);
2583
+ if (!options.fields && !this.node.title)
2584
+ this.node = { ...this.node, title: this.node.id };
2585
+ this.renderBody(options.nodeTypes);
2586
+ this.renderFooter();
2587
+ }
2588
+ renderBody(nodeTypes) {
2589
+ console.log(`renderBody`, this.node);
2590
+ this.body.innerHTML = "";
2591
+ for (const [name, type] of this.fields) {
2592
+ const label = name.charAt(0).toUpperCase() + name.slice(1);
2593
+ const currentValue = this.node[name];
2594
+ const fieldGroup = this.renderField(name, label, type, currentValue);
2595
+ this.body.appendChild(fieldGroup);
2596
+ }
2597
+ if (nodeTypes && nodeTypes.length > 0) {
2598
+ const currentType = this.node.type ?? "";
2599
+ const typeGroup = /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-field", children: [
2600
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { className: "g3p-modal-label", children: "Type" }),
2601
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2602
+ "select",
2603
+ {
2604
+ className: "g3p-modal-select",
2605
+ ref: (el) => this.typeSelect = el,
2606
+ children: [
2607
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "", selected: !currentType, children: "Default" }),
2608
+ nodeTypes.map((type) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: type, selected: type === currentType, children: type }))
2609
+ ]
2610
+ }
2611
+ )
2612
+ ] });
2613
+ this.body.appendChild(typeGroup);
2614
+ }
2615
+ }
2616
+ renderField(name, label, type, currentValue) {
2617
+ if (type === "boolean") {
2618
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "g3p-modal-field g3p-modal-field-checkbox", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("label", { className: "g3p-modal-label", children: [
2619
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2620
+ "input",
2621
+ {
2622
+ type: "checkbox",
2623
+ className: "g3p-modal-checkbox",
2624
+ checked: !!currentValue,
2625
+ ref: (el) => this.fieldInputs.set(name, el)
2626
+ }
2627
+ ),
2628
+ label
2629
+ ] }) });
2630
+ }
2631
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-field", children: [
2632
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { className: "g3p-modal-label", children: label }),
2633
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2634
+ "input",
2635
+ {
2636
+ type: type === "number" ? "number" : "text",
2637
+ className: "g3p-modal-input",
2638
+ value: currentValue ?? "",
2639
+ ref: (el) => this.fieldInputs.set(name, el)
2640
+ }
2641
+ )
2642
+ ] });
2643
+ }
2644
+ renderFooter() {
2645
+ this.footer.innerHTML = "";
2646
+ this.footer.appendChild(
2647
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-buttons", children: [
2648
+ this.deleteCallback && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2649
+ "button",
2650
+ {
2651
+ className: "g3p-modal-btn g3p-modal-btn-danger",
2652
+ onClick: () => this.delete(),
2653
+ children: "Delete"
2654
+ }
2655
+ ),
2656
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "g3p-modal-spacer" }),
2657
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2658
+ "button",
2659
+ {
2660
+ className: "g3p-modal-btn g3p-modal-btn-secondary",
2661
+ onClick: () => this.close(),
2662
+ children: "Cancel"
2663
+ }
2664
+ ),
2665
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2666
+ "button",
2667
+ {
2668
+ className: "g3p-modal-btn g3p-modal-btn-primary",
2669
+ onClick: () => this.submit(),
2670
+ children: "Save"
2671
+ }
2672
+ )
2673
+ ] })
2674
+ );
2675
+ }
2676
+ submit() {
2677
+ const data = { ...this.node };
2678
+ for (const [name, type] of this.fields) {
2679
+ const input = this.fieldInputs.get(name);
2680
+ if (!input) continue;
2681
+ if (type === "boolean") {
2682
+ data[name] = input.checked;
2683
+ } else if (type === "number") {
2684
+ const val = input.value;
2685
+ data[name] = val ? Number(val) : void 0;
2686
+ } else {
2687
+ const val = input.value.trim();
2688
+ data[name] = val || void 0;
2689
+ }
2690
+ }
2691
+ if (this.typeSelect) {
2692
+ data.type = this.typeSelect.value || void 0;
2693
+ }
2694
+ document.removeEventListener("keydown", this.handleKeyDown);
2695
+ this.container.remove();
2696
+ this.submitCallback(data);
2697
+ }
2698
+ delete() {
2699
+ document.removeEventListener("keydown", this.handleKeyDown);
2700
+ this.container.remove();
2701
+ this.deleteCallback?.();
2702
+ }
2703
+ };
2704
+ var EditEdgeModal = class _EditEdgeModal extends Modal {
2705
+ sourceMarkerSelect;
2706
+ targetMarkerSelect;
2707
+ typeSelect;
2708
+ edge;
2709
+ submitCallback;
2710
+ deleteCallback;
2711
+ static markerTypes = ["none", "arrow", "circle", "diamond", "bar"];
2712
+ constructor(options) {
2713
+ super({
2714
+ title: "Edit Edge",
2715
+ onClose: () => options.onCancel?.()
2716
+ });
2717
+ this.edge = options.edge;
2718
+ this.submitCallback = options.onSubmit;
2719
+ this.deleteCallback = options.onDelete;
2720
+ this.renderBody(options.edgeTypes);
2721
+ this.renderFooter();
2722
+ }
2723
+ renderBody(edgeTypes) {
2724
+ this.body.innerHTML = "";
2725
+ const currentSourceMarker = this.edge.source?.marker ?? "none";
2726
+ const currentTargetMarker = this.edge.target?.marker ?? "arrow";
2727
+ const sourceGroup = /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-field", children: [
2728
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { className: "g3p-modal-label", children: "Source Marker" }),
2729
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2730
+ "select",
2731
+ {
2732
+ className: "g3p-modal-select",
2733
+ ref: (el) => this.sourceMarkerSelect = el,
2734
+ children: _EditEdgeModal.markerTypes.map((type) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: type, selected: type === currentSourceMarker, children: type }))
2735
+ }
2736
+ )
2737
+ ] });
2738
+ this.body.appendChild(sourceGroup);
2739
+ const targetGroup = /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-field", children: [
2740
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { className: "g3p-modal-label", children: "Target Marker" }),
2741
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2742
+ "select",
2743
+ {
2744
+ className: "g3p-modal-select",
2745
+ ref: (el) => this.targetMarkerSelect = el,
2746
+ children: _EditEdgeModal.markerTypes.map((type) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: type, selected: type === currentTargetMarker, children: type }))
2747
+ }
2748
+ )
2749
+ ] });
2750
+ this.body.appendChild(targetGroup);
2751
+ if (edgeTypes && edgeTypes.length > 0) {
2752
+ const currentType = this.edge.type ?? "";
2753
+ const typeGroup = /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-field", children: [
2754
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { className: "g3p-modal-label", children: "Type" }),
2755
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2756
+ "select",
2757
+ {
2758
+ className: "g3p-modal-select",
2759
+ ref: (el) => this.typeSelect = el,
2760
+ children: [
2761
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "", selected: !currentType, children: "Default" }),
2762
+ edgeTypes.map((type) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: type, selected: type === currentType, children: type }))
2763
+ ]
2764
+ }
2765
+ )
2766
+ ] });
2767
+ this.body.appendChild(typeGroup);
2768
+ }
2769
+ }
2770
+ renderFooter() {
2771
+ this.footer.innerHTML = "";
2772
+ this.footer.appendChild(
2773
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-modal-buttons", children: [
2774
+ this.deleteCallback && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2775
+ "button",
2776
+ {
2777
+ className: "g3p-modal-btn g3p-modal-btn-danger",
2778
+ onClick: () => this.delete(),
2779
+ children: "Delete"
2780
+ }
2781
+ ),
2782
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "g3p-modal-spacer" }),
2783
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2784
+ "button",
2785
+ {
2786
+ className: "g3p-modal-btn g3p-modal-btn-secondary",
2787
+ onClick: () => this.close(),
2788
+ children: "Cancel"
2789
+ }
2790
+ ),
2791
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2792
+ "button",
2793
+ {
2794
+ className: "g3p-modal-btn g3p-modal-btn-primary",
2795
+ onClick: () => this.submit(),
2796
+ children: "Save"
2797
+ }
2798
+ )
2799
+ ] })
2800
+ );
2801
+ }
2802
+ submit() {
2803
+ const data = {
2804
+ ...this.edge,
2805
+ source: {
2806
+ ...this.edge.source,
2807
+ marker: this.sourceMarkerSelect.value === "none" ? void 0 : this.sourceMarkerSelect.value
2808
+ },
2809
+ target: {
2810
+ ...this.edge.target,
2811
+ marker: this.targetMarkerSelect.value === "none" ? void 0 : this.targetMarkerSelect.value
2812
+ }
2813
+ };
2814
+ if (this.typeSelect) {
2815
+ data.type = this.typeSelect.value || void 0;
2816
+ }
2817
+ document.removeEventListener("keydown", this.handleKeyDown);
2818
+ this.container.remove();
2819
+ this.submitCallback(data);
2820
+ }
2821
+ delete() {
2822
+ document.removeEventListener("keydown", this.handleKeyDown);
2823
+ this.container.remove();
2824
+ this.deleteCallback?.();
2825
+ }
2826
+ };
2827
+
2828
+ // src/canvas/canvas.tsx
2829
+ var import_styles = __toESM(require("./styles.css?raw"), 1);
2830
+ var import_jsx_runtime6 = require("jsx-dom/jsx-runtime");
2831
+ var log10 = logger("canvas");
2286
2832
  var Canvas = class {
2287
2833
  container;
2288
2834
  root;
@@ -2295,19 +2841,26 @@ var Canvas = class {
2295
2841
  curSegs;
2296
2842
  updating;
2297
2843
  // Pan-zoom state
2298
- isPanning = false;
2299
- panStart = null;
2300
- transformStart = null;
2301
2844
  panScale = null;
2302
2845
  zoomControls;
2303
- constructor(options) {
2846
+ // Edit mode state machine
2847
+ editMode;
2848
+ api;
2849
+ // New-edge visual element
2850
+ newEdgeEl;
2851
+ // Pending drag state (for double-click debounce)
2852
+ pendingDrag = null;
2853
+ constructor(api, options) {
2304
2854
  Object.assign(this, options);
2855
+ this.api = api;
2305
2856
  this.allNodes = /* @__PURE__ */ new Map();
2306
2857
  this.curNodes = /* @__PURE__ */ new Map();
2307
2858
  this.curSegs = /* @__PURE__ */ new Map();
2308
2859
  this.updating = false;
2309
2860
  this.bounds = { min: { x: 0, y: 0 }, max: { x: 0, y: 0 } };
2310
2861
  this.transform = { x: 0, y: 0, scale: 1 };
2862
+ this.editMode = new EditMode();
2863
+ this.editMode.editable = this.editable;
2311
2864
  this.createMeasurementContainer();
2312
2865
  this.createCanvasContainer();
2313
2866
  if (this.panZoom) this.setupPanZoom();
@@ -2352,7 +2905,6 @@ var Canvas = class {
2352
2905
  if (gnode.isDummy) {
2353
2906
  node = new Node2(this, gnode, true);
2354
2907
  node.renderContainer();
2355
- node.setPos(gnode.pos);
2356
2908
  this.allNodes.set(key, node);
2357
2909
  } else {
2358
2910
  if (!this.allNodes.has(key))
@@ -2361,6 +2913,7 @@ var Canvas = class {
2361
2913
  }
2362
2914
  this.curNodes.set(gnode.id, node);
2363
2915
  node.append();
2916
+ node.setPos(gnode.pos);
2364
2917
  }
2365
2918
  updateNode(gnode) {
2366
2919
  if (gnode.isDummy) throw new Error("dummy node cannot be updated");
@@ -2382,10 +2935,10 @@ var Canvas = class {
2382
2935
  this.curSegs.set(gseg.id, seg);
2383
2936
  seg.append();
2384
2937
  }
2385
- updateSeg(gseg) {
2938
+ updateSeg(gseg, g) {
2386
2939
  const seg = this.curSegs.get(gseg.id);
2387
2940
  if (!seg) throw new Error("seg not found");
2388
- seg.update(gseg);
2941
+ seg.update(gseg, g);
2389
2942
  }
2390
2943
  deleteSeg(gseg) {
2391
2944
  const seg = this.curSegs.get(gseg.id);
@@ -2405,24 +2958,88 @@ var Canvas = class {
2405
2958
  for (const node of newNodes.values()) {
2406
2959
  node.measure(isVertical);
2407
2960
  const { id, version } = node.data;
2408
- const key = `${id}:${version}`;
2961
+ const key = `k:${id}:${version}`;
2409
2962
  this.allNodes.set(key, node);
2410
2963
  node.renderContainer();
2411
2964
  }
2412
2965
  this.measurement.innerHTML = "";
2413
2966
  return newNodes;
2414
2967
  }
2968
+ // ========== Mouse event handlers ==========
2415
2969
  onClick(e) {
2416
- console.log("click", e);
2417
- }
2418
- onContextMenu(e) {
2419
- console.log("context menu", e);
2970
+ const hit = this.hitTest(e.clientX, e.clientY);
2971
+ if (hit.type === "node") {
2972
+ this.api.handleClickNode(hit.node.data.id);
2973
+ } else if (hit.type === "edge") {
2974
+ this.api.handleClickEdge(hit.segId);
2975
+ }
2420
2976
  }
2421
- groupTransform() {
2422
- return `translate(${this.transform.x}, ${this.transform.y}) scale(${this.transform.scale})`;
2977
+ onDoubleClick(e) {
2978
+ if (this.pendingDrag) {
2979
+ window.clearTimeout(this.pendingDrag.timeout);
2980
+ this.pendingDrag = null;
2981
+ }
2982
+ if (!this.editMode.editable) return;
2983
+ const hit = this.hitTest(e.clientX, e.clientY);
2984
+ if (hit.type === "node") {
2985
+ if (hit.node.isDummy) return;
2986
+ this.api.handleEditNode(hit.node.data.id);
2987
+ } else if (hit.type === "edge") {
2988
+ this.api.handleEditEdge(hit.segId);
2989
+ } else {
2990
+ this.api.handleNewNode();
2991
+ }
2423
2992
  }
2424
- viewBox() {
2425
- const p = this.padding;
2993
+ // ========== Built-in Modals ==========
2994
+ /** Show the new node modal */
2995
+ showNewNodeModal(callback) {
2996
+ const nodeTypes = Object.keys(this.nodeTypes);
2997
+ const fields = this.api.getNodeFields();
2998
+ const modal = new NewNodeModal({
2999
+ nodeTypes: nodeTypes.length > 0 ? nodeTypes : void 0,
3000
+ fields: fields.size > 0 ? fields : void 0,
3001
+ onSubmit: (data) => {
3002
+ callback(data);
3003
+ }
3004
+ });
3005
+ modal.show(document.body);
3006
+ }
3007
+ /** Show the edit node modal */
3008
+ showEditNodeModal(node, callback) {
3009
+ const nodeTypes = Object.keys(this.nodeTypes);
3010
+ const fields = this.api.getNodeFields();
3011
+ const modal = new EditNodeModal({
3012
+ node,
3013
+ nodeTypes: nodeTypes.length > 0 ? nodeTypes : void 0,
3014
+ fields: fields.size > 0 ? fields : void 0,
3015
+ onSubmit: (data) => {
3016
+ callback(data);
3017
+ },
3018
+ onDelete: () => {
3019
+ this.api.handleDeleteNode(node.id);
3020
+ }
3021
+ });
3022
+ modal.show(document.body);
3023
+ }
3024
+ /** Show the edit edge modal */
3025
+ showEditEdgeModal(edge, callback) {
3026
+ const modal = new EditEdgeModal({
3027
+ edge,
3028
+ edgeTypes: Object.keys(this.edgeTypes),
3029
+ onSubmit: callback,
3030
+ onDelete: () => {
3031
+ this.api.handleDeleteEdge(edge.id);
3032
+ }
3033
+ });
3034
+ modal.show(document.body);
3035
+ }
3036
+ onContextMenu(e) {
3037
+ }
3038
+ groupTransform() {
3039
+ return `translate(${this.transform.x}, ${this.transform.y}) scale(${this.transform.scale})`;
3040
+ }
3041
+ viewBox() {
3042
+ const p = this.padding;
2426
3043
  const x = this.bounds.min.x - p;
2427
3044
  const y = this.bounds.min.y - p;
2428
3045
  const w = this.bounds.max.x - this.bounds.min.x + p * 2;
@@ -2431,53 +3048,52 @@ var Canvas = class {
2431
3048
  }
2432
3049
  generateDynamicStyles() {
2433
3050
  let css = "";
2434
- const prefix = this.classPrefix;
2435
- css += themeToCSS(this.theme, `.${prefix}-canvas-container`);
3051
+ css += themeToCSS(this.theme, `.g3p-canvas-container`);
2436
3052
  for (const [type, vars] of Object.entries(this.nodeTypes)) {
2437
- css += themeToCSS(vars, `.${prefix}-node-type-${type}`, "node");
3053
+ css += themeToCSS(vars, `.g3p-node-type-${type}`, "node");
2438
3054
  }
2439
3055
  for (const [type, vars] of Object.entries(this.edgeTypes)) {
2440
- css += themeToCSS(vars, `.${prefix}-edge-type-${type}`);
3056
+ css += themeToCSS(vars, `.g3p-edge-type-${type}`);
2441
3057
  }
2442
3058
  return css;
2443
3059
  }
2444
3060
  createCanvasContainer() {
2445
- const markerStyleEl = document.createElement("style");
2446
- markerStyleEl.textContent = import_marker.default;
2447
- document.head.appendChild(markerStyleEl);
2448
- const zoomStyleEl = document.createElement("style");
2449
- zoomStyleEl.textContent = import_zoom.default;
2450
- document.head.appendChild(zoomStyleEl);
3061
+ if (!document.getElementById("g3p-styles")) {
3062
+ const baseStyleEl = document.createElement("style");
3063
+ baseStyleEl.id = "g3p-styles";
3064
+ baseStyleEl.textContent = import_styles.default;
3065
+ document.head.appendChild(baseStyleEl);
3066
+ }
2451
3067
  const dynamicStyles = this.generateDynamicStyles();
2452
3068
  if (dynamicStyles) {
2453
- const themeStyleEl = document.createElement("style");
2454
- themeStyleEl.textContent = dynamicStyles;
2455
- document.head.appendChild(themeStyleEl);
3069
+ const dynamicStyleEl = document.createElement("style");
3070
+ dynamicStyleEl.textContent = dynamicStyles;
3071
+ document.head.appendChild(dynamicStyleEl);
2456
3072
  }
2457
- const c = styler("canvas", import_canvas.default, this.classPrefix);
2458
3073
  const colorModeClass = this.colorMode !== "system" ? `g3p-${this.colorMode}` : "";
2459
- this.container = /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3074
+ this.container = /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2460
3075
  "div",
2461
3076
  {
2462
- className: `${c("container")} ${colorModeClass}`.trim(),
3077
+ className: `g3p-canvas-container ${colorModeClass}`.trim(),
2463
3078
  ref: (el) => this.container = el,
2464
3079
  onContextMenu: this.onContextMenu.bind(this),
2465
- children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
3080
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
2466
3081
  "svg",
2467
3082
  {
2468
3083
  ref: (el) => this.root = el,
2469
- className: c("root"),
3084
+ className: "g3p-canvas-root",
2470
3085
  width: this.width,
2471
3086
  height: this.height,
2472
3087
  viewBox: this.viewBox(),
2473
3088
  preserveAspectRatio: "xMidYMid meet",
2474
3089
  onClick: this.onClick.bind(this),
3090
+ onDblClick: this.onDoubleClick.bind(this),
2475
3091
  children: [
2476
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("defs", { children: [
2477
- Object.values(markerDefs).map((marker) => marker(this.markerSize, this.classPrefix, false)),
2478
- Object.values(markerDefs).map((marker) => marker(this.markerSize, this.classPrefix, true))
3092
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("defs", { children: [
3093
+ Object.values(markerDefs).map((marker) => marker(this.markerSize, false)),
3094
+ Object.values(markerDefs).map((marker) => marker(this.markerSize, true))
2479
3095
  ] }),
2480
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3096
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2481
3097
  "g",
2482
3098
  {
2483
3099
  ref: (el) => this.group = el,
@@ -2496,14 +3112,34 @@ var Canvas = class {
2496
3112
  this.container.addEventListener("mousedown", this.onMouseDown.bind(this));
2497
3113
  document.addEventListener("mousemove", this.onMouseMove.bind(this));
2498
3114
  document.addEventListener("mouseup", this.onMouseUp.bind(this));
3115
+ document.addEventListener("keydown", this.onKeyDown.bind(this));
2499
3116
  this.createZoomControls();
2500
3117
  }
3118
+ onKeyDown(e) {
3119
+ if (e.key === "Escape" && this.editMode.isCreatingEdge) {
3120
+ this.endNewEdge(true);
3121
+ }
3122
+ }
2501
3123
  /** Convert screen coordinates to canvas-relative coordinates */
2502
3124
  screenToCanvas(screen) {
2503
3125
  const rect = this.container.getBoundingClientRect();
2504
3126
  return canvasPos(screen.x - rect.left, screen.y - rect.top);
2505
3127
  }
2506
- /**
3128
+ /** Convert canvas coordinates to graph coordinates */
3129
+ canvasToGraph(canvas) {
3130
+ const vb = this.currentViewBox();
3131
+ const { scale, offsetX, offsetY } = this.getEffectiveScale();
3132
+ return graphPos(
3133
+ vb.x - offsetX + canvas.x * scale,
3134
+ vb.y - offsetY + canvas.y * scale
3135
+ );
3136
+ }
3137
+ /** Convert screen coordinates to graph coordinates */
3138
+ screenToGraph(screen) {
3139
+ const canvas = this.screenToCanvas(screen);
3140
+ return this.canvasToGraph(canvas);
3141
+ }
3142
+ /**
2507
3143
  * Get the effective scale from canvas pixels to graph units,
2508
3144
  * accounting for preserveAspectRatio="xMidYMid meet" which uses
2509
3145
  * the smaller scale (to fit) and centers the content.
@@ -2520,15 +3156,6 @@ var Canvas = class {
2520
3156
  const offsetY = (actualH - vb.h) / 2;
2521
3157
  return { scale, offsetX, offsetY };
2522
3158
  }
2523
- /** Convert canvas coordinates to graph coordinates */
2524
- canvasToGraph(canvas) {
2525
- const vb = this.currentViewBox();
2526
- const { scale, offsetX, offsetY } = this.getEffectiveScale();
2527
- return graphPos(
2528
- vb.x - offsetX + canvas.x * scale,
2529
- vb.y - offsetY + canvas.y * scale
2530
- );
2531
- }
2532
3159
  /** Get current viewBox as an object */
2533
3160
  currentViewBox() {
2534
3161
  const p = this.padding;
@@ -2563,28 +3190,67 @@ var Canvas = class {
2563
3190
  onMouseDown(e) {
2564
3191
  if (e.button !== 0) return;
2565
3192
  if (e.target.closest(".g3p-zoom-controls")) return;
2566
- this.isPanning = true;
2567
- this.panStart = this.screenToCanvas(screenPos(e.clientX, e.clientY));
2568
- this.transformStart = { ...this.transform };
2569
- const { scale } = this.getEffectiveScale();
2570
- this.panScale = { x: scale, y: scale };
2571
- this.container.style.cursor = "grabbing";
2572
- e.preventDefault();
3193
+ const hit = this.hitTest(e.clientX, e.clientY);
3194
+ if (this.editMode.editable && (hit.type === "node" || hit.type === "port")) {
3195
+ const node = hit.node;
3196
+ if (node.isDummy) return;
3197
+ e.preventDefault();
3198
+ e.stopPropagation();
3199
+ const startGraph = this.screenToGraph(hit.center);
3200
+ const portId = hit.type === "port" ? hit.port : void 0;
3201
+ this.pendingDrag = {
3202
+ timeout: window.setTimeout(() => {
3203
+ if (this.pendingDrag) {
3204
+ this.startNewEdge(this.pendingDrag.nodeId, this.pendingDrag.startGraph, this.pendingDrag.portId);
3205
+ this.pendingDrag = null;
3206
+ }
3207
+ }, 200),
3208
+ nodeId: node.data.id,
3209
+ startGraph,
3210
+ portId
3211
+ };
3212
+ return;
3213
+ }
3214
+ if (hit.type === "canvas" || hit.type === "edge") {
3215
+ const startCanvas = this.screenToCanvas(screenPos(e.clientX, e.clientY));
3216
+ this.editMode.startPan(startCanvas, { ...this.transform });
3217
+ const { scale } = this.getEffectiveScale();
3218
+ this.panScale = { x: scale, y: scale };
3219
+ this.container.style.cursor = "grabbing";
3220
+ e.preventDefault();
3221
+ }
2573
3222
  }
2574
3223
  onMouseMove(e) {
2575
- if (!this.isPanning || !this.panStart || !this.transformStart || !this.panScale) return;
3224
+ if (this.editMode.isCreatingEdge) {
3225
+ const screenCursor = screenPos(e.clientX, e.clientY);
3226
+ const canvasCursor = this.screenToCanvas(screenCursor);
3227
+ const graphCursor = this.canvasToGraph(canvasCursor);
3228
+ this.editMode.updateNewEdgePosition(graphCursor);
3229
+ this.updateNewEdgeVisual();
3230
+ this.detectHoverTarget(e.clientX, e.clientY);
3231
+ return;
3232
+ }
3233
+ if (!this.editMode.isPanning || !this.panScale) return;
3234
+ const panState = this.editMode.state;
3235
+ if (panState.type !== "panning") return;
2576
3236
  const current = this.screenToCanvas(screenPos(e.clientX, e.clientY));
2577
- const dx = current.x - this.panStart.x;
2578
- const dy = current.y - this.panStart.y;
2579
- this.transform.x = this.transformStart.x + dx * this.panScale.x;
2580
- this.transform.y = this.transformStart.y + dy * this.panScale.y;
3237
+ const dx = current.x - panState.startCanvas.x;
3238
+ const dy = current.y - panState.startCanvas.y;
3239
+ this.transform.x = panState.startTransform.x + dx * this.panScale.x;
3240
+ this.transform.y = panState.startTransform.y + dy * this.panScale.y;
2581
3241
  this.applyTransform();
2582
3242
  }
2583
3243
  onMouseUp(e) {
2584
- if (!this.isPanning) return;
2585
- this.isPanning = false;
2586
- this.panStart = null;
2587
- this.transformStart = null;
3244
+ if (this.pendingDrag) {
3245
+ window.clearTimeout(this.pendingDrag.timeout);
3246
+ this.pendingDrag = null;
3247
+ }
3248
+ if (this.editMode.isCreatingEdge) {
3249
+ this.endNewEdge(false);
3250
+ return;
3251
+ }
3252
+ if (!this.editMode.isPanning) return;
3253
+ this.editMode.reset();
2588
3254
  this.panScale = null;
2589
3255
  this.container.style.cursor = "";
2590
3256
  }
@@ -2594,12 +3260,11 @@ var Canvas = class {
2594
3260
  this.updateZoomLevel();
2595
3261
  }
2596
3262
  createZoomControls() {
2597
- const c = styler("zoom", import_zoom.default, this.classPrefix);
2598
- this.zoomControls = /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: c("controls"), children: [
2599
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: c("btn"), onClick: () => this.zoomIn(), children: "+" }),
2600
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: c("level"), id: "g3p-zoom-level", children: "100%" }),
2601
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: c("btn"), onClick: () => this.zoomOut(), children: "\u2212" }),
2602
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: `${c("btn")} ${c("reset")}`, onClick: () => this.zoomReset(), children: "\u27F2" })
3263
+ this.zoomControls = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "g3p-zoom-controls", children: [
3264
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { className: "g3p-zoom-btn", onClick: () => this.zoomIn(), children: "+" }),
3265
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "g3p-zoom-level", id: "g3p-zoom-level", children: "100%" }),
3266
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { className: "g3p-zoom-btn", onClick: () => this.zoomOut(), children: "\u2212" }),
3267
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { className: "g3p-zoom-btn g3p-zoom-reset", onClick: () => this.zoomReset(), children: "\u27F2" })
2603
3268
  ] });
2604
3269
  this.container.appendChild(this.zoomControls);
2605
3270
  }
@@ -2621,17 +3286,215 @@ var Canvas = class {
2621
3286
  this.transform = { x: 0, y: 0, scale: 1 };
2622
3287
  this.applyTransform();
2623
3288
  }
3289
+ // ==================== New-Edge Mode ====================
3290
+ /** Start creating a new edge from a node */
3291
+ startNewEdge(sourceNodeId, startGraph, sourcePortId) {
3292
+ this.editMode.startNewEdge(sourceNodeId, startGraph, sourcePortId);
3293
+ this.updateNewEdgeVisual();
3294
+ this.container.style.cursor = "crosshair";
3295
+ }
3296
+ /** Update the new-edge visual during drag */
3297
+ updateNewEdgeVisual() {
3298
+ const state = this.editMode.getNewEdgeState();
3299
+ if (!state) {
3300
+ this.removeNewEdgeVisual();
3301
+ return;
3302
+ }
3303
+ if (this.newEdgeEl) {
3304
+ this.newEdgeEl.remove();
3305
+ }
3306
+ this.newEdgeEl = renderNewEdge({
3307
+ start: state.startGraph,
3308
+ end: state.currentGraph
3309
+ });
3310
+ this.group.appendChild(this.newEdgeEl);
3311
+ }
3312
+ /** Remove the new-edge visual */
3313
+ removeNewEdgeVisual() {
3314
+ if (this.newEdgeEl) {
3315
+ this.newEdgeEl.remove();
3316
+ this.newEdgeEl = void 0;
3317
+ }
3318
+ }
3319
+ /** Complete or cancel the new-edge creation */
3320
+ endNewEdge(cancelled = false) {
3321
+ const state = this.editMode.getNewEdgeState();
3322
+ if (!state) return;
3323
+ if (!cancelled) {
3324
+ const { target, source } = state;
3325
+ if (target?.type == "node") {
3326
+ this.api.handleAddEdge({ id: "", source, target });
3327
+ } else {
3328
+ this.api.handleNewNodeFrom(source);
3329
+ }
3330
+ }
3331
+ this.removeNewEdgeVisual();
3332
+ this.clearDropTargetHighlight();
3333
+ this.editMode.reset();
3334
+ this.container.style.cursor = "";
3335
+ }
3336
+ /** Find node data by internal ID */
3337
+ findNodeDataById(nodeId) {
3338
+ for (const node of this.curNodes.values()) {
3339
+ if (node.data?.id === nodeId) {
3340
+ return node.data.data;
3341
+ }
3342
+ }
3343
+ return null;
3344
+ }
3345
+ /** Set hover target for new-edge mode */
3346
+ setNewEdgeHoverTarget(id, port) {
3347
+ this.clearDropTargetHighlight();
3348
+ this.editMode.setHoverTarget({ type: "node", id, port });
3349
+ if (port) {
3350
+ const portEl = this.container?.querySelector(`.g3p-node-port[data-node-id="${id}"][data-port-id="${port}"]`);
3351
+ if (portEl) portEl.classList.add("g3p-drop-target");
3352
+ } else {
3353
+ const node = this.curNodes.get(id);
3354
+ if (node?.container) node.container.classList.add("g3p-drop-target");
3355
+ }
3356
+ }
3357
+ /** Clear hover target for new-edge mode */
3358
+ clearNewEdgeHoverTarget() {
3359
+ this.clearDropTargetHighlight();
3360
+ this.editMode.setHoverTarget(null);
3361
+ }
3362
+ /** Remove drop target highlight from all elements */
3363
+ clearDropTargetHighlight() {
3364
+ for (const node of this.curNodes.values()) {
3365
+ node.container?.classList.remove("g3p-drop-target");
3366
+ }
3367
+ this.container?.querySelectorAll(".g3p-drop-target").forEach((el) => {
3368
+ el.classList.remove("g3p-drop-target");
3369
+ });
3370
+ }
3371
+ /** Detect hover target during new-edge drag using elementFromPoint */
3372
+ detectHoverTarget(clientX, clientY) {
3373
+ if (this.newEdgeEl) {
3374
+ this.newEdgeEl.style.display = "none";
3375
+ }
3376
+ const el = document.elementFromPoint(clientX, clientY);
3377
+ if (this.newEdgeEl) {
3378
+ this.newEdgeEl.style.display = "";
3379
+ }
3380
+ if (!el) {
3381
+ this.clearNewEdgeHoverTarget();
3382
+ return;
3383
+ }
3384
+ const portEl = el.closest(".g3p-node-port");
3385
+ if (portEl) {
3386
+ const nodeId = portEl.getAttribute("data-node-id");
3387
+ const portId = portEl.getAttribute("data-port-id");
3388
+ if (nodeId && portId) {
3389
+ const node = this.curNodes.get(nodeId);
3390
+ if (node && !node.isDummy) {
3391
+ this.setNewEdgeHoverTarget(nodeId, portId);
3392
+ return;
3393
+ }
3394
+ }
3395
+ }
3396
+ const nodeEl = el.closest(".g3p-node-container");
3397
+ if (nodeEl) {
3398
+ const nodeId = nodeEl.getAttribute("data-node-id");
3399
+ if (nodeId) {
3400
+ const node = this.curNodes.get(nodeId);
3401
+ if (node && !node.isDummy) {
3402
+ this.setNewEdgeHoverTarget(node.data.id);
3403
+ return;
3404
+ }
3405
+ }
3406
+ }
3407
+ this.clearNewEdgeHoverTarget();
3408
+ }
3409
+ // ==================== Hit Testing ====================
3410
+ /** Result of a hit test */
3411
+ hitTest(clientX, clientY) {
3412
+ const el = document.elementFromPoint(clientX, clientY);
3413
+ if (!el) return { type: "canvas" };
3414
+ const getCenter = (el2) => {
3415
+ const rect = el2.getBoundingClientRect();
3416
+ return screenPos(rect.left + rect.width / 2, rect.top + rect.height / 2);
3417
+ };
3418
+ const portEl = el.closest(".g3p-node-port");
3419
+ if (portEl) {
3420
+ const nodeId = portEl.getAttribute("data-node-id");
3421
+ const portId = portEl.getAttribute("data-port-id");
3422
+ if (nodeId && portId) {
3423
+ const center = getCenter(portEl);
3424
+ const node = this.curNodes.get(nodeId);
3425
+ if (node) {
3426
+ return { type: "port", node, port: portId, center };
3427
+ }
3428
+ }
3429
+ }
3430
+ const nodeEl = el.closest(".g3p-node-container");
3431
+ if (nodeEl) {
3432
+ const nodeId = nodeEl.getAttribute("data-node-id");
3433
+ if (nodeId) {
3434
+ const borderEl = el.closest(".g3p-node-border");
3435
+ const center = getCenter(borderEl ?? nodeEl);
3436
+ const node = this.curNodes.get(nodeId);
3437
+ if (node) {
3438
+ return { type: "node", node, center };
3439
+ }
3440
+ }
3441
+ }
3442
+ const edgeEl = el.closest(".g3p-seg-container");
3443
+ if (edgeEl) {
3444
+ const segId = edgeEl.getAttribute("data-edge-id");
3445
+ if (segId) {
3446
+ return { type: "edge", segId };
3447
+ }
3448
+ }
3449
+ return { type: "canvas" };
3450
+ }
2624
3451
  };
3452
+ var themeVarMap = {
3453
+ // Canvas
3454
+ bg: "--g3p-bg",
3455
+ shadow: "--g3p-shadow",
3456
+ // Node
3457
+ border: "--g3p-border",
3458
+ borderHover: "--g3p-border-hover",
3459
+ borderSelected: "--g3p-border-selected",
3460
+ text: "--g3p-text",
3461
+ textMuted: "--g3p-text-muted",
3462
+ // Port
3463
+ bgHover: "--g3p-port-bg-hover",
3464
+ // Edge
3465
+ color: "--g3p-edge-color"
3466
+ };
3467
+ function themeToCSS(theme, selector, prefix = "") {
3468
+ const entries = Object.entries(theme).filter(([_, v]) => v !== void 0);
3469
+ if (!entries.length) return "";
3470
+ let css = `${selector} {
3471
+ `;
3472
+ for (const [key, value] of entries) {
3473
+ let cssVar = themeVarMap[key];
3474
+ if (key === "bg" && prefix === "node") {
3475
+ cssVar = "--g3p-bg-node";
3476
+ } else if (key === "bg" && prefix === "port") {
3477
+ cssVar = "--g3p-port-bg";
3478
+ }
3479
+ if (cssVar) {
3480
+ css += ` ${cssVar}: ${value};
3481
+ `;
3482
+ }
3483
+ }
3484
+ css += "}\n";
3485
+ return css;
3486
+ }
2625
3487
 
2626
3488
  // src/canvas/render-node.tsx
2627
- var import_jsx_runtime5 = require("jsx-dom/jsx-runtime");
2628
- function renderNode(node) {
3489
+ var import_jsx_runtime7 = require("jsx-dom/jsx-runtime");
3490
+ function renderNode(node, props) {
2629
3491
  if (typeof node == "string") node = { id: node };
2630
- const title = node?.title ?? node?.label ?? node?.name ?? node?.text ?? node?.id ?? "?";
3492
+ const title = node?.title ?? props?.title ?? node?.label ?? node?.name ?? node?.text ?? props?.text ?? node?.id ?? "?";
2631
3493
  const detail = node?.detail ?? node?.description ?? node?.subtitle;
2632
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "g3p-node-default", children: [
2633
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "g3p-node-title", children: title }),
2634
- detail && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "g3p-node-detail", children: detail })
3494
+ console.log(`renderNode: ${node.id} ${title} ${detail}`);
3495
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "g3p-node-default", children: [
3496
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "g3p-node-title", children: title }),
3497
+ detail && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "g3p-node-detail", children: detail })
2635
3498
  ] });
2636
3499
  }
2637
3500
 
@@ -2663,7 +3526,6 @@ function defaults() {
2663
3526
  },
2664
3527
  canvas: {
2665
3528
  renderNode,
2666
- classPrefix: "g3p",
2667
3529
  width: "100%",
2668
3530
  height: "100%",
2669
3531
  padding: 20,
@@ -2700,26 +3562,50 @@ var Updater = class _Updater {
2700
3562
  this.update.addNodes.push(node);
2701
3563
  return this;
2702
3564
  }
3565
+ addNodes(...nodes) {
3566
+ this.update.addNodes.push(...nodes);
3567
+ return this;
3568
+ }
2703
3569
  deleteNode(node) {
2704
3570
  this.update.removeNodes.push(node);
2705
3571
  return this;
2706
3572
  }
3573
+ deleteNodes(...nodes) {
3574
+ this.update.removeNodes.push(...nodes);
3575
+ return this;
3576
+ }
2707
3577
  updateNode(node) {
2708
3578
  this.update.updateNodes.push(node);
2709
3579
  return this;
2710
3580
  }
3581
+ updateNodes(...nodes) {
3582
+ this.update.updateNodes.push(...nodes);
3583
+ return this;
3584
+ }
2711
3585
  addEdge(edge) {
2712
3586
  this.update.addEdges.push(edge);
2713
3587
  return this;
2714
3588
  }
3589
+ addEdges(...edges) {
3590
+ this.update.addEdges.push(...edges);
3591
+ return this;
3592
+ }
2715
3593
  deleteEdge(edge) {
2716
3594
  this.update.removeEdges.push(edge);
2717
3595
  return this;
2718
3596
  }
3597
+ deleteEdges(...edges) {
3598
+ this.update.removeEdges.push(...edges);
3599
+ return this;
3600
+ }
2719
3601
  updateEdge(edge) {
2720
3602
  this.update.updateEdges.push(edge);
2721
3603
  return this;
2722
3604
  }
3605
+ updateEdges(...edges) {
3606
+ this.update.updateEdges.push(...edges);
3607
+ return this;
3608
+ }
2723
3609
  static add(nodes, edges) {
2724
3610
  const updater = new _Updater();
2725
3611
  updater.update.addNodes = nodes;
@@ -2740,22 +3626,19 @@ var API = class {
2740
3626
  nodeIds;
2741
3627
  edgeIds;
2742
3628
  nodeVersions;
3629
+ nodeOverrides;
3630
+ edgeOverrides;
3631
+ nodeFields;
2743
3632
  nextNodeId;
2744
3633
  nextEdgeId;
3634
+ events;
2745
3635
  root;
2746
3636
  constructor(args) {
2747
3637
  this.root = args.root;
2748
3638
  this.options = applyDefaults(args.options);
2749
- let graph2 = new Graph({ options: this.options.graph });
2750
- this.state = { graph: graph2, update: null };
2751
- this.seq = [this.state];
2752
- this.index = 0;
2753
- this.nodeIds = /* @__PURE__ */ new Map();
2754
- this.edgeIds = /* @__PURE__ */ new Map();
2755
- this.nodeVersions = /* @__PURE__ */ new Map();
2756
- this.nextNodeId = 1;
2757
- this.nextEdgeId = 1;
2758
- this.canvas = new Canvas({
3639
+ this.events = args.events || {};
3640
+ this.reset();
3641
+ this.canvas = new Canvas(this, {
2759
3642
  ...this.options.canvas,
2760
3643
  dummyNodeSize: this.options.graph.dummyNodeSize,
2761
3644
  orientation: this.options.graph.orientation
@@ -2768,13 +3651,63 @@ var API = class {
2768
3651
  this.history = [];
2769
3652
  }
2770
3653
  }
3654
+ reset() {
3655
+ let graph2 = new Graph({ options: this.options.graph });
3656
+ this.state = { graph: graph2, update: null };
3657
+ this.seq = [this.state];
3658
+ this.index = 0;
3659
+ this.nodeIds = /* @__PURE__ */ new Map();
3660
+ this.edgeIds = /* @__PURE__ */ new Map();
3661
+ this.nodeVersions = /* @__PURE__ */ new Map();
3662
+ this.nodeOverrides = /* @__PURE__ */ new Map();
3663
+ this.edgeOverrides = /* @__PURE__ */ new Map();
3664
+ this.nodeFields = /* @__PURE__ */ new Map();
3665
+ this.nextNodeId = 1;
3666
+ this.nextEdgeId = 1;
3667
+ }
3668
+ /** Initialize the API */
2771
3669
  async init() {
2772
3670
  const root = document.getElementById(this.root);
2773
3671
  if (!root) throw new Error("root element not found");
2774
3672
  root.appendChild(this.canvas.container);
3673
+ await this.applyHistory();
3674
+ }
3675
+ async applyHistory() {
2775
3676
  for (const update of this.history)
2776
3677
  await this.applyUpdate(update);
2777
3678
  }
3679
+ /** Current history index (0-based) */
3680
+ getHistoryIndex() {
3681
+ return this.index;
3682
+ }
3683
+ /** Current history length */
3684
+ getHistoryLength() {
3685
+ return this.seq.length;
3686
+ }
3687
+ /** Toggle canvas editable mode without re-creating the graph */
3688
+ setEditable(editable) {
3689
+ this.canvas.editMode.editable = editable;
3690
+ }
3691
+ /** Replace entire history (clears prior) */
3692
+ async replaceHistory(frames) {
3693
+ this.reset();
3694
+ this.history = frames;
3695
+ await this.applyHistory();
3696
+ }
3697
+ /** Rebuild from snapshot (nodes/edges) */
3698
+ async replaceSnapshot(nodes, edges, description) {
3699
+ this.reset();
3700
+ this.history = [{
3701
+ addNodes: nodes,
3702
+ addEdges: edges,
3703
+ description
3704
+ }];
3705
+ await this.applyHistory();
3706
+ }
3707
+ get graph() {
3708
+ return this.state.graph;
3709
+ }
3710
+ /** Navigate to a different state */
2778
3711
  nav(nav) {
2779
3712
  let newIndex;
2780
3713
  switch (nav) {
@@ -2796,6 +3729,8 @@ var API = class {
2796
3729
  this.applyDiff(this.index, newIndex);
2797
3730
  this.index = newIndex;
2798
3731
  this.state = this.seq[this.index];
3732
+ if (this.events.historyChange)
3733
+ this.events.historyChange(this.index, this.seq.length);
2799
3734
  }
2800
3735
  applyDiff(oldIndex, newIndex) {
2801
3736
  const oldGraph = this.seq[oldIndex].graph;
@@ -2805,36 +3740,72 @@ var API = class {
2805
3740
  if (!newNode) this.canvas.deleteNode(oldNode);
2806
3741
  }
2807
3742
  for (const newNode of newGraph.nodes.values()) {
2808
- if (!oldGraph.nodes.has(newNode.id)) this.canvas.addNode(newNode);
3743
+ const oldNode = oldGraph.nodes.get(newNode.id);
3744
+ if (!oldNode) {
3745
+ this.canvas.addNode(newNode);
3746
+ } else if (oldNode.key !== newNode.key) {
3747
+ this.canvas.deleteNode(oldNode);
3748
+ this.canvas.addNode(newNode);
3749
+ } else if (oldNode.pos !== newNode.pos) {
3750
+ this.canvas.getNode(newNode.key).setPos(newNode.pos);
3751
+ }
2809
3752
  }
2810
3753
  for (const oldSeg of oldGraph.segs.values()) {
2811
3754
  const newSeg = newGraph.segs.get(oldSeg.id);
2812
- if (!newSeg)
3755
+ if (!newSeg) {
2813
3756
  this.canvas.deleteSeg(oldSeg);
2814
- else if (oldSeg.svg != newSeg.svg)
2815
- this.canvas.updateSeg(newSeg);
3757
+ } else if (oldSeg !== newSeg) {
3758
+ this.canvas.updateSeg(newSeg, newGraph);
3759
+ }
2816
3760
  }
2817
3761
  for (const newSeg of newGraph.segs.values()) {
2818
- if (!oldGraph.segs.has(newSeg.id))
3762
+ if (!oldGraph.segs.has(newSeg.id)) {
2819
3763
  this.canvas.addSeg(newSeg, newGraph);
3764
+ }
2820
3765
  }
2821
3766
  this.canvas.update();
2822
3767
  }
3768
+ /** Add a node */
2823
3769
  async addNode(node) {
2824
3770
  await this.update((update) => update.addNode(node));
2825
3771
  }
3772
+ /** Delete a node */
2826
3773
  async deleteNode(node) {
2827
3774
  await this.update((update) => update.deleteNode(node));
2828
3775
  }
3776
+ /** Update a node */
2829
3777
  async updateNode(node) {
2830
3778
  await this.update((update) => update.updateNode(node));
2831
3779
  }
3780
+ /** Add an edge */
2832
3781
  async addEdge(edge) {
2833
3782
  await this.update((update) => update.addEdge(edge));
2834
3783
  }
3784
+ /** Delete an edge */
2835
3785
  async deleteEdge(edge) {
2836
3786
  await this.update((update) => update.deleteEdge(edge));
2837
3787
  }
3788
+ /** Update an edge */
3789
+ async updateEdge(edge) {
3790
+ await this.update((update) => update.updateEdge(edge));
3791
+ }
3792
+ /** Perform a batch of updates */
3793
+ async update(callback) {
3794
+ const updater = new Updater();
3795
+ callback(updater);
3796
+ await this.applyUpdate(updater.update);
3797
+ }
3798
+ /** Rebuild the graph from scratch (removes all then re-adds all nodes/edges) */
3799
+ async rebuild() {
3800
+ const nodes = [...this.nodeIds.keys()];
3801
+ const edges = [...this.edgeIds.keys()];
3802
+ await this.update((updater) => {
3803
+ for (const edge of edges) updater.deleteEdge(edge);
3804
+ for (const node of nodes) updater.deleteNode(node);
3805
+ for (const node of nodes) updater.addNode(node);
3806
+ for (const edge of edges) updater.addEdge(edge);
3807
+ });
3808
+ }
2838
3809
  async applyUpdate(update) {
2839
3810
  log11.info("applyUpdate", update);
2840
3811
  const nodes = await this.measureNodes(update);
@@ -2851,12 +3822,16 @@ var API = class {
2851
3822
  this._addEdge(edge, mut);
2852
3823
  for (const edge of update.updateEdges ?? [])
2853
3824
  this._updateEdge(edge, mut);
3825
+ this.nodeOverrides.clear();
3826
+ this.edgeOverrides.clear();
2854
3827
  });
2855
3828
  this.state = { graph: graph2, update };
2856
3829
  this.setNodePositions();
2857
3830
  this.seq.splice(this.index + 1);
2858
3831
  this.seq.push(this.state);
2859
3832
  this.nav("last");
3833
+ if (this.events.historyChange)
3834
+ this.events.historyChange(this.index, this.seq.length);
2860
3835
  }
2861
3836
  setNodePositions() {
2862
3837
  const { graph: graph2 } = this.state;
@@ -2866,11 +3841,6 @@ var API = class {
2866
3841
  this.canvas.getNode(node.key).setPos(node.pos);
2867
3842
  }
2868
3843
  }
2869
- async update(callback) {
2870
- const updater = new Updater();
2871
- callback(updater);
2872
- await this.applyUpdate(updater.update);
2873
- }
2874
3844
  async measureNodes(update) {
2875
3845
  const data = [];
2876
3846
  for (const set of [update.updateNodes, update.addNodes])
@@ -2885,7 +3855,10 @@ var API = class {
2885
3855
  else if (!data) throw new Error(`invalid node ${data}`);
2886
3856
  else if (typeof data == "string") props = { id: data };
2887
3857
  else if (typeof data == "object") props = data;
2888
- else throw new Error(`invalid node ${data}`);
3858
+ else throw new Error(`invalid node ${JSON.stringify(data)}`);
3859
+ this.detectNodeFields(data);
3860
+ const overrides = this.nodeOverrides.get(data);
3861
+ if (overrides) props = { ...props, ...overrides };
2889
3862
  let { id, title, text, type, render } = props;
2890
3863
  id ??= this.getNodeId(data);
2891
3864
  const ports = this.parsePorts(props.ports);
@@ -2895,6 +3868,21 @@ var API = class {
2895
3868
  this.nodeVersions.set(data, version);
2896
3869
  return { id, data, ports, title, text, type, render, version };
2897
3870
  }
3871
+ detectNodeFields(data) {
3872
+ if (typeof data != "object" || !data) return;
3873
+ const skip = /* @__PURE__ */ new Set(["id", "ports", "render", "version"]);
3874
+ for (const [key, value] of Object.entries(data)) {
3875
+ if (skip.has(key)) continue;
3876
+ if (value === null || value === void 0) continue;
3877
+ const type = typeof value;
3878
+ if (type === "string" || type === "number" || type === "boolean") {
3879
+ this.nodeFields.set(key, type);
3880
+ }
3881
+ }
3882
+ }
3883
+ getNodeFields() {
3884
+ return this.nodeFields;
3885
+ }
2898
3886
  parseEdge(data) {
2899
3887
  const get = this.options.props.edge;
2900
3888
  let props;
@@ -2903,8 +3891,10 @@ var API = class {
2903
3891
  else if (typeof data == "string") props = this.parseStringEdge(data);
2904
3892
  else if (typeof data == "object") props = data;
2905
3893
  else throw new Error(`invalid edge ${data}`);
3894
+ const overrides = this.edgeOverrides.get(data);
3895
+ if (overrides) props = { ...props, ...overrides };
2906
3896
  let { id, source, target, type } = props;
2907
- id ??= this.getEdgeId(data);
3897
+ if (!id) id = this.getEdgeId(data);
2908
3898
  source = this.parseEdgeEnd(source);
2909
3899
  target = this.parseEdgeEnd(target);
2910
3900
  const edge = { id, source, target, type, data };
@@ -2917,14 +3907,14 @@ var API = class {
2917
3907
  const keys = Object.keys(end);
2918
3908
  const pidx = keys.indexOf("port");
2919
3909
  if (pidx != -1) {
2920
- if (typeof end.port != "string") return end;
3910
+ if (end.port !== void 0 && typeof end.port != "string") return end;
2921
3911
  keys.splice(pidx, 1);
2922
3912
  }
2923
3913
  if (keys.length != 1) return end;
2924
3914
  if (keys[0] == "id") return end;
2925
3915
  if (keys[0] != "node") return end;
2926
3916
  const id = this.nodeIds.get(end.node);
2927
- if (!id) throw new Error(`edge end ${end} references unknown node ${end.node}`);
3917
+ if (!id) throw new Error(`edge end references unknown node ${end.node}`);
2928
3918
  return { id, port: end.port };
2929
3919
  }
2930
3920
  throw new Error(`invalid edge end ${end}`);
@@ -2934,13 +3924,19 @@ var API = class {
2934
3924
  return { source, target };
2935
3925
  }
2936
3926
  parsePorts(ports) {
2937
- const fixed = { in: null, out: null };
3927
+ const fixed = {};
2938
3928
  for (const key of ["in", "out"]) {
2939
3929
  if (ports?.[key] && ports[key].length > 0)
2940
3930
  fixed[key] = ports[key].map((port) => typeof port == "string" ? { id: port } : port);
2941
3931
  }
2942
3932
  return fixed;
2943
3933
  }
3934
+ getNode(id) {
3935
+ return this.graph.getNode(id);
3936
+ }
3937
+ getEdge(id) {
3938
+ return this.graph.getEdge(id);
3939
+ }
2944
3940
  getNodeId(node) {
2945
3941
  let id = this.nodeIds.get(node);
2946
3942
  if (!id) {
@@ -2960,7 +3956,6 @@ var API = class {
2960
3956
  _addNode(node, mut) {
2961
3957
  const { data, id: newId } = node.data;
2962
3958
  const oldId = this.nodeIds.get(data);
2963
- console.log("addNode", node, oldId, newId);
2964
3959
  if (oldId && oldId != newId)
2965
3960
  throw new Error(`node id of ${data} changed from ${oldId} to ${newId}`);
2966
3961
  this.nodeIds.set(data, newId);
@@ -2968,36 +3963,1152 @@ var API = class {
2968
3963
  }
2969
3964
  _removeNode(node, mut) {
2970
3965
  const id = this.nodeIds.get(node);
2971
- if (!id) throw new Error(`removing node ${node} which does not exist`);
3966
+ if (!id) throw new Error(`removing node ${JSON.stringify(node)} which does not exist`);
2972
3967
  mut.removeNode({ id });
2973
3968
  }
2974
3969
  _updateNode(node, mut) {
2975
3970
  const { data, id: newId } = node.data;
2976
3971
  const oldId = this.nodeIds.get(data);
2977
- if (!oldId) throw new Error(`updating unknown node ${node}`);
2978
- if (oldId != newId) throw new Error(`node id changed from ${oldId} to ${newId}`);
3972
+ if (!oldId) throw new Error(`updating unknown node ${JSON.stringify(node)} `);
3973
+ if (oldId != newId) throw new Error(`node id changed from ${oldId} to ${newId} `);
2979
3974
  mut.updateNode(node.data);
2980
3975
  }
2981
3976
  _addEdge(edge, mut) {
2982
3977
  const data = this.parseEdge(edge);
2983
3978
  const id = this.edgeIds.get(edge);
2984
3979
  if (id && id != data.id)
2985
- throw new Error(`edge id changed from ${id} to ${data.id}`);
3980
+ throw new Error(`edge id changed from ${id} to ${data.id} `);
2986
3981
  this.edgeIds.set(edge, data.id);
2987
3982
  mut.addEdge(data);
2988
3983
  }
2989
3984
  _removeEdge(edge, mut) {
2990
3985
  const id = this.edgeIds.get(edge);
2991
- if (!id) throw new Error(`removing edge ${edge} which does not exist`);
3986
+ if (!id) throw new Error(`removing edge ${JSON.stringify(edge)} which does not exist`);
2992
3987
  mut.removeEdge(this.parseEdge(edge));
2993
3988
  }
2994
3989
  _updateEdge(edge, mut) {
2995
3990
  const id = this.edgeIds.get(edge);
2996
- if (!id) throw new Error(`updating unknown edge ${edge}`);
3991
+ if (!id) throw new Error(`updating unknown edge ${JSON.stringify(edge)} `);
2997
3992
  const data = this.parseEdge(edge);
2998
- if (data.id !== id) throw new Error(`edge id changed from ${id} to ${data.id}`);
3993
+ if (data.id !== id) throw new Error(`edge id changed from ${id} to ${data.id} `);
2999
3994
  mut.updateEdge(data);
3000
3995
  }
3996
+ // Event Handlers
3997
+ handleClickNode(id) {
3998
+ const handler = this.events.nodeClick;
3999
+ const node = this.graph.getNode(id);
4000
+ if (handler) handler(node.data);
4001
+ }
4002
+ handleClickEdge(id) {
4003
+ const handler = this.events.edgeClick;
4004
+ if (!handler) return;
4005
+ const seg = this.graph.getSeg(id);
4006
+ if (seg.edgeIds.size != 1) return;
4007
+ const edge = this.graph.getEdge(seg.edgeIds.values().next().value);
4008
+ handler(edge.data);
4009
+ }
4010
+ async handleNewNode() {
4011
+ const gotNode = async (node) => {
4012
+ await this.addNode(node);
4013
+ };
4014
+ if (this.events.newNode)
4015
+ this.events.newNode(gotNode);
4016
+ else
4017
+ this.canvas.showNewNodeModal(async (data) => {
4018
+ if (this.events.addNode)
4019
+ this.events.addNode(data, gotNode);
4020
+ else
4021
+ await gotNode(data);
4022
+ });
4023
+ }
4024
+ async handleNewNodeFrom(source) {
4025
+ const gotNode = async (node) => {
4026
+ const gotEdge = async (edge) => {
4027
+ await this.update((u) => {
4028
+ u.addNode(node).addEdge(edge);
4029
+ });
4030
+ };
4031
+ const data = this.graph.getNode(source.id).data;
4032
+ const newEdge = {
4033
+ source: { node: data, port: source.port },
4034
+ target: { node }
4035
+ };
4036
+ if (this.events.addEdge)
4037
+ this.events.addEdge(newEdge, gotEdge);
4038
+ else
4039
+ await gotEdge(newEdge);
4040
+ };
4041
+ if (this.events.newNode)
4042
+ this.events.newNode(gotNode);
4043
+ else
4044
+ this.canvas.showNewNodeModal(async (data) => {
4045
+ if (this.events.addNode)
4046
+ this.events.addNode(data, gotNode);
4047
+ else
4048
+ await gotNode(data);
4049
+ });
4050
+ }
4051
+ async handleEditNode(id) {
4052
+ const node = this.graph.getNode(id);
4053
+ const gotNode = async (node2) => {
4054
+ if (node2) await this.updateNode(node2);
4055
+ };
4056
+ if (this.events.editNode)
4057
+ this.events.editNode(node.data, gotNode);
4058
+ else {
4059
+ this.canvas.showEditNodeModal(node, async (data) => {
4060
+ if (this.events.updateNode)
4061
+ this.events.updateNode(node.data, data, gotNode);
4062
+ else {
4063
+ this.nodeOverrides.set(node.data, data);
4064
+ await gotNode(node.data);
4065
+ }
4066
+ });
4067
+ }
4068
+ }
4069
+ async handleEditEdge(id) {
4070
+ const seg = this.graph.getSeg(id);
4071
+ if (seg.edgeIds.size != 1) return;
4072
+ const edge = this.graph.getEdge(seg.edgeIds.values().next().value);
4073
+ const gotEdge = async (edge2) => {
4074
+ if (edge2) await this.updateEdge(edge2);
4075
+ };
4076
+ if (this.events.editEdge)
4077
+ this.events.editEdge(edge.data, gotEdge);
4078
+ else
4079
+ this.canvas.showEditEdgeModal(edge, async (data) => {
4080
+ const sourceNode = edge.sourceNode(this.graph);
4081
+ const targetNode = edge.targetNode(this.graph);
4082
+ const update = {
4083
+ source: { node: sourceNode.data, port: data.source.port, marker: data.source.marker },
4084
+ target: { node: targetNode.data, port: data.target.port, marker: data.target.marker }
4085
+ };
4086
+ if (this.events.updateEdge)
4087
+ this.events.updateEdge(edge.data, update, gotEdge);
4088
+ else {
4089
+ this.edgeOverrides.set(edge.data, {
4090
+ source: { id: sourceNode.id, port: data.source.port, marker: data.source.marker },
4091
+ target: { id: targetNode.id, port: data.target.port, marker: data.target.marker },
4092
+ type: data.type
4093
+ });
4094
+ await gotEdge(edge.data);
4095
+ }
4096
+ });
4097
+ }
4098
+ async handleAddEdge(data) {
4099
+ const gotEdge = async (edge) => {
4100
+ if (edge) await this.addEdge(edge);
4101
+ };
4102
+ const newEdge = {
4103
+ source: { node: this.graph.getNode(data.source.id).data, port: data.source.port, marker: data.source.marker },
4104
+ target: { node: this.graph.getNode(data.target.id).data, port: data.target.port, marker: data.target.marker }
4105
+ };
4106
+ if (this.events.addEdge)
4107
+ this.events.addEdge(newEdge, gotEdge);
4108
+ else
4109
+ await gotEdge(data);
4110
+ }
4111
+ async handleDeleteNode(id) {
4112
+ const node = this.getNode(id);
4113
+ if (this.events.removeNode)
4114
+ this.events.removeNode(node.data, async (remove) => {
4115
+ if (remove) await this.deleteNode(node.data);
4116
+ });
4117
+ else
4118
+ await this.deleteNode(node.data);
4119
+ }
4120
+ async handleDeleteEdge(id) {
4121
+ const edge = this.getEdge(id);
4122
+ if (this.events.removeEdge)
4123
+ this.events.removeEdge(edge.data, async (remove) => {
4124
+ if (remove) await this.deleteEdge(edge.data);
4125
+ });
4126
+ else
4127
+ await this.deleteEdge(edge.data);
4128
+ }
4129
+ };
4130
+
4131
+ // src/api/ingest.ts
4132
+ var Ingest = class {
4133
+ constructor(api) {
4134
+ this.api = api;
4135
+ }
4136
+ /**
4137
+ * Apply an incoming ingest message to the API.
4138
+ * - snapshot: rebuild state from nodes/edges (clears prior history)
4139
+ * - update: apply incremental update
4140
+ * - history: initialize from a set of frames (clears prior history)
4141
+ */
4142
+ async apply(msg) {
4143
+ switch (msg.type) {
4144
+ case "snapshot": {
4145
+ await this.api.replaceSnapshot(msg.nodes, msg.edges, msg.description);
4146
+ break;
4147
+ }
4148
+ case "update": {
4149
+ await this.api.update((u) => {
4150
+ if (msg.addNodes) u.addNodes(...msg.addNodes);
4151
+ if (msg.removeNodes) u.deleteNodes(...msg.removeNodes);
4152
+ if (msg.updateNodes) u.updateNodes(...msg.updateNodes);
4153
+ if (msg.addEdges) u.addEdges(...msg.addEdges);
4154
+ if (msg.removeEdges) u.deleteEdges(...msg.removeEdges);
4155
+ if (msg.updateEdges) u.updateEdges(...msg.updateEdges);
4156
+ if (msg.description) u.describe(msg.description);
4157
+ });
4158
+ break;
4159
+ }
4160
+ case "history": {
4161
+ await this.api.replaceHistory(msg.frames);
4162
+ break;
4163
+ }
4164
+ }
4165
+ }
4166
+ };
4167
+
4168
+ // src/api/sources/WebSocketSource.ts
4169
+ var WebSocketSource = class {
4170
+ url;
4171
+ ws = null;
4172
+ onMessage;
4173
+ onStatus;
4174
+ reconnectMs;
4175
+ closedByUser = false;
4176
+ connectStartTime = null;
4177
+ totalTimeoutMs = 1e4;
4178
+ totalTimeoutTimer = null;
4179
+ constructor(url, onMessage, onStatus, reconnectMs = 1500) {
4180
+ this.url = url;
4181
+ this.onMessage = onMessage;
4182
+ this.onStatus = onStatus;
4183
+ this.reconnectMs = reconnectMs;
4184
+ }
4185
+ connect() {
4186
+ this.closedByUser = false;
4187
+ this.connectStartTime = Date.now();
4188
+ this.startTotalTimeout();
4189
+ this.open();
4190
+ }
4191
+ disconnect() {
4192
+ this.closedByUser = true;
4193
+ this.clearTotalTimeout();
4194
+ if (this.ws) {
4195
+ try {
4196
+ this.ws.close();
4197
+ } catch {
4198
+ }
4199
+ this.ws = null;
4200
+ }
4201
+ this.onStatus?.("closed");
4202
+ }
4203
+ startTotalTimeout() {
4204
+ this.clearTotalTimeout();
4205
+ this.totalTimeoutTimer = window.setTimeout(() => {
4206
+ if (!this.closedByUser && this.ws?.readyState !== WebSocket.OPEN) {
4207
+ this.closedByUser = true;
4208
+ if (this.ws) {
4209
+ try {
4210
+ this.ws.close();
4211
+ } catch {
4212
+ }
4213
+ this.ws = null;
4214
+ }
4215
+ this.clearTotalTimeout();
4216
+ this.onStatus?.("error", new Error("Connection timeout after 10 seconds"));
4217
+ }
4218
+ }, this.totalTimeoutMs);
4219
+ }
4220
+ clearTotalTimeout() {
4221
+ if (this.totalTimeoutTimer !== null) {
4222
+ clearTimeout(this.totalTimeoutTimer);
4223
+ this.totalTimeoutTimer = null;
4224
+ }
4225
+ this.connectStartTime = null;
4226
+ }
4227
+ open() {
4228
+ if (this.connectStartTime && Date.now() - this.connectStartTime >= this.totalTimeoutMs) {
4229
+ if (!this.closedByUser) {
4230
+ this.closedByUser = true;
4231
+ this.clearTotalTimeout();
4232
+ this.onStatus?.("error", new Error("Connection timeout after 10 seconds"));
4233
+ }
4234
+ return;
4235
+ }
4236
+ this.onStatus?.(this.ws ? "reconnecting" : "connecting");
4237
+ const ws = new WebSocket(this.url);
4238
+ this.ws = ws;
4239
+ ws.onopen = () => {
4240
+ this.clearTotalTimeout();
4241
+ this.onStatus?.("connected");
4242
+ };
4243
+ ws.onerror = (e) => {
4244
+ this.onStatus?.("error", e);
4245
+ };
4246
+ ws.onclose = () => {
4247
+ if (this.closedByUser) {
4248
+ this.onStatus?.("closed");
4249
+ return;
4250
+ }
4251
+ if (this.connectStartTime && Date.now() - this.connectStartTime >= this.totalTimeoutMs) {
4252
+ this.closedByUser = true;
4253
+ this.clearTotalTimeout();
4254
+ this.onStatus?.("error", new Error("Connection timeout after 10 seconds"));
4255
+ return;
4256
+ }
4257
+ this.onStatus?.("reconnecting");
4258
+ setTimeout(() => this.open(), this.reconnectMs);
4259
+ };
4260
+ ws.onmessage = (ev) => {
4261
+ const data = typeof ev.data === "string" ? ev.data : "";
4262
+ const lines = data.split("\n").map((l) => l.trim()).filter(Boolean);
4263
+ for (const line of lines) {
4264
+ try {
4265
+ const obj = JSON.parse(line);
4266
+ this.onMessage(obj);
4267
+ } catch {
4268
+ }
4269
+ }
4270
+ };
4271
+ }
4272
+ };
4273
+
4274
+ // src/api/sources/FileSystemSource.ts
4275
+ var FileSystemSource = class {
4276
+ handle = null;
4277
+ onMessage;
4278
+ onStatus;
4279
+ timer = null;
4280
+ lastSize = 0;
4281
+ filename;
4282
+ intervalMs;
4283
+ constructor(onMessage, onStatus, filename = "graph.ndjson", intervalMs = 1e3) {
4284
+ this.onMessage = onMessage;
4285
+ this.onStatus = onStatus;
4286
+ this.filename = filename;
4287
+ this.intervalMs = intervalMs;
4288
+ }
4289
+ async openDirectory() {
4290
+ try {
4291
+ const dir = await window.showDirectoryPicker?.();
4292
+ if (!dir) throw new Error("File System Access not supported or cancelled");
4293
+ const handle = await dir.getFileHandle(this.filename, { create: false });
4294
+ this.handle = handle;
4295
+ this.onStatus?.("opened", { file: this.filename });
4296
+ this.lastSize = 0;
4297
+ this.startPolling();
4298
+ } catch (e) {
4299
+ this.onStatus?.("error", e);
4300
+ }
4301
+ }
4302
+ close() {
4303
+ if (this.timer) {
4304
+ window.clearInterval(this.timer);
4305
+ this.timer = null;
4306
+ }
4307
+ this.handle = null;
4308
+ this.onStatus?.("closed");
4309
+ }
4310
+ startPolling() {
4311
+ if (this.timer) window.clearInterval(this.timer);
4312
+ this.timer = window.setInterval(() => this.readNewLines(), this.intervalMs);
4313
+ }
4314
+ async readNewLines() {
4315
+ try {
4316
+ if (!this.handle) return;
4317
+ this.onStatus?.("reading");
4318
+ const file = await this.handle.getFile();
4319
+ if (file.size === this.lastSize) return;
4320
+ const slice = await file.slice(this.lastSize).text();
4321
+ this.lastSize = file.size;
4322
+ const lines = slice.split("\n").map((l) => l.trim()).filter(Boolean);
4323
+ for (const line of lines) {
4324
+ try {
4325
+ const obj = JSON.parse(line);
4326
+ this.onMessage(obj);
4327
+ } catch {
4328
+ }
4329
+ }
4330
+ } catch (e) {
4331
+ this.onStatus?.("error", e);
4332
+ }
4333
+ }
4334
+ };
4335
+
4336
+ // src/api/sources/FileSource.ts
4337
+ var FileSource = class {
4338
+ url;
4339
+ onMessage;
4340
+ onStatus;
4341
+ timer = null;
4342
+ lastETag = null;
4343
+ lastContent = "";
4344
+ intervalMs = 1e3;
4345
+ closed = false;
4346
+ constructor(url, onMessage, onStatus, intervalMs = 1e3) {
4347
+ this.url = url;
4348
+ this.onMessage = onMessage;
4349
+ this.onStatus = onStatus;
4350
+ this.intervalMs = intervalMs;
4351
+ }
4352
+ async connect() {
4353
+ this.closed = false;
4354
+ this.lastETag = null;
4355
+ this.lastContent = "";
4356
+ this.onStatus?.("opened");
4357
+ this.startPolling();
4358
+ }
4359
+ close() {
4360
+ this.closed = true;
4361
+ if (this.timer) {
4362
+ window.clearInterval(this.timer);
4363
+ this.timer = null;
4364
+ }
4365
+ this.onStatus?.("closed");
4366
+ }
4367
+ startPolling() {
4368
+ if (this.timer) window.clearInterval(this.timer);
4369
+ this.timer = window.setInterval(() => this.poll(), this.intervalMs);
4370
+ this.poll();
4371
+ }
4372
+ async poll() {
4373
+ if (this.closed) return;
4374
+ try {
4375
+ this.onStatus?.("reading");
4376
+ const headers = {};
4377
+ if (this.lastETag) {
4378
+ headers["If-None-Match"] = this.lastETag;
4379
+ }
4380
+ const response = await fetch(this.url, { headers });
4381
+ if (response.status === 304) {
4382
+ return;
4383
+ }
4384
+ if (!response.ok) {
4385
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
4386
+ }
4387
+ const etag = response.headers.get("ETag");
4388
+ if (etag) {
4389
+ this.lastETag = etag;
4390
+ }
4391
+ const content = await response.text();
4392
+ if (content === this.lastContent) {
4393
+ return;
4394
+ }
4395
+ const lines = content.split("\n").map((l) => l.trim()).filter(Boolean);
4396
+ const lastContentLines = this.lastContent.split("\n").map((l) => l.trim()).filter(Boolean);
4397
+ const newLines = lines.slice(lastContentLines.length);
4398
+ for (const line of newLines) {
4399
+ try {
4400
+ const obj = JSON.parse(line);
4401
+ this.onMessage(obj);
4402
+ } catch {
4403
+ }
4404
+ }
4405
+ this.lastContent = content;
4406
+ } catch (e) {
4407
+ this.onStatus?.("error", e);
4408
+ }
4409
+ }
4410
+ };
4411
+
4412
+ // src/playground/playground.ts
4413
+ var import_styles2 = __toESM(require("./styles.css?raw"), 1);
4414
+ var Playground = class {
4415
+ options;
4416
+ rootElement;
4417
+ currentExample;
4418
+ currentGraph = null;
4419
+ ingest = null;
4420
+ isEditable = false;
4421
+ wsSource = null;
4422
+ fsSource = null;
4423
+ fileSource = null;
4424
+ wsStatus = "disconnected";
4425
+ fsStatus = "disconnected";
4426
+ fileStatus = "disconnected";
4427
+ activeSourceType = null;
4428
+ wsUrl = "ws://localhost:8787";
4429
+ sourceModal = null;
4430
+ helpOverlay = null;
4431
+ exampleList;
4432
+ graphContainerId;
4433
+ constructor(options) {
4434
+ this.options = options;
4435
+ this.exampleList = Object.keys(options.examples);
4436
+ this.currentExample = options.defaultExample || this.exampleList[0];
4437
+ this.graphContainerId = `playground-graph-${Math.random().toString(36).substr(2, 9)}`;
4438
+ if (typeof options.root === "string") {
4439
+ const el = document.getElementById(options.root);
4440
+ if (!el) throw new Error(`Element with id "${options.root}" not found`);
4441
+ this.rootElement = el;
4442
+ } else {
4443
+ this.rootElement = options.root;
4444
+ }
4445
+ }
4446
+ async init() {
4447
+ this.injectStyles();
4448
+ this.createDOM();
4449
+ this.setupEventListeners();
4450
+ await this.renderGraph();
4451
+ this.updateSourceIcon();
4452
+ this.connectExampleSource();
4453
+ const rebuildBtn = this.rootElement.querySelector("#rebuild");
4454
+ if (rebuildBtn) rebuildBtn.disabled = !this.isEditable;
4455
+ }
4456
+ injectStyles() {
4457
+ if (!document.getElementById("g3p-playground-styles")) {
4458
+ const styleEl = document.createElement("style");
4459
+ styleEl.id = "g3p-playground-styles";
4460
+ styleEl.textContent = import_styles2.default;
4461
+ document.head.appendChild(styleEl);
4462
+ }
4463
+ }
4464
+ createDOM() {
4465
+ const exampleList = this.exampleList.map((key, i) => {
4466
+ const example = this.options.examples[key];
4467
+ const isActive = i === 0 || key === this.currentExample;
4468
+ return `
4469
+ <button class="example-btn ${isActive ? "active" : ""}" data-example="${key}">
4470
+ ${example.name}
4471
+ </button>
4472
+ `;
4473
+ }).join("");
4474
+ this.rootElement.innerHTML = `
4475
+ <main class="playground">
4476
+ <div class="sidebar">
4477
+ <h2>Examples</h2>
4478
+ <div class="example-list">
4479
+ ${exampleList}
4480
+ </div>
4481
+
4482
+ <h2>Options</h2>
4483
+ <div class="options">
4484
+ <div class="option-group">
4485
+ <label>Orientation</label>
4486
+ <select id="orientation">
4487
+ <option value="TB">Top to Bottom</option>
4488
+ <option value="BT">Bottom to Top</option>
4489
+ <option value="LR">Left to Right</option>
4490
+ <option value="RL">Right to Left</option>
4491
+ </select>
4492
+ </div>
4493
+
4494
+ <div class="option-group">
4495
+ <label>Port Style</label>
4496
+ <select id="portStyle">
4497
+ <option value="outside">Outside</option>
4498
+ <option value="inside">Inside</option>
4499
+ </select>
4500
+ </div>
4501
+
4502
+ <div class="option-group">
4503
+ <label>
4504
+ <input type="checkbox" id="portLabelRotate" />
4505
+ Rotate Port Labels
4506
+ </label>
4507
+ </div>
4508
+
4509
+ <div class="option-group">
4510
+ <label>Theme</label>
4511
+ <select id="colorMode">
4512
+ <option value="system">System</option>
4513
+ <option value="light">Light</option>
4514
+ <option value="dark">Dark</option>
4515
+ </select>
4516
+ </div>
4517
+ </div>
4518
+ </div>
4519
+
4520
+ <div class="graph-area">
4521
+ <div class="graph-toolbar">
4522
+ <div class="nav-controls">
4523
+ <button class="nav-btn" id="nav-first" title="First (Home)">\u23EE</button>
4524
+ <button class="nav-btn" id="nav-prev" title="Previous (\u2190)">\u25C0</button>
4525
+ <span id="history-label" style="min-width: 4rem; text-align: center; display: inline-flex; align-items: center; justify-content: center; height: 2.25rem;">\u2014 / \u2014</span>
4526
+ <button class="nav-btn" id="nav-next" title="Next (\u2192)">\u25B6</button>
4527
+ <button class="nav-btn" id="nav-last" title="Last (End)">\u23ED</button>
4528
+ </div>
4529
+ <div class="connect-controls" style="display:flex; gap:.5rem; align-items:center;">
4530
+ <button class="nav-btn source-icon-btn" id="source-icon" title="Data Source Connection">\u{1F4E1}</button>
4531
+ </div>
4532
+ <button class="nav-btn" id="help-btn" title="How to edit">\u2753</button>
4533
+ <button class="nav-btn" id="edit-toggle" title="Toggle edit mode">\u270E Edit</button>
4534
+ <button class="nav-btn" id="rebuild" title="Rebuild graph from scratch">\u{1F504} Rebuild</button>
4535
+ </div>
4536
+ <div class="graph-container" id="${this.graphContainerId}"></div>
4537
+ </div>
4538
+ </main>
4539
+ `;
4540
+ }
4541
+ setupEventListeners() {
4542
+ this.rootElement.querySelectorAll(".example-btn").forEach((btn) => {
4543
+ btn.addEventListener("click", () => {
4544
+ this.rootElement.querySelectorAll(".example-btn").forEach((b) => b.classList.remove("active"));
4545
+ btn.classList.add("active");
4546
+ this.currentExample = btn.getAttribute("data-example") || this.exampleList[0];
4547
+ this.renderGraph();
4548
+ this.connectExampleSource();
4549
+ });
4550
+ });
4551
+ this.rootElement.querySelectorAll(".options select, .options input").forEach((el) => {
4552
+ el.addEventListener("change", () => this.renderGraph());
4553
+ });
4554
+ this.rootElement.querySelector("#nav-first")?.addEventListener("click", () => {
4555
+ this.currentGraph?.nav("first");
4556
+ this.updateHistoryLabel();
4557
+ });
4558
+ this.rootElement.querySelector("#nav-prev")?.addEventListener("click", () => {
4559
+ this.currentGraph?.nav("prev");
4560
+ this.updateHistoryLabel();
4561
+ });
4562
+ this.rootElement.querySelector("#nav-next")?.addEventListener("click", () => {
4563
+ this.currentGraph?.nav("next");
4564
+ this.updateHistoryLabel();
4565
+ });
4566
+ this.rootElement.querySelector("#nav-last")?.addEventListener("click", () => {
4567
+ this.currentGraph?.nav("last");
4568
+ this.updateHistoryLabel();
4569
+ });
4570
+ this.rootElement.querySelector("#rebuild")?.addEventListener("click", () => {
4571
+ this.currentGraph?.rebuild();
4572
+ });
4573
+ this.rootElement.querySelector("#edit-toggle")?.addEventListener("click", () => {
4574
+ this.isEditable = !this.isEditable;
4575
+ const btn = this.rootElement.querySelector("#edit-toggle");
4576
+ if (btn) btn.textContent = this.isEditable ? "\u2713 Done" : "\u270E Edit";
4577
+ const rebuildBtn = this.rootElement.querySelector("#rebuild");
4578
+ if (rebuildBtn) rebuildBtn.disabled = !this.isEditable;
4579
+ try {
4580
+ this.currentGraph?.setEditable?.(this.isEditable);
4581
+ } catch {
4582
+ }
4583
+ });
4584
+ this.rootElement.querySelector("#help-btn")?.addEventListener("click", () => this.openHelp());
4585
+ const sourceIconBtn = this.rootElement.querySelector("#source-icon");
4586
+ if (sourceIconBtn) {
4587
+ sourceIconBtn.addEventListener("click", (e) => {
4588
+ e.preventDefault();
4589
+ e.stopPropagation();
4590
+ this.openSourceModal();
4591
+ });
4592
+ }
4593
+ document.addEventListener("keydown", (e) => {
4594
+ if (!this.currentGraph) return;
4595
+ if (e.target instanceof HTMLInputElement || e.target instanceof HTMLSelectElement) return;
4596
+ switch (e.key) {
4597
+ case "Home":
4598
+ this.currentGraph.nav("first");
4599
+ this.updateHistoryLabel();
4600
+ break;
4601
+ case "End":
4602
+ this.currentGraph.nav("last");
4603
+ this.updateHistoryLabel();
4604
+ break;
4605
+ case "ArrowLeft":
4606
+ this.currentGraph.nav("prev");
4607
+ this.updateHistoryLabel();
4608
+ break;
4609
+ case "ArrowRight":
4610
+ this.currentGraph.nav("next");
4611
+ this.updateHistoryLabel();
4612
+ break;
4613
+ }
4614
+ });
4615
+ }
4616
+ getResolvedColorMode() {
4617
+ const mode = this.rootElement.querySelector("#colorMode")?.value;
4618
+ if (mode === "system") {
4619
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
4620
+ }
4621
+ return mode;
4622
+ }
4623
+ getOptions(exampleOptions) {
4624
+ const orientation = this.rootElement.querySelector("#orientation")?.value;
4625
+ return {
4626
+ graph: { orientation },
4627
+ canvas: {
4628
+ width: "100%",
4629
+ height: "100%",
4630
+ colorMode: this.getResolvedColorMode(),
4631
+ editable: this.isEditable,
4632
+ ...exampleOptions?.canvas
4633
+ }
4634
+ };
4635
+ }
4636
+ async renderGraph() {
4637
+ const container = this.rootElement.querySelector(`#${this.graphContainerId}`);
4638
+ if (!container) return;
4639
+ container.innerHTML = "";
4640
+ const example = this.options.examples[this.currentExample];
4641
+ const options = this.getOptions(example.options);
4642
+ try {
4643
+ this.currentGraph = await graph({
4644
+ root: this.graphContainerId,
4645
+ nodes: example.nodes,
4646
+ edges: example.edges,
4647
+ options,
4648
+ events: {
4649
+ historyChange: () => this.updateHistoryLabel()
4650
+ }
4651
+ });
4652
+ this.ingest = new Ingest(this.currentGraph);
4653
+ this.updateHistoryLabel();
4654
+ } catch (e) {
4655
+ console.error("Failed to render graph:", e);
4656
+ container.innerHTML = '<p style="padding: 2rem; color: #ef4444;">Failed to load graph</p>';
4657
+ }
4658
+ }
4659
+ updateHistoryLabel() {
4660
+ const label = this.rootElement.querySelector("#history-label");
4661
+ if (!label || !this.currentGraph) return;
4662
+ try {
4663
+ const idx = this.currentGraph.getHistoryIndex?.() ?? 0;
4664
+ const len = this.currentGraph.getHistoryLength?.() ?? 1;
4665
+ label.textContent = `${idx + 1} / ${len}`;
4666
+ } catch {
4667
+ label.textContent = "\u2014 / \u2014";
4668
+ }
4669
+ }
4670
+ connectExampleSource() {
4671
+ const example = this.options.examples[this.currentExample];
4672
+ if (!example.source) {
4673
+ this.disconnectAllSources();
4674
+ return;
4675
+ }
4676
+ this.disconnectAllSources();
4677
+ if (example.source.type === "websocket") {
4678
+ this.wsUrl = example.source.url;
4679
+ this.wsSource = new WebSocketSource(example.source.url, this.handleIngestMessage.bind(this), this.updateWsStatus);
4680
+ this.wsSource.connect();
4681
+ } else if (example.source.type === "file") {
4682
+ this.fileSource = new FileSource(example.source.path, this.handleIngestMessage.bind(this), this.updateFileStatus);
4683
+ this.fileSource.connect();
4684
+ }
4685
+ }
4686
+ disconnectAllSources() {
4687
+ this.wsSource?.disconnect();
4688
+ this.fsSource?.close();
4689
+ this.fileSource?.close();
4690
+ this.wsSource = null;
4691
+ this.fsSource = null;
4692
+ this.fileSource = null;
4693
+ this.activeSourceType = null;
4694
+ this.wsStatus = "disconnected";
4695
+ this.fsStatus = "disconnected";
4696
+ this.fileStatus = "disconnected";
4697
+ this.updateSourceIcon();
4698
+ }
4699
+ openHelp() {
4700
+ if (!this.helpOverlay) {
4701
+ this.helpOverlay = document.createElement("div");
4702
+ this.helpOverlay.className = "modal-overlay";
4703
+ this.helpOverlay.innerHTML = `
4704
+ <div class="modal" role="dialog" aria-modal="true" aria-labelledby="help-title">
4705
+ <div class="modal-header">
4706
+ <h3 id="help-title">Editing the Graph</h3>
4707
+ <button class="modal-close" title="Close" aria-label="Close">\xD7</button>
4708
+ </div>
4709
+ <div class="modal-body">
4710
+ <p>Here's how to edit the graph:</p>
4711
+ <ul>
4712
+ <li><strong>Enable editing</strong>: Click "Edit"</li>
4713
+ <li><strong>Add a node</strong>: Double\u2011click an empty area</li>
4714
+ <li><strong>Edit a node</strong>: Double\u2011click a node</li>
4715
+ <li><strong>Edit an edge</strong>: Double\u2011click an edge</li>
4716
+ <li><strong>Create an edge</strong>: Click and drag from a node (or its port) onto another node; press Esc to cancel</li>
4717
+ <li><strong>Pan</strong>: Drag on canvas or edges; <strong>Zoom</strong>: Mouse wheel or controls</li>
4718
+ <li><strong>Rebuild</strong>: Use "Rebuild" to re-layout from scratch (enabled in edit mode)</li>
4719
+ </ul>
4720
+ <p>When you're done, click "Done" to lock the canvas.</p>
4721
+ </div>
4722
+ </div>
4723
+ `;
4724
+ document.body.appendChild(this.helpOverlay);
4725
+ this.helpOverlay.addEventListener("click", (e) => {
4726
+ const target = e.target;
4727
+ if (target.classList.contains("modal-overlay") || target.classList.contains("modal-close")) {
4728
+ this.closeHelp();
4729
+ }
4730
+ });
4731
+ }
4732
+ this.helpOverlay.style.display = "flex";
4733
+ }
4734
+ closeHelp() {
4735
+ if (this.helpOverlay) this.helpOverlay.style.display = "none";
4736
+ }
4737
+ handleIngestMessage = async (msg) => {
4738
+ if (!this.ingest) return;
4739
+ await this.ingest.apply(msg);
4740
+ };
4741
+ updateSourceIcon() {
4742
+ const iconBtn = this.rootElement.querySelector("#source-icon");
4743
+ if (!iconBtn) return;
4744
+ iconBtn.classList.remove("active", "connecting", "error");
4745
+ const isConnected = this.activeSourceType === "ws" && this.wsStatus === "connected" || this.activeSourceType === "folder" && this.fsStatus === "connected" || this.activeSourceType === "file" && this.fileStatus === "connected";
4746
+ const isConnecting = this.activeSourceType === "ws" && this.wsStatus === "connecting" || this.activeSourceType === "folder" && this.fsStatus === "opening" || this.activeSourceType === "file" && this.fileStatus === "connecting";
4747
+ const hasError = this.activeSourceType === "ws" && this.wsStatus === "error" || this.activeSourceType === "folder" && this.fsStatus === "error" || this.activeSourceType === "file" && this.fileStatus === "error";
4748
+ let icon = "\u{1F4E1}";
4749
+ if (this.activeSourceType === "folder") {
4750
+ icon = "\u{1F4C1}";
4751
+ } else if (this.activeSourceType === "file") {
4752
+ icon = "\u{1F4C4}";
4753
+ }
4754
+ if (isConnected) {
4755
+ iconBtn.classList.add("active");
4756
+ iconBtn.textContent = icon;
4757
+ } else if (isConnecting) {
4758
+ iconBtn.classList.add("connecting");
4759
+ iconBtn.textContent = icon;
4760
+ } else if (hasError) {
4761
+ iconBtn.classList.add("error");
4762
+ iconBtn.textContent = icon;
4763
+ } else {
4764
+ iconBtn.textContent = "\u{1F4E1}";
4765
+ }
4766
+ }
4767
+ updateWsStatus = (status, detail) => {
4768
+ if (status === "connecting" || status === "reconnecting") {
4769
+ this.wsStatus = "connecting";
4770
+ this.activeSourceType = "ws";
4771
+ } else if (status === "connected") {
4772
+ this.wsStatus = "connected";
4773
+ this.activeSourceType = "ws";
4774
+ if (this.fsSource && this.fsStatus === "connected") {
4775
+ this.fsSource.close();
4776
+ }
4777
+ if (this.fileSource && this.fileStatus === "connected") {
4778
+ this.fileSource.close();
4779
+ }
4780
+ } else if (status === "error") {
4781
+ this.wsStatus = "error";
4782
+ } else {
4783
+ this.wsStatus = "disconnected";
4784
+ if (this.activeSourceType === "ws") {
4785
+ this.activeSourceType = null;
4786
+ }
4787
+ }
4788
+ this.updateSourceIcon();
4789
+ this.updateSourceModal();
4790
+ };
4791
+ updateFsStatus = (status, detail) => {
4792
+ if (status === "opened") {
4793
+ this.fsStatus = "opening";
4794
+ this.activeSourceType = "folder";
4795
+ if (this.wsSource && this.wsStatus === "connected") {
4796
+ this.wsSource.disconnect();
4797
+ }
4798
+ } else if (status === "reading") {
4799
+ this.fsStatus = "connected";
4800
+ this.activeSourceType = "folder";
4801
+ } else if (status === "error") {
4802
+ this.fsStatus = "error";
4803
+ } else if (status === "closed") {
4804
+ this.fsStatus = "disconnected";
4805
+ if (this.activeSourceType === "folder") {
4806
+ this.activeSourceType = null;
4807
+ }
4808
+ } else {
4809
+ this.fsStatus = "disconnected";
4810
+ }
4811
+ this.updateSourceIcon();
4812
+ this.updateSourceModal();
4813
+ };
4814
+ updateFileStatus = (status, detail) => {
4815
+ if (status === "opened") {
4816
+ this.fileStatus = "connecting";
4817
+ this.activeSourceType = "file";
4818
+ if (this.wsSource && this.wsStatus === "connected") {
4819
+ this.wsSource.disconnect();
4820
+ }
4821
+ if (this.fsSource && this.fsStatus === "connected") {
4822
+ this.fsSource.close();
4823
+ }
4824
+ } else if (status === "reading") {
4825
+ this.fileStatus = "connected";
4826
+ this.activeSourceType = "file";
4827
+ } else if (status === "error") {
4828
+ this.fileStatus = "error";
4829
+ } else if (status === "closed") {
4830
+ this.fileStatus = "disconnected";
4831
+ if (this.activeSourceType === "file") {
4832
+ this.activeSourceType = null;
4833
+ }
4834
+ } else {
4835
+ this.fileStatus = "disconnected";
4836
+ }
4837
+ this.updateSourceIcon();
4838
+ this.updateSourceModal();
4839
+ };
4840
+ createSourceModal() {
4841
+ if (this.sourceModal) return this.sourceModal;
4842
+ this.sourceModal = document.createElement("div");
4843
+ this.sourceModal.className = "modal-overlay";
4844
+ this.sourceModal.style.display = "none";
4845
+ this.sourceModal.innerHTML = `
4846
+ <div class="modal" role="dialog" aria-modal="true" aria-labelledby="source-modal-title">
4847
+ <div class="modal-header">
4848
+ <h3 id="source-modal-title">Data Source Connection</h3>
4849
+ <button class="modal-close" title="Close" aria-label="Close">\xD7</button>
4850
+ </div>
4851
+ <div class="modal-body">
4852
+ <div class="source-type-selector">
4853
+ <button class="source-type-option" data-source="ws">\u{1F4E1} WebSocket</button>
4854
+ <button class="source-type-option" data-source="folder">\u{1F4C1} Folder</button>
4855
+ </div>
4856
+
4857
+ <div class="source-controls" data-source="ws">
4858
+ <div class="form-group">
4859
+ <label for="source-modal-url">WebSocket URL</label>
4860
+ <input type="text" id="source-modal-url" value="${this.wsUrl}" />
4861
+ </div>
4862
+ <div class="button-group">
4863
+ <button id="source-modal-connect-ws" class="primary">Connect</button>
4864
+ <button id="source-modal-disconnect-ws">Disconnect</button>
4865
+ <button id="source-modal-change-ws">Change Connection</button>
4866
+ </div>
4867
+ </div>
4868
+
4869
+ <div class="source-controls" data-source="folder">
4870
+ <div class="form-group">
4871
+ <label>File System Source</label>
4872
+ <p style="font-size: 0.875rem; color: var(--color-text-muted); margin-top: 0.25rem;">
4873
+ Select a directory containing a graph.ndjson file to watch for changes.
4874
+ </p>
4875
+ </div>
4876
+ <div class="button-group">
4877
+ <button id="source-modal-connect-folder" class="primary">Open Folder</button>
4878
+ <button id="source-modal-disconnect-folder">Disconnect</button>
4879
+ </div>
4880
+ </div>
4881
+
4882
+ <div id="source-modal-status"></div>
4883
+ </div>
4884
+ </div>
4885
+ `;
4886
+ document.body.appendChild(this.sourceModal);
4887
+ this.sourceModal.addEventListener("click", (e) => {
4888
+ const target = e.target;
4889
+ if (target.classList.contains("modal-overlay") || target.classList.contains("modal-close")) {
4890
+ this.closeSourceModal();
4891
+ }
4892
+ });
4893
+ this.sourceModal.querySelectorAll(".source-type-option").forEach((btn) => {
4894
+ btn.addEventListener("click", () => {
4895
+ const sourceType = btn.getAttribute("data-source");
4896
+ if (sourceType) {
4897
+ this.selectSourceType(sourceType);
4898
+ }
4899
+ });
4900
+ });
4901
+ document.getElementById("source-modal-connect-ws")?.addEventListener("click", () => this.handleConnect());
4902
+ document.getElementById("source-modal-disconnect-ws")?.addEventListener("click", () => this.handleDisconnect());
4903
+ document.getElementById("source-modal-change-ws")?.addEventListener("click", () => this.handleChangeConnection());
4904
+ document.getElementById("source-modal-connect-folder")?.addEventListener("click", () => this.handleOpenFolder());
4905
+ document.getElementById("source-modal-disconnect-folder")?.addEventListener("click", () => this.handleCloseFolder());
4906
+ document.getElementById("source-modal-url")?.addEventListener("keydown", (e) => {
4907
+ if (e.key === "Enter" && this.wsStatus !== "connected") {
4908
+ this.handleConnect();
4909
+ }
4910
+ });
4911
+ return this.sourceModal;
4912
+ }
4913
+ selectSourceType(type, skipUpdate = false) {
4914
+ if (!this.sourceModal) return;
4915
+ this.sourceModal.querySelectorAll(".source-type-option").forEach((btn) => {
4916
+ if (btn.getAttribute("data-source") === type) {
4917
+ btn.classList.add("active");
4918
+ } else {
4919
+ btn.classList.remove("active");
4920
+ }
4921
+ });
4922
+ this.sourceModal.querySelectorAll(".source-controls").forEach((controls) => {
4923
+ if (controls.getAttribute("data-source") === type) {
4924
+ controls.classList.add("active");
4925
+ } else {
4926
+ controls.classList.remove("active");
4927
+ }
4928
+ });
4929
+ if (!skipUpdate) {
4930
+ this.updateSourceModalContent();
4931
+ }
4932
+ }
4933
+ updateSourceModalContent() {
4934
+ if (!this.sourceModal) return;
4935
+ const urlInput = document.getElementById("source-modal-url");
4936
+ const connectWsBtn = document.getElementById("source-modal-connect-ws");
4937
+ const disconnectWsBtn = document.getElementById("source-modal-disconnect-ws");
4938
+ const changeWsBtn = document.getElementById("source-modal-change-ws");
4939
+ if (urlInput && connectWsBtn && disconnectWsBtn && changeWsBtn) {
4940
+ const isWsConnected = this.wsStatus === "connected";
4941
+ const isWsConnecting = this.wsStatus === "connecting";
4942
+ connectWsBtn.disabled = isWsConnected || isWsConnecting;
4943
+ disconnectWsBtn.disabled = !isWsConnected || isWsConnecting;
4944
+ changeWsBtn.disabled = !isWsConnected || isWsConnecting;
4945
+ urlInput.disabled = isWsConnecting;
4946
+ }
4947
+ const connectFolderBtn = document.getElementById("source-modal-connect-folder");
4948
+ const disconnectFolderBtn = document.getElementById("source-modal-disconnect-folder");
4949
+ if (connectFolderBtn && disconnectFolderBtn) {
4950
+ const isFolderConnected = this.fsStatus === "connected";
4951
+ const isFolderOpening = this.fsStatus === "opening";
4952
+ connectFolderBtn.disabled = isFolderConnected || isFolderOpening;
4953
+ disconnectFolderBtn.disabled = !isFolderConnected || isFolderOpening;
4954
+ }
4955
+ const statusDiv = document.getElementById("source-modal-status");
4956
+ if (!statusDiv) return;
4957
+ const currentUrl = urlInput?.value || this.wsUrl;
4958
+ statusDiv.innerHTML = "";
4959
+ if (this.activeSourceType === "ws") {
4960
+ if (this.wsStatus === "connecting") {
4961
+ statusDiv.innerHTML = `
4962
+ <div class="status-message info">
4963
+ <span class="loading-spinner"></span>
4964
+ Connecting to ${currentUrl}...
4965
+ </div>
4966
+ `;
4967
+ } else if (this.wsStatus === "connected") {
4968
+ statusDiv.innerHTML = `
4969
+ <div class="status-message success">
4970
+ \u2713 Connected to ${currentUrl}
4971
+ </div>
4972
+ `;
4973
+ } else if (this.wsStatus === "error") {
4974
+ statusDiv.innerHTML = `
4975
+ <div class="status-message error">
4976
+ \u2717 Connection error. Please check the URL and try again.
4977
+ </div>
4978
+ `;
4979
+ } else {
4980
+ statusDiv.innerHTML = `
4981
+ <div class="status-message info">
4982
+ Not connected
4983
+ </div>
4984
+ `;
4985
+ }
4986
+ } else if (this.activeSourceType === "folder") {
4987
+ if (this.fsStatus === "opening") {
4988
+ statusDiv.innerHTML = `
4989
+ <div class="status-message info">
4990
+ <span class="loading-spinner"></span>
4991
+ Opening folder...
4992
+ </div>
4993
+ `;
4994
+ } else if (this.fsStatus === "connected") {
4995
+ statusDiv.innerHTML = `
4996
+ <div class="status-message success">
4997
+ \u2713 Folder connected and watching for changes
4998
+ </div>
4999
+ `;
5000
+ } else if (this.fsStatus === "error") {
5001
+ statusDiv.innerHTML = `
5002
+ <div class="status-message error">
5003
+ \u2717 Error opening folder. Please try again.
5004
+ </div>
5005
+ `;
5006
+ } else {
5007
+ statusDiv.innerHTML = `
5008
+ <div class="status-message info">
5009
+ Not connected
5010
+ </div>
5011
+ `;
5012
+ }
5013
+ } else if (this.activeSourceType === "file") {
5014
+ const example = this.options.examples[this.currentExample];
5015
+ const filePath = example.source?.type === "file" ? example.source.path : "";
5016
+ if (this.fileStatus === "connecting") {
5017
+ statusDiv.innerHTML = `
5018
+ <div class="status-message info">
5019
+ <span class="loading-spinner"></span>
5020
+ Connecting to ${filePath}...
5021
+ </div>
5022
+ `;
5023
+ } else if (this.fileStatus === "connected") {
5024
+ statusDiv.innerHTML = `
5025
+ <div class="status-message success">
5026
+ \u2713 Connected to ${filePath}
5027
+ </div>
5028
+ `;
5029
+ } else if (this.fileStatus === "error") {
5030
+ statusDiv.innerHTML = `
5031
+ <div class="status-message error">
5032
+ \u2717 Error loading file. Please check the path and try again.
5033
+ </div>
5034
+ `;
5035
+ } else {
5036
+ statusDiv.innerHTML = `
5037
+ <div class="status-message info">
5038
+ Not connected
5039
+ </div>
5040
+ `;
5041
+ }
5042
+ } else {
5043
+ statusDiv.innerHTML = `
5044
+ <div class="status-message info">
5045
+ Select a source type to connect
5046
+ </div>
5047
+ `;
5048
+ }
5049
+ }
5050
+ updateSourceModal() {
5051
+ if (!this.sourceModal) return;
5052
+ const activeType = (this.activeSourceType === "file" ? "ws" : this.activeSourceType) || "ws";
5053
+ this.selectSourceType(activeType, true);
5054
+ this.updateSourceModalContent();
5055
+ }
5056
+ openSourceModal() {
5057
+ this.createSourceModal();
5058
+ if (this.sourceModal) {
5059
+ const urlInput = document.getElementById("source-modal-url");
5060
+ if (urlInput) {
5061
+ urlInput.value = this.wsUrl;
5062
+ }
5063
+ const activeType = (this.activeSourceType === "file" ? "ws" : this.activeSourceType) || "ws";
5064
+ this.selectSourceType(activeType);
5065
+ this.updateSourceModal();
5066
+ this.sourceModal.style.display = "flex";
5067
+ }
5068
+ }
5069
+ closeSourceModal() {
5070
+ if (this.sourceModal) {
5071
+ this.sourceModal.style.display = "none";
5072
+ }
5073
+ }
5074
+ handleConnect() {
5075
+ const urlInput = document.getElementById("source-modal-url");
5076
+ if (!urlInput) return;
5077
+ const url = urlInput.value.trim() || "ws://localhost:8787";
5078
+ this.wsUrl = url;
5079
+ if (this.wsSource) {
5080
+ this.wsSource.disconnect();
5081
+ }
5082
+ this.wsSource = new WebSocketSource(url, this.handleIngestMessage.bind(this), this.updateWsStatus);
5083
+ this.wsSource.connect();
5084
+ this.updateSourceModal();
5085
+ }
5086
+ handleDisconnect() {
5087
+ this.wsSource?.disconnect();
5088
+ this.updateSourceModal();
5089
+ }
5090
+ handleChangeConnection() {
5091
+ if (this.wsSource) {
5092
+ this.wsSource.disconnect();
5093
+ }
5094
+ const urlInput = document.getElementById("source-modal-url");
5095
+ if (urlInput) {
5096
+ urlInput.focus();
5097
+ urlInput.select();
5098
+ }
5099
+ this.updateSourceModal();
5100
+ }
5101
+ async handleOpenFolder() {
5102
+ if (!this.fsSource) {
5103
+ this.fsSource = new FileSystemSource(this.handleIngestMessage.bind(this), this.updateFsStatus);
5104
+ }
5105
+ this.updateSourceModal();
5106
+ await this.fsSource.openDirectory();
5107
+ }
5108
+ handleCloseFolder() {
5109
+ this.fsSource?.close();
5110
+ this.updateSourceModal();
5111
+ }
3001
5112
  };
3002
5113
 
3003
5114
  // src/index.ts
@@ -3009,5 +5120,10 @@ async function graph(args = { root: "app" }) {
3009
5120
  var index_default = graph;
3010
5121
  // Annotate the CommonJS export names for ESM import in node:
3011
5122
  0 && (module.exports = {
5123
+ FileSource,
5124
+ FileSystemSource,
5125
+ Ingest,
5126
+ Playground,
5127
+ WebSocketSource,
3012
5128
  graph
3013
5129
  });