@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.js CHANGED
@@ -3,6 +3,70 @@ import { Map as IMap, List as IList, Set as ISet6 } from "immutable";
3
3
 
4
4
  // src/graph/node.ts
5
5
  import { Record, Set as ISet } from "immutable";
6
+
7
+ // src/log.ts
8
+ import pino from "pino";
9
+ var resolvedLevel = typeof globalThis !== "undefined" && globalThis.__LOG_LEVEL || typeof process !== "undefined" && process.env?.LOG_LEVEL || "debug";
10
+ var browserOpts = { asObject: true };
11
+ browserOpts.transmit = {
12
+ level: resolvedLevel,
13
+ send: (level, log12) => {
14
+ try {
15
+ const endpoint = typeof globalThis !== "undefined" && globalThis.__LOG_INGEST_URL || typeof process !== "undefined" && process.env?.LOG_INGEST_URL || void 0;
16
+ if (!endpoint || typeof window === "undefined") {
17
+ try {
18
+ console.debug("[graph-core] transmit skipped", { endpoint, hasWindow: typeof window !== "undefined", level });
19
+ } catch {
20
+ }
21
+ return;
22
+ }
23
+ const line = JSON.stringify({ level, ...log12, ts: Date.now() }) + "\n";
24
+ try {
25
+ console.debug("[graph-core] transmit sending", { endpoint, level, bytes: line.length });
26
+ } catch {
27
+ }
28
+ fetch(endpoint, {
29
+ method: "POST",
30
+ mode: "no-cors",
31
+ credentials: "omit",
32
+ body: line,
33
+ keepalive: true
34
+ }).then(() => {
35
+ try {
36
+ console.debug("[graph-core] transmit fetch ok");
37
+ } catch {
38
+ }
39
+ }).catch((err) => {
40
+ try {
41
+ console.debug("[graph-core] transmit fetch error", err?.message || err);
42
+ } catch {
43
+ }
44
+ });
45
+ } catch (e) {
46
+ try {
47
+ console.debug("[graph-core] transmit error", e?.message || e);
48
+ } catch {
49
+ }
50
+ }
51
+ }
52
+ };
53
+ var base = pino({
54
+ level: resolvedLevel,
55
+ browser: browserOpts
56
+ });
57
+ function logger(module) {
58
+ const child = base.child({ module });
59
+ return {
60
+ error: (msg, ...args) => child.error({ args }, msg),
61
+ warn: (msg, ...args) => child.warn({ args }, msg),
62
+ info: (msg, ...args) => child.info({ args }, msg),
63
+ debug: (msg, ...args) => child.debug({ args }, msg)
64
+ };
65
+ }
66
+ var log = logger("core");
67
+
68
+ // src/graph/node.ts
69
+ var log2 = logger("node");
6
70
  var defNodeData = {
7
71
  id: "",
8
72
  data: void 0,
@@ -11,7 +75,7 @@ var defNodeData = {
11
75
  text: void 0,
12
76
  type: void 0,
13
77
  render: void 0,
14
- ports: { in: null, out: null },
78
+ ports: {},
15
79
  aligned: {},
16
80
  edges: { in: ISet(), out: ISet() },
17
81
  segs: { in: ISet(), out: ISet() },
@@ -45,10 +109,7 @@ var Node = class _Node extends Record(defNodeData) {
45
109
  return this.isDummy ? this.id : _Node.key(this);
46
110
  }
47
111
  static key(node) {
48
- return `${node.id}:${node.version}`;
49
- }
50
- static isDummyId(nodeId) {
51
- return nodeId.startsWith(_Node.dummyPrefix);
112
+ return `k:${node.id}:${node.version}`;
52
113
  }
53
114
  static addNormal(g, data) {
54
115
  const layer = g.layerAt(0);
@@ -58,7 +119,9 @@ var Node = class _Node extends Record(defNodeData) {
58
119
  segs: { in: ISet(), out: ISet() },
59
120
  aligned: {},
60
121
  edgeIds: [],
61
- layerId: layer.id
122
+ layerId: layer.id,
123
+ lpos: void 0,
124
+ pos: void 0
62
125
  });
63
126
  layer.addNode(g, node.id);
64
127
  g.nodes.set(node.id, node);
@@ -121,6 +184,18 @@ var Node = class _Node extends Record(defNodeData) {
121
184
  getLayer(g) {
122
185
  return g.getLayer(this.layerId);
123
186
  }
187
+ margin(g) {
188
+ return this.isDummy ? g.options.edgeSpacing - g.options.dummyNodeSize : g.options.nodeMargin;
189
+ }
190
+ marginWith(g, other) {
191
+ return Math.max(this.margin(g), other.margin(g));
192
+ }
193
+ width(g) {
194
+ return this.dims?.[g.w] ?? 0;
195
+ }
196
+ right(g) {
197
+ return this.lpos + this.width(g);
198
+ }
124
199
  setIndex(g, index) {
125
200
  if (this.index == index) return this;
126
201
  return this.mut(g).set("index", index);
@@ -167,9 +242,22 @@ var Node = class _Node extends Record(defNodeData) {
167
242
  return node;
168
243
  }
169
244
  delSelf(g) {
245
+ if (this.aligned.in)
246
+ g.getNode(this.aligned.in).setAligned(g, "out", void 0);
247
+ if (this.aligned.out)
248
+ g.getNode(this.aligned.out).setAligned(g, "in", void 0);
170
249
  this.getLayer(g).delNode(g, this.id);
171
- for (const rel of this.rels(g))
172
- rel.delSelf(g);
250
+ for (const edge of this.rels(g, "edges", "both"))
251
+ edge.delSelf(g);
252
+ const remainingSegIds = [...this.segs.in, ...this.segs.out];
253
+ if (remainingSegIds.length > 0) {
254
+ for (const segId of remainingSegIds) {
255
+ if (g.segs?.has?.(segId)) {
256
+ g.getSeg(segId).delSelf(g);
257
+ } else {
258
+ }
259
+ }
260
+ }
173
261
  g.nodes.delete(this.id);
174
262
  g.dirtyNodes.delete(this.id);
175
263
  g.delNodes.add(this.id);
@@ -262,30 +350,7 @@ var Node = class _Node extends Record(defNodeData) {
262
350
 
263
351
  // src/graph/edge.ts
264
352
  import { Record as Record2 } from "immutable";
265
-
266
- // src/log.ts
267
- var levels = {
268
- error: 0,
269
- warn: 1,
270
- info: 2,
271
- debug: 3
272
- };
273
- var currentLevel = "debug";
274
- function shouldLog(level) {
275
- return levels[level] <= levels[currentLevel];
276
- }
277
- function logger(module) {
278
- return {
279
- error: (msg, ...args) => shouldLog("error") && console.error(`[${module}] ${msg}`, ...args),
280
- warn: (msg, ...args) => shouldLog("warn") && console.warn(`[${module}] ${msg}`, ...args),
281
- info: (msg, ...args) => shouldLog("info") && console.info(`[${module}] ${msg}`, ...args),
282
- debug: (msg, ...args) => shouldLog("debug") && console.debug(`[${module}] ${msg}`, ...args)
283
- };
284
- }
285
- var log = logger("core");
286
-
287
- // src/graph/edge.ts
288
- var log2 = logger("edge");
353
+ var log3 = logger("edge");
289
354
  var defEdgeData = {
290
355
  id: "",
291
356
  data: null,
@@ -293,7 +358,6 @@ var defEdgeData = {
293
358
  source: { id: "" },
294
359
  target: { id: "" },
295
360
  type: void 0,
296
- style: void 0,
297
361
  mutable: false,
298
362
  segIds: []
299
363
  };
@@ -374,7 +438,7 @@ var Edge = class _Edge extends Record2(defEdgeData) {
374
438
  source = edge.source.id;
375
439
  if (edge.source?.port)
376
440
  source = `${source}.${edge.source.port}`;
377
- const marker = edge.source?.marker ?? edge.style?.marker?.source;
441
+ const marker = edge.source?.marker;
378
442
  if (marker && marker != "none") source += `[${marker}]`;
379
443
  source += "-";
380
444
  }
@@ -384,7 +448,7 @@ var Edge = class _Edge extends Record2(defEdgeData) {
384
448
  if (edge.target.port)
385
449
  target = `${target}.${edge.target.port}`;
386
450
  target = "-" + target;
387
- const marker = edge.target?.marker ?? edge.style?.marker?.target ?? "arrow";
451
+ const marker = edge.target?.marker ?? "arrow";
388
452
  if (marker && marker != "none") target += `[${marker}]`;
389
453
  }
390
454
  const type = edge.type || "";
@@ -426,7 +490,6 @@ var defSegData = {
426
490
  source: { id: "" },
427
491
  target: { id: "" },
428
492
  type: void 0,
429
- style: void 0,
430
493
  edgeIds: ISet2(),
431
494
  trackPos: void 0,
432
495
  svg: void 0,
@@ -457,7 +520,7 @@ var Seg = class _Seg extends Record3(defSegData) {
457
520
  sameEnd(other, side) {
458
521
  const mine = this[side];
459
522
  const yours = other[side];
460
- return mine.id === yours.id && mine.port === yours.port && mine.marker === yours.marker;
523
+ return mine.id === yours.id && mine.port === yours.port && mine.marker === yours.marker && this.type === other.type;
461
524
  }
462
525
  setPos(g, source, target) {
463
526
  return this.mut(g).merge({
@@ -529,7 +592,7 @@ var Seg = class _Seg extends Record3(defSegData) {
529
592
 
530
593
  // src/graph/layer.ts
531
594
  import { Record as Record4, Set as ISet3 } from "immutable";
532
- var log3 = logger("layer");
595
+ var log4 = logger("layer");
533
596
  var defLayerData = {
534
597
  id: "",
535
598
  index: 0,
@@ -554,7 +617,7 @@ var Layer = class extends Record4(defLayerData) {
554
617
  mutable: false
555
618
  }).asImmutable();
556
619
  }
557
- get size() {
620
+ get nodeCount() {
558
621
  return this.nodeIds.size;
559
622
  }
560
623
  *nodes(g) {
@@ -609,9 +672,8 @@ var Layer = class extends Record4(defLayerData) {
609
672
  reindex(g, nodeId) {
610
673
  if (!this.isSorted) return void 0;
611
674
  const sorted = this.sorted.filter((id) => id != nodeId);
612
- const idx = this.sorted.findIndex((id) => id == nodeId);
613
- for (let i = idx; i < sorted.length; i++)
614
- g.getNode(sorted[i]).setIndex(g, i);
675
+ for (const [i, id] of this.sorted.entries())
676
+ g.getNode(id).setIndex(g, i);
615
677
  return sorted;
616
678
  }
617
679
  delNode(g, nodeId) {
@@ -783,12 +845,12 @@ var Cycles = class _Cycles {
783
845
 
784
846
  // src/graph/services/dummy.ts
785
847
  import { Set as ISet4 } from "immutable";
786
- var log4 = logger("dummy");
848
+ var log5 = logger("dummy");
787
849
  var Dummy = class _Dummy {
788
850
  static updateDummies(g) {
789
851
  for (const edgeId of g.dirtyEdges) {
790
852
  const edge = g.getEdge(edgeId);
791
- const { type, style } = edge;
853
+ const { type } = edge;
792
854
  const sourceLayer = edge.sourceNode(g).layerIndex(g);
793
855
  const targetLayer = edge.targetNode(g).layerIndex(g);
794
856
  let segIndex = 0;
@@ -812,7 +874,7 @@ var Dummy = class _Dummy {
812
874
  });
813
875
  target = { id: dummy.id };
814
876
  }
815
- seg = Seg.add(g, { source, target, type, style, edgeIds: ISet4([edgeId]) });
877
+ seg = Seg.add(g, { source, target, type, edgeIds: ISet4([edgeId]) });
816
878
  segs.splice(segIndex, 0, seg.id);
817
879
  changed = true;
818
880
  } 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)) {
@@ -851,9 +913,8 @@ var Dummy = class _Dummy {
851
913
  let layer = g.getLayer(layerId);
852
914
  const groups = /* @__PURE__ */ new Map();
853
915
  for (const nodeId of layer.nodeIds) {
854
- if (!Node.isDummyId(nodeId)) continue;
855
916
  const node = g.getNode(nodeId);
856
- if (node.isMerged) continue;
917
+ if (!node.isDummy || node.isMerged) continue;
857
918
  const edge = g.getEdge(node.edgeIds[0]);
858
919
  const key = Edge.key(edge, "k:", side);
859
920
  if (!groups.has(key)) groups.set(key, /* @__PURE__ */ new Set());
@@ -861,7 +922,7 @@ var Dummy = class _Dummy {
861
922
  }
862
923
  for (const [key, group] of groups) {
863
924
  if (group.size == 1) continue;
864
- const edgeIds = [...group].map((node) => node.edgeId);
925
+ const edgeIds = [...group].map((node) => node.edgeIds[0]);
865
926
  const dummy = Node.addDummy(g, { edgeIds, layerId, isMerged: true });
866
927
  let seg;
867
928
  for (const old of group) {
@@ -871,8 +932,9 @@ var Dummy = class _Dummy {
871
932
  const example = g.getSeg(segId);
872
933
  seg = Seg.add(g, {
873
934
  ...example,
874
- edgeIds: ISet4([old.edgeId]),
875
- [altSide]: { ...example[altSide], id: dummy.id }
935
+ edgeIds: ISet4(edgeIds),
936
+ [side]: { ...example[side] },
937
+ [altSide]: { ...example[altSide], id: dummy.id, port: void 0 }
876
938
  });
877
939
  }
878
940
  edge = edge.replaceSegId(g, segId, seg.id);
@@ -884,12 +946,15 @@ var Dummy = class _Dummy {
884
946
  const example = g.getSeg(segId);
885
947
  const seg2 = Seg.add(g, {
886
948
  ...example,
887
- edgeIds: ISet4([old.edgeId]),
888
- [side]: { ...example[side], id: dummy.id }
949
+ edgeIds: ISet4([old.edgeIds[0]]),
950
+ [altSide]: { ...example[altSide] },
951
+ [side]: { ...example[side], id: dummy.id, port: void 0 }
889
952
  });
890
953
  edge = edge.replaceSegId(g, segId, seg2.id);
891
954
  }
892
955
  }
956
+ for (const old of group)
957
+ old.delSelf(g);
893
958
  }
894
959
  }
895
960
  }
@@ -897,7 +962,7 @@ var Dummy = class _Dummy {
897
962
 
898
963
  // src/graph/services/layers.ts
899
964
  import { Seq } from "immutable";
900
- var log5 = logger("layers");
965
+ var log6 = logger("layers");
901
966
  var Layers = class {
902
967
  static updateLayers(g) {
903
968
  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);
@@ -960,7 +1025,7 @@ var Layers = class {
960
1025
 
961
1026
  // src/graph/services/layout.ts
962
1027
  import { Seq as Seq2 } from "immutable";
963
- var log6 = logger("layout");
1028
+ var log7 = logger("layout");
964
1029
  var Layout = class _Layout {
965
1030
  static parentIndex(g, node) {
966
1031
  const parents = Seq2([...node.adjs(g, "segs", "in")]);
@@ -977,8 +1042,8 @@ var Layout = class _Layout {
977
1042
  if (a.isDummy && !b.isDummy) return -1;
978
1043
  if (!a.isDummy && b.isDummy) return 1;
979
1044
  if (!a.isDummy) return a.id.localeCompare(b.id);
980
- const minA = a.edgeId ?? Seq2(a.edgeIds).min();
981
- const minB = b.edgeId ?? Seq2(b.edgeIds).min();
1045
+ const minA = Seq2(a.edgeIds).min();
1046
+ const minB = Seq2(b.edgeIds).min();
982
1047
  return minA.localeCompare(minB);
983
1048
  }
984
1049
  static positionNodes(g) {
@@ -1004,7 +1069,12 @@ var Layout = class _Layout {
1004
1069
  let node = g.getNode(sorted[i]);
1005
1070
  node = node.setIndex(g, i).setLayerPos(g, lpos);
1006
1071
  const size = node.dims?.[g.w] ?? 0;
1007
- lpos += size + g.options.nodeMargin;
1072
+ let margin = node.margin(g);
1073
+ if (i + 1 < sorted.length) {
1074
+ const next = g.getNode(sorted[i + 1]);
1075
+ margin = node.marginWith(g, next);
1076
+ }
1077
+ lpos += size + margin;
1008
1078
  }
1009
1079
  }
1010
1080
  }
@@ -1036,7 +1106,7 @@ var Layout = class _Layout {
1036
1106
  let iterations = 0;
1037
1107
  while (true) {
1038
1108
  if (++iterations > 10) {
1039
- log6.error(`alignNodes: infinite loop detected in layer ${layerId}`);
1109
+ log7.error(`alignNodes: infinite loop detected in layer ${layerId}`);
1040
1110
  break;
1041
1111
  }
1042
1112
  let changed = false;
@@ -1096,7 +1166,7 @@ var Layout = class _Layout {
1096
1166
  static anchorPos(g, seg, side) {
1097
1167
  const nodeId = seg[side].id;
1098
1168
  const node = g.getNode(nodeId);
1099
- let p = { x: 0, y: 0, [g.x]: node.lpos, ...node.pos || {} };
1169
+ let p = { [g.x]: node.lpos, [g.y]: node.pos?.[g.y] ?? 0 };
1100
1170
  let w = node.dims?.[g.w] ?? 0;
1101
1171
  let h = node.dims?.[g.h] ?? 0;
1102
1172
  if (node.isDummy)
@@ -1142,22 +1212,18 @@ var Layout = class _Layout {
1142
1212
  }
1143
1213
  static shiftNode(g, nodeId, alignId, dir, lpos, reverseMove, conservative) {
1144
1214
  const node = g.getNode(nodeId);
1145
- log6.debug(`shift ${nodeId} (at ${node.lpos}) to ${alignId} (at ${lpos})`);
1146
1215
  if (!conservative)
1147
1216
  _Layout.markAligned(g, nodeId, alignId, dir, lpos);
1148
- const space = g.options.nodeMargin;
1149
- const nodeWidth = node.dims?.[g.w] ?? 0;
1150
- const aMin = lpos - space, aMax = lpos + nodeWidth + space;
1217
+ const nodeRight = lpos + node.width(g);
1151
1218
  repeat:
1152
1219
  for (const otherId of node.getLayer(g).nodeIds) {
1153
1220
  if (otherId == nodeId) continue;
1154
1221
  const other = g.getNode(otherId);
1155
- const opos = other.lpos;
1156
- const otherWidth = other.dims?.[g.w] ?? 0;
1157
- const bMin = opos, bMax = opos + otherWidth;
1158
- if (aMin < bMax && bMin < aMax) {
1222
+ const margin = node.marginWith(g, other);
1223
+ const gap = lpos < other.lpos ? other.lpos - nodeRight : lpos - other.right(g);
1224
+ if (gap < margin) {
1159
1225
  if (conservative) return false;
1160
- const safePos = reverseMove ? aMin - otherWidth : aMax;
1226
+ const safePos = reverseMove ? lpos - other.width(g) - margin : nodeRight + margin;
1161
1227
  _Layout.shiftNode(g, otherId, void 0, dir, safePos, reverseMove, conservative);
1162
1228
  continue repeat;
1163
1229
  }
@@ -1206,11 +1272,12 @@ var Layout = class _Layout {
1206
1272
  for (const layerId of g.layerList) {
1207
1273
  const layer = g.getLayer(layerId);
1208
1274
  if (layer.sorted.length < 2) continue;
1209
- for (const nodeId of layer.sorted) {
1275
+ for (const [i, nodeId] of layer.sorted.entries()) {
1210
1276
  const node = g.getNode(nodeId);
1211
1277
  if (node.index == 0) continue;
1212
1278
  let minGap = Infinity;
1213
1279
  const stack = [];
1280
+ let maxMargin = 0;
1214
1281
  for (const right of _Layout.aligned(g, nodeId, "both")) {
1215
1282
  stack.push(right);
1216
1283
  const leftId = _Layout.leftOf(g, right);
@@ -1219,8 +1286,10 @@ var Layout = class _Layout {
1219
1286
  const leftWidth = left.dims?.[g.w] ?? 0;
1220
1287
  const gap = right.lpos - left.lpos - leftWidth;
1221
1288
  if (gap < minGap) minGap = gap;
1289
+ const margin = right.marginWith(g, left);
1290
+ if (margin > maxMargin) maxMargin = margin;
1222
1291
  }
1223
- const delta = minGap - g.options.nodeMargin;
1292
+ const delta = minGap - maxMargin;
1224
1293
  if (delta <= 0) continue;
1225
1294
  anyChanged = true;
1226
1295
  for (const right of stack)
@@ -1268,9 +1337,8 @@ var Layout = class _Layout {
1268
1337
  };
1269
1338
 
1270
1339
  // src/canvas/marker.tsx
1271
- import { default as default2 } from "./marker.css?raw";
1272
1340
  import { jsx } from "jsx-dom/jsx-runtime";
1273
- function arrow(size, classPrefix, reverse = false) {
1341
+ function arrow(size, reverse = false) {
1274
1342
  const h = size / 1.5;
1275
1343
  const w = size;
1276
1344
  const ry = h / 2;
@@ -1279,7 +1347,7 @@ function arrow(size, classPrefix, reverse = false) {
1279
1347
  "marker",
1280
1348
  {
1281
1349
  id: `g3p-marker-arrow${suffix}`,
1282
- className: `${classPrefix}-marker ${classPrefix}-marker-arrow`,
1350
+ className: "g3p-marker g3p-marker-arrow",
1283
1351
  markerWidth: size,
1284
1352
  markerHeight: size,
1285
1353
  refX: "2",
@@ -1290,7 +1358,7 @@ function arrow(size, classPrefix, reverse = false) {
1290
1358
  }
1291
1359
  );
1292
1360
  }
1293
- function circle(size, classPrefix, reverse = false) {
1361
+ function circle(size, reverse = false) {
1294
1362
  const r = size / 3;
1295
1363
  const cy = size / 2;
1296
1364
  const suffix = reverse ? "-reverse" : "";
@@ -1298,7 +1366,7 @@ function circle(size, classPrefix, reverse = false) {
1298
1366
  "marker",
1299
1367
  {
1300
1368
  id: `g3p-marker-circle${suffix}`,
1301
- className: `${classPrefix}-marker ${classPrefix}-marker-circle`,
1369
+ className: "g3p-marker g3p-marker-circle",
1302
1370
  markerWidth: size,
1303
1371
  markerHeight: size,
1304
1372
  refX: "2",
@@ -1309,7 +1377,7 @@ function circle(size, classPrefix, reverse = false) {
1309
1377
  }
1310
1378
  );
1311
1379
  }
1312
- function diamond(size, classPrefix, reverse = false) {
1380
+ function diamond(size, reverse = false) {
1313
1381
  const w = size * 0.7;
1314
1382
  const h = size / 2;
1315
1383
  const cy = size / 2;
@@ -1318,7 +1386,7 @@ function diamond(size, classPrefix, reverse = false) {
1318
1386
  "marker",
1319
1387
  {
1320
1388
  id: `g3p-marker-diamond${suffix}`,
1321
- className: `${classPrefix}-marker ${classPrefix}-marker-diamond`,
1389
+ className: "g3p-marker g3p-marker-diamond",
1322
1390
  markerWidth: size,
1323
1391
  markerHeight: size,
1324
1392
  refX: "2",
@@ -1329,7 +1397,7 @@ function diamond(size, classPrefix, reverse = false) {
1329
1397
  }
1330
1398
  );
1331
1399
  }
1332
- function bar(size, classPrefix, reverse = false) {
1400
+ function bar(size, reverse = false) {
1333
1401
  const h = size * 0.6;
1334
1402
  const cy = size / 2;
1335
1403
  const suffix = reverse ? "-reverse" : "";
@@ -1337,7 +1405,7 @@ function bar(size, classPrefix, reverse = false) {
1337
1405
  "marker",
1338
1406
  {
1339
1407
  id: `g3p-marker-bar${suffix}`,
1340
- className: `${classPrefix}-marker ${classPrefix}-marker-bar`,
1408
+ className: "g3p-marker g3p-marker-bar",
1341
1409
  markerWidth: size,
1342
1410
  markerHeight: size,
1343
1411
  refX: "2",
@@ -1348,7 +1416,7 @@ function bar(size, classPrefix, reverse = false) {
1348
1416
  }
1349
1417
  );
1350
1418
  }
1351
- function none(size, classPrefix, reverse = false) {
1419
+ function none(size, reverse = false) {
1352
1420
  return void 0;
1353
1421
  }
1354
1422
  function normalize(data) {
@@ -1367,7 +1435,7 @@ var markerDefs = {
1367
1435
  };
1368
1436
 
1369
1437
  // src/graph/services/lines.ts
1370
- var log7 = logger("lines");
1438
+ var log8 = logger("lines");
1371
1439
  var Lines = class _Lines {
1372
1440
  static layoutSeg(g, seg) {
1373
1441
  const sourcePos = Layout.anchorPos(g, seg, "source");
@@ -1413,7 +1481,11 @@ var Lines = class _Lines {
1413
1481
  validTrack = track;
1414
1482
  break;
1415
1483
  }
1416
- if (other.p1 < seg.p2 && seg.p1 < other.p2) {
1484
+ const minA = Math.min(seg.p1, seg.p2);
1485
+ const maxA = Math.max(seg.p1, seg.p2);
1486
+ const minB = Math.min(other.p1, other.p2);
1487
+ const maxB = Math.max(other.p1, other.p2);
1488
+ if (minA < maxB && minB < maxA) {
1417
1489
  overlap = true;
1418
1490
  break;
1419
1491
  }
@@ -1428,6 +1500,21 @@ var Lines = class _Lines {
1428
1500
  else
1429
1501
  trackSet.push([seg]);
1430
1502
  }
1503
+ const midpoint = (s) => (s.p1 + s.p2) / 2;
1504
+ const sortTracks = (tracks2, goingRight) => {
1505
+ tracks2.sort((a, b) => {
1506
+ const midA = Math.max(...a.map(midpoint));
1507
+ const midB = Math.max(...b.map(midpoint));
1508
+ return goingRight ? midB - midA : midA - midB;
1509
+ });
1510
+ };
1511
+ sortTracks(rightTracks, true);
1512
+ sortTracks(leftTracks, false);
1513
+ allTracks.sort((a, b) => {
1514
+ const avgA = a.reduce((sum, s) => sum + midpoint(s), 0) / a.length;
1515
+ const avgB = b.reduce((sum, s) => sum + midpoint(s), 0) / b.length;
1516
+ return avgB - avgA;
1517
+ });
1431
1518
  const tracks = [];
1432
1519
  const all = leftTracks.concat(rightTracks).concat(allTracks);
1433
1520
  for (const track of all)
@@ -1598,6 +1685,7 @@ var Lines = class _Lines {
1598
1685
  };
1599
1686
 
1600
1687
  // src/graph/graph.ts
1688
+ var log9 = logger("graph");
1601
1689
  var emptyChanges = {
1602
1690
  addedNodes: [],
1603
1691
  removedNodes: [],
@@ -1872,29 +1960,8 @@ var screenPos = (x, y) => ({ x, y });
1872
1960
  var canvasPos = (x, y) => ({ x, y });
1873
1961
  var graphPos = (x, y) => ({ x, y });
1874
1962
 
1875
- // src/canvas/node.tsx
1876
- import styles from "./node.css?raw";
1877
-
1878
- // src/canvas/styler.ts
1879
- var injected = {};
1880
- function styler(name, styles4, prefix) {
1881
- if (prefix === "g3p" && !injected[name]) {
1882
- const style = document.createElement("style");
1883
- style.textContent = styles4;
1884
- document.head.appendChild(style);
1885
- injected[name] = true;
1886
- }
1887
- return (str, condition) => {
1888
- if (!(condition ?? true)) return "";
1889
- const parts = str.split(/\s+/);
1890
- const fixed = parts.map((p) => `${prefix}-${name}-${p}`);
1891
- return fixed.join(" ");
1892
- };
1893
- }
1894
-
1895
1963
  // src/canvas/node.tsx
1896
1964
  import { jsx as jsx2, jsxs } from "jsx-dom/jsx-runtime";
1897
- var log8 = logger("canvas");
1898
1965
  var Node2 = class {
1899
1966
  selected;
1900
1967
  hovered;
@@ -1902,7 +1969,6 @@ var Node2 = class {
1902
1969
  content;
1903
1970
  canvas;
1904
1971
  data;
1905
- classPrefix;
1906
1972
  isDummy;
1907
1973
  pos;
1908
1974
  constructor(canvas, data, isDummy = false) {
@@ -1910,20 +1976,18 @@ var Node2 = class {
1910
1976
  this.data = data;
1911
1977
  this.selected = false;
1912
1978
  this.hovered = false;
1913
- this.classPrefix = canvas.classPrefix;
1914
1979
  this.isDummy = isDummy;
1915
1980
  if (this.isDummy) {
1916
1981
  const size = canvas.dummyNodeSize;
1917
1982
  } else {
1918
1983
  const render = data.render ?? canvas.renderNode;
1919
- this.content = this.renderContent(render(data.data));
1984
+ this.content = this.renderContent(render(data.data, data));
1920
1985
  }
1921
1986
  }
1922
1987
  remove() {
1923
1988
  this.container.remove();
1924
1989
  }
1925
1990
  append() {
1926
- console.log("append", this);
1927
1991
  this.canvas.group.appendChild(this.container);
1928
1992
  }
1929
1993
  needsContentSize() {
@@ -1932,19 +1996,6 @@ var Node2 = class {
1932
1996
  needsContainerSize() {
1933
1997
  return !this.isDummy;
1934
1998
  }
1935
- handleClick(e) {
1936
- e.stopPropagation();
1937
- }
1938
- handleMouseEnter(e) {
1939
- }
1940
- handleMouseLeave(e) {
1941
- }
1942
- handleContextMenu(e) {
1943
- }
1944
- handleMouseDown(e) {
1945
- }
1946
- handleMouseUp(e) {
1947
- }
1948
1999
  setPos(pos) {
1949
2000
  this.pos = pos;
1950
2001
  const { x, y } = pos;
@@ -1961,7 +2012,6 @@ var Node2 = class {
1961
2012
  return el;
1962
2013
  }
1963
2014
  renderContainer() {
1964
- const c = styler("node", styles, this.classPrefix);
1965
2015
  const hasPorts = this.hasPorts();
1966
2016
  const inner = this.isDummy ? this.renderDummy() : this.renderForeign();
1967
2017
  const nodeType = this.data?.type;
@@ -1969,14 +2019,8 @@ var Node2 = class {
1969
2019
  this.container = /* @__PURE__ */ jsx2(
1970
2020
  "g",
1971
2021
  {
1972
- className: `${c("container")} ${c("dummy", this.isDummy)} ${typeClass}`.trim(),
1973
- onClick: (e) => this.handleClick(e),
1974
- onMouseEnter: (e) => this.handleMouseEnter(e),
1975
- onMouseLeave: (e) => this.handleMouseLeave(e),
1976
- onContextMenu: (e) => this.handleContextMenu(e),
1977
- onMouseDown: (e) => this.handleMouseDown(e),
1978
- onMouseUp: (e) => this.handleMouseUp(e),
1979
- style: { cursor: "pointer" },
2022
+ className: `g3p-node-container ${this.isDummy ? "g3p-node-dummy" : ""} ${typeClass}`.trim(),
2023
+ "data-node-id": this.data?.id,
1980
2024
  children: inner
1981
2025
  }
1982
2026
  );
@@ -1986,7 +2030,6 @@ var Node2 = class {
1986
2030
  return /* @__PURE__ */ jsx2("foreignObject", { width: w, height: h, children: this.content });
1987
2031
  }
1988
2032
  renderDummy() {
1989
- const c = styler("node", styles, this.classPrefix);
1990
2033
  let w = this.canvas.dummyNodeSize;
1991
2034
  let h = this.canvas.dummyNodeSize;
1992
2035
  w /= 2;
@@ -1999,7 +2042,7 @@ var Node2 = class {
1999
2042
  cy: h,
2000
2043
  rx: w,
2001
2044
  ry: h,
2002
- className: c("background")
2045
+ className: "g3p-node-background"
2003
2046
  }
2004
2047
  ),
2005
2048
  /* @__PURE__ */ jsx2(
@@ -2010,7 +2053,7 @@ var Node2 = class {
2010
2053
  rx: w,
2011
2054
  ry: h,
2012
2055
  fill: "none",
2013
- className: c("border")
2056
+ className: "g3p-node-border"
2014
2057
  }
2015
2058
  )
2016
2059
  ] });
@@ -2023,7 +2066,7 @@ var Node2 = class {
2023
2066
  const ports = data.ports?.[dir];
2024
2067
  if (!ports) continue;
2025
2068
  for (const port of ports) {
2026
- const el = this.content.querySelector(`#g3p-port-${data.id}-${port.id}`);
2069
+ const el = this.content.querySelector(`.g3p-node-port[data-node-id="${data.id}"][data-port-id="${port.id}"]`);
2027
2070
  if (!el) continue;
2028
2071
  const portRect = el.getBoundingClientRect();
2029
2072
  if (isVertical) {
@@ -2061,23 +2104,22 @@ var Node2 = class {
2061
2104
  renderPortRow(dir, inout) {
2062
2105
  const ports = this.data?.ports?.[dir];
2063
2106
  if (!ports?.length) return null;
2064
- const c = styler("node", styles, this.classPrefix);
2065
2107
  const pos = this.getPortPosition(dir);
2066
2108
  const isVertical = this.isVerticalOrientation();
2067
2109
  const layoutClass = isVertical ? "row" : "col";
2068
2110
  const rotateLabels = false;
2069
2111
  const rotateClass = rotateLabels ? `port-rotated-${pos}` : "";
2070
- return /* @__PURE__ */ jsx2("div", { className: `${c("ports")} ${c(`ports-${layoutClass}`)}`, children: ports.map((port) => /* @__PURE__ */ jsx2(
2112
+ return /* @__PURE__ */ jsx2("div", { className: `g3p-node-ports g3p-node-ports-${layoutClass}`, children: ports.map((port) => /* @__PURE__ */ jsx2(
2071
2113
  "div",
2072
2114
  {
2073
- id: `g3p-port-${this.data.id}-${port.id}`,
2074
- className: `${c("port")} ${c(`port-${inout}-${pos}`)} ${c(rotateClass)}`,
2115
+ className: `g3p-node-port g3p-node-port-${inout}-${pos} ${rotateClass}`,
2116
+ "data-node-id": this.data.id,
2117
+ "data-port-id": port.id,
2075
2118
  children: port.label ?? port.id
2076
2119
  }
2077
2120
  )) });
2078
2121
  }
2079
2122
  renderInsidePorts(el) {
2080
- const c = styler("node", styles, this.classPrefix);
2081
2123
  const isVertical = this.isVerticalOrientation();
2082
2124
  const isReversed = this.isReversedOrientation();
2083
2125
  let inPorts = this.renderPortRow("in", "in");
@@ -2085,14 +2127,13 @@ var Node2 = class {
2085
2127
  if (!inPorts && !outPorts) return el;
2086
2128
  if (isReversed) [inPorts, outPorts] = [outPorts, inPorts];
2087
2129
  const wrapperClass = isVertical ? "v" : "h";
2088
- return /* @__PURE__ */ jsxs("div", { className: `${c("with-ports")} ${c(`with-ports-${wrapperClass}`)}`, children: [
2130
+ return /* @__PURE__ */ jsxs("div", { className: `g3p-node-with-ports g3p-node-with-ports-${wrapperClass}`, children: [
2089
2131
  inPorts,
2090
2132
  el,
2091
2133
  outPorts
2092
2134
  ] });
2093
2135
  }
2094
2136
  renderOutsidePorts(el) {
2095
- const c = styler("node", styles, this.classPrefix);
2096
2137
  const isVertical = this.isVerticalOrientation();
2097
2138
  const isReversed = this.isReversedOrientation();
2098
2139
  let inPorts = this.renderPortRow("in", "out");
@@ -2100,71 +2141,59 @@ var Node2 = class {
2100
2141
  if (!inPorts && !outPorts) return el;
2101
2142
  if (isReversed) [inPorts, outPorts] = [outPorts, inPorts];
2102
2143
  const wrapperClass = isVertical ? "v" : "h";
2103
- return /* @__PURE__ */ jsxs("div", { className: `${c("with-ports")} ${c(`with-ports-${wrapperClass}`)}`, children: [
2144
+ return /* @__PURE__ */ jsxs("div", { className: `g3p-node-with-ports g3p-node-with-ports-${wrapperClass}`, children: [
2104
2145
  inPorts,
2105
2146
  el,
2106
2147
  outPorts
2107
2148
  ] });
2108
2149
  }
2109
2150
  renderBorder(el) {
2110
- const c = styler("node", styles, this.classPrefix);
2111
- return /* @__PURE__ */ jsx2("div", { className: c("border"), children: el });
2151
+ return /* @__PURE__ */ jsx2("div", { className: "g3p-node-border", children: el });
2112
2152
  }
2113
2153
  };
2114
2154
 
2115
2155
  // src/canvas/seg.tsx
2116
- import styles2 from "./seg.css?raw";
2117
2156
  import { jsx as jsx3, jsxs as jsxs2 } from "jsx-dom/jsx-runtime";
2118
- var log9 = logger("canvas");
2119
2157
  var Seg2 = class {
2120
2158
  id;
2121
2159
  selected;
2122
2160
  hovered;
2123
2161
  canvas;
2124
- classPrefix;
2125
2162
  type;
2126
2163
  svg;
2127
2164
  el;
2128
2165
  source;
2129
2166
  target;
2167
+ edgeIds;
2130
2168
  constructor(canvas, data, g) {
2131
2169
  this.id = data.id;
2132
2170
  this.canvas = canvas;
2133
2171
  this.selected = false;
2134
2172
  this.hovered = false;
2135
2173
  this.svg = data.svg;
2136
- this.classPrefix = canvas.classPrefix;
2137
2174
  this.source = { ...data.source, isDummy: data.sourceNode(g).isDummy };
2138
2175
  this.target = { ...data.target, isDummy: data.targetNode(g).isDummy };
2139
2176
  this.type = data.type;
2177
+ this.edgeIds = data.edgeIds.toArray();
2140
2178
  this.el = this.render();
2141
2179
  }
2142
- handleClick(e) {
2143
- e.stopPropagation();
2144
- }
2145
- handleMouseEnter(e) {
2146
- }
2147
- handleMouseLeave(e) {
2148
- }
2149
- handleContextMenu(e) {
2150
- }
2151
2180
  append() {
2152
2181
  this.canvas.group.appendChild(this.el);
2153
2182
  }
2154
2183
  remove() {
2155
2184
  this.el.remove();
2156
2185
  }
2157
- update(data) {
2186
+ update(data, g) {
2158
2187
  this.svg = data.svg;
2159
2188
  this.type = data.type;
2160
- this.source = data.source;
2161
- this.target = data.target;
2189
+ this.source = { ...data.source, isDummy: data.sourceNode(g).isDummy };
2190
+ this.target = { ...data.target, isDummy: data.targetNode(g).isDummy };
2191
+ this.edgeIds = data.edgeIds.toArray();
2162
2192
  this.remove();
2163
2193
  this.el = this.render();
2164
2194
  this.append();
2165
2195
  }
2166
2196
  render() {
2167
- const c = styler("seg", styles2, this.classPrefix);
2168
2197
  let { source, target } = normalize(this);
2169
2198
  if (this.source.isDummy) source = void 0;
2170
2199
  if (this.target.isDummy) target = void 0;
@@ -2174,18 +2203,15 @@ var Seg2 = class {
2174
2203
  {
2175
2204
  ref: (el) => this.el = el,
2176
2205
  id: `g3p-seg-${this.id}`,
2177
- className: `${c("container")} ${typeClass}`.trim(),
2178
- onClick: this.handleClick.bind(this),
2179
- onMouseEnter: this.handleMouseEnter.bind(this),
2180
- onMouseLeave: this.handleMouseLeave.bind(this),
2181
- onContextMenu: this.handleContextMenu.bind(this),
2206
+ className: `g3p-seg-container ${typeClass}`.trim(),
2207
+ "data-edge-id": this.id,
2182
2208
  children: [
2183
2209
  /* @__PURE__ */ jsx3(
2184
2210
  "path",
2185
2211
  {
2186
2212
  d: this.svg,
2187
2213
  fill: "none",
2188
- className: c("line"),
2214
+ className: "g3p-seg-line",
2189
2215
  markerStart: source ? `url(#g3p-marker-${source}-reverse)` : void 0,
2190
2216
  markerEnd: target ? `url(#g3p-marker-${target})` : void 0
2191
2217
  }
@@ -2196,7 +2222,7 @@ var Seg2 = class {
2196
2222
  d: this.svg,
2197
2223
  stroke: "transparent",
2198
2224
  fill: "none",
2199
- className: c("hitbox"),
2225
+ className: "g3p-seg-hitbox",
2200
2226
  style: { cursor: "pointer" }
2201
2227
  }
2202
2228
  )
@@ -2206,46 +2232,561 @@ var Seg2 = class {
2206
2232
  }
2207
2233
  };
2208
2234
 
2209
- // src/canvas/canvas.tsx
2210
- import styles3 from "./canvas.css?raw";
2211
- import zoomStyles from "./zoom.css?raw";
2235
+ // src/canvas/editMode.ts
2236
+ var EditMode = class {
2237
+ _state = { type: "idle" };
2238
+ _editable = false;
2239
+ get state() {
2240
+ return this._state;
2241
+ }
2242
+ get editable() {
2243
+ return this._editable;
2244
+ }
2245
+ set editable(value) {
2246
+ this._editable = value;
2247
+ if (!value) {
2248
+ this.reset();
2249
+ }
2250
+ }
2251
+ get isIdle() {
2252
+ return this._state.type === "idle";
2253
+ }
2254
+ get isPanning() {
2255
+ return this._state.type === "panning";
2256
+ }
2257
+ get isCreatingEdge() {
2258
+ return this._state.type === "new-edge";
2259
+ }
2260
+ /** Start panning the canvas */
2261
+ startPan(startCanvas, startTransform) {
2262
+ this._state = { type: "panning", startCanvas, startTransform };
2263
+ }
2264
+ /** Start creating a new edge from a node or port */
2265
+ startNewEdge(id, startGraph, port) {
2266
+ if (!this._editable) return;
2267
+ this._state = {
2268
+ type: "new-edge",
2269
+ source: { id, port },
2270
+ startGraph,
2271
+ currentGraph: startGraph,
2272
+ target: null
2273
+ };
2274
+ }
2275
+ /** Update the current position during new-edge mode */
2276
+ updateNewEdgePosition(currentGraph) {
2277
+ if (this._state.type === "new-edge") {
2278
+ this._state = { ...this._state, currentGraph };
2279
+ }
2280
+ }
2281
+ /** Update the hover target during new-edge mode */
2282
+ setHoverTarget(target) {
2283
+ if (this._state.type === "new-edge") {
2284
+ this._state = { ...this._state, target };
2285
+ }
2286
+ }
2287
+ /** Get the new-edge state if active */
2288
+ getNewEdgeState() {
2289
+ if (this._state.type === "new-edge") {
2290
+ return this._state;
2291
+ }
2292
+ return null;
2293
+ }
2294
+ /** Reset to idle state */
2295
+ reset() {
2296
+ this._state = { type: "idle" };
2297
+ }
2298
+ };
2299
+
2300
+ // src/canvas/newEdge.tsx
2212
2301
  import { jsx as jsx4, jsxs as jsxs3 } from "jsx-dom/jsx-runtime";
2213
- var log10 = logger("canvas");
2214
- var themeVarMap = {
2215
- // Canvas
2216
- bg: "--g3p-bg",
2217
- shadow: "--g3p-shadow",
2218
- // Node
2219
- border: "--g3p-border",
2220
- borderHover: "--g3p-border-hover",
2221
- borderSelected: "--g3p-border-selected",
2222
- text: "--g3p-text",
2223
- textMuted: "--g3p-text-muted",
2224
- // Port
2225
- bgHover: "--g3p-port-bg-hover",
2226
- // Edge
2227
- color: "--g3p-edge-color"
2302
+ function renderNewEdge({ start, end }) {
2303
+ return /* @__PURE__ */ jsxs3("g", { className: "g3p-new-edge-container", children: [
2304
+ /* @__PURE__ */ jsx4(
2305
+ "circle",
2306
+ {
2307
+ cx: start.x,
2308
+ cy: start.y,
2309
+ r: 4,
2310
+ className: "g3p-new-edge-origin"
2311
+ }
2312
+ ),
2313
+ /* @__PURE__ */ jsx4(
2314
+ "line",
2315
+ {
2316
+ x1: start.x,
2317
+ y1: start.y,
2318
+ x2: end.x,
2319
+ y2: end.y,
2320
+ className: "g3p-new-edge-line"
2321
+ }
2322
+ ),
2323
+ /* @__PURE__ */ jsx4(
2324
+ "circle",
2325
+ {
2326
+ cx: end.x,
2327
+ cy: end.y,
2328
+ r: 3,
2329
+ className: "g3p-new-edge-end"
2330
+ }
2331
+ )
2332
+ ] });
2333
+ }
2334
+
2335
+ // src/canvas/modal.tsx
2336
+ import { jsx as jsx5, jsxs as jsxs4 } from "jsx-dom/jsx-runtime";
2337
+ var Modal = class {
2338
+ container;
2339
+ overlay;
2340
+ dialog;
2341
+ onClose;
2342
+ mouseDownOnOverlay = false;
2343
+ constructor(options) {
2344
+ this.onClose = options.onClose;
2345
+ this.overlay = /* @__PURE__ */ jsx5(
2346
+ "div",
2347
+ {
2348
+ className: "g3p-modal-overlay",
2349
+ onMouseDown: (e) => {
2350
+ this.mouseDownOnOverlay = e.target === this.overlay;
2351
+ },
2352
+ onMouseUp: (e) => {
2353
+ if (this.mouseDownOnOverlay && e.target === this.overlay) this.close();
2354
+ this.mouseDownOnOverlay = false;
2355
+ }
2356
+ }
2357
+ );
2358
+ this.dialog = /* @__PURE__ */ jsxs4("div", { className: "g3p-modal-dialog", children: [
2359
+ /* @__PURE__ */ jsxs4("div", { className: "g3p-modal-header", children: [
2360
+ /* @__PURE__ */ jsx5("span", { className: "g3p-modal-title", children: options.title }),
2361
+ /* @__PURE__ */ jsx5(
2362
+ "button",
2363
+ {
2364
+ className: "g3p-modal-close",
2365
+ onClick: () => this.close(),
2366
+ children: "\xD7"
2367
+ }
2368
+ )
2369
+ ] }),
2370
+ /* @__PURE__ */ jsx5("div", { className: "g3p-modal-body" }),
2371
+ /* @__PURE__ */ jsx5("div", { className: "g3p-modal-footer" })
2372
+ ] });
2373
+ if (options.position) {
2374
+ this.dialog.style.position = "absolute";
2375
+ this.dialog.style.left = `${options.position.x}px`;
2376
+ this.dialog.style.top = `${options.position.y}px`;
2377
+ this.dialog.style.transform = "translate(-50%, -50%)";
2378
+ }
2379
+ this.overlay.appendChild(this.dialog);
2380
+ this.container = this.overlay;
2381
+ this.handleKeyDown = this.handleKeyDown.bind(this);
2382
+ document.addEventListener("keydown", this.handleKeyDown);
2383
+ }
2384
+ handleKeyDown(e) {
2385
+ if (e.key === "Escape") {
2386
+ this.close();
2387
+ }
2388
+ }
2389
+ get body() {
2390
+ return this.dialog.querySelector(".g3p-modal-body");
2391
+ }
2392
+ get footer() {
2393
+ return this.dialog.querySelector(".g3p-modal-footer");
2394
+ }
2395
+ show(parent) {
2396
+ parent.appendChild(this.container);
2397
+ const firstInput = this.dialog.querySelector("input, select, button");
2398
+ if (firstInput) firstInput.focus();
2399
+ }
2400
+ close() {
2401
+ document.removeEventListener("keydown", this.handleKeyDown);
2402
+ this.container.remove();
2403
+ this.onClose();
2404
+ }
2228
2405
  };
2229
- function themeToCSS(theme, selector, prefix = "") {
2230
- const entries = Object.entries(theme).filter(([_, v]) => v !== void 0);
2231
- if (!entries.length) return "";
2232
- let css = `${selector} {
2233
- `;
2234
- for (const [key, value] of entries) {
2235
- let cssVar = themeVarMap[key];
2236
- if (key === "bg" && prefix === "node") {
2237
- cssVar = "--g3p-bg-node";
2238
- } else if (key === "bg" && prefix === "port") {
2239
- cssVar = "--g3p-port-bg";
2406
+ var NewNodeModal = class extends Modal {
2407
+ fieldInputs = /* @__PURE__ */ new Map();
2408
+ typeSelect;
2409
+ submitCallback;
2410
+ fields;
2411
+ constructor(options) {
2412
+ super({
2413
+ title: "New Node",
2414
+ onClose: () => options.onCancel?.()
2415
+ });
2416
+ this.submitCallback = options.onSubmit;
2417
+ this.fields = options.fields ?? /* @__PURE__ */ new Map([["title", "string"]]);
2418
+ this.renderBody(options.nodeTypes);
2419
+ this.renderFooter();
2420
+ }
2421
+ renderBody(nodeTypes) {
2422
+ this.body.innerHTML = "";
2423
+ for (const [name, type] of this.fields) {
2424
+ const label = name.charAt(0).toUpperCase() + name.slice(1);
2425
+ const fieldGroup = this.renderField(name, label, type);
2426
+ this.body.appendChild(fieldGroup);
2240
2427
  }
2241
- if (cssVar) {
2242
- css += ` ${cssVar}: ${value};
2243
- `;
2428
+ if (nodeTypes && nodeTypes.length > 0) {
2429
+ const typeGroup = /* @__PURE__ */ jsxs4("div", { className: "g3p-modal-field", children: [
2430
+ /* @__PURE__ */ jsx5("label", { className: "g3p-modal-label", children: "Type" }),
2431
+ /* @__PURE__ */ jsxs4(
2432
+ "select",
2433
+ {
2434
+ className: "g3p-modal-select",
2435
+ ref: (el) => this.typeSelect = el,
2436
+ children: [
2437
+ /* @__PURE__ */ jsx5("option", { value: "", children: "Default" }),
2438
+ nodeTypes.map((type) => /* @__PURE__ */ jsx5("option", { value: type, children: type }))
2439
+ ]
2440
+ }
2441
+ )
2442
+ ] });
2443
+ this.body.appendChild(typeGroup);
2244
2444
  }
2245
2445
  }
2246
- css += "}\n";
2247
- return css;
2248
- }
2446
+ renderField(name, label, type) {
2447
+ if (type === "boolean") {
2448
+ return /* @__PURE__ */ jsx5("div", { className: "g3p-modal-field g3p-modal-field-checkbox", children: /* @__PURE__ */ jsxs4("label", { className: "g3p-modal-label", children: [
2449
+ /* @__PURE__ */ jsx5(
2450
+ "input",
2451
+ {
2452
+ type: "checkbox",
2453
+ className: "g3p-modal-checkbox",
2454
+ ref: (el) => this.fieldInputs.set(name, el)
2455
+ }
2456
+ ),
2457
+ label
2458
+ ] }) });
2459
+ }
2460
+ return /* @__PURE__ */ jsxs4("div", { className: "g3p-modal-field", children: [
2461
+ /* @__PURE__ */ jsx5("label", { className: "g3p-modal-label", children: label }),
2462
+ /* @__PURE__ */ jsx5(
2463
+ "input",
2464
+ {
2465
+ type: type === "number" ? "number" : "text",
2466
+ className: "g3p-modal-input",
2467
+ placeholder: `Enter ${label.toLowerCase()}`,
2468
+ ref: (el) => this.fieldInputs.set(name, el)
2469
+ }
2470
+ )
2471
+ ] });
2472
+ }
2473
+ renderFooter() {
2474
+ this.footer.innerHTML = "";
2475
+ this.footer.appendChild(
2476
+ /* @__PURE__ */ jsxs4("div", { className: "g3p-modal-buttons", children: [
2477
+ /* @__PURE__ */ jsx5(
2478
+ "button",
2479
+ {
2480
+ className: "g3p-modal-btn g3p-modal-btn-secondary",
2481
+ onClick: () => this.close(),
2482
+ children: "Cancel"
2483
+ }
2484
+ ),
2485
+ /* @__PURE__ */ jsx5(
2486
+ "button",
2487
+ {
2488
+ className: "g3p-modal-btn g3p-modal-btn-primary",
2489
+ onClick: () => this.submit(),
2490
+ children: "Create"
2491
+ }
2492
+ )
2493
+ ] })
2494
+ );
2495
+ }
2496
+ submit() {
2497
+ const data = {};
2498
+ for (const [name, type] of this.fields) {
2499
+ const input = this.fieldInputs.get(name);
2500
+ if (!input) continue;
2501
+ if (type === "boolean") {
2502
+ data[name] = input.checked;
2503
+ } else if (type === "number") {
2504
+ const val = input.value;
2505
+ if (val) data[name] = Number(val);
2506
+ } else {
2507
+ const val = input.value.trim();
2508
+ if (val) data[name] = val;
2509
+ }
2510
+ }
2511
+ if (Object.keys(data).length === 0) {
2512
+ const firstInput = this.fieldInputs.values().next().value;
2513
+ if (firstInput) firstInput.focus();
2514
+ return;
2515
+ }
2516
+ if (this.typeSelect?.value) {
2517
+ data.type = this.typeSelect.value;
2518
+ }
2519
+ document.removeEventListener("keydown", this.handleKeyDown);
2520
+ this.container.remove();
2521
+ this.submitCallback(data);
2522
+ }
2523
+ };
2524
+ var EditNodeModal = class extends Modal {
2525
+ fieldInputs = /* @__PURE__ */ new Map();
2526
+ typeSelect;
2527
+ node;
2528
+ fields;
2529
+ submitCallback;
2530
+ deleteCallback;
2531
+ constructor(options) {
2532
+ super({
2533
+ title: "Edit Node",
2534
+ position: options.position,
2535
+ onClose: () => options.onCancel?.()
2536
+ });
2537
+ this.node = options.node;
2538
+ this.submitCallback = options.onSubmit;
2539
+ this.deleteCallback = options.onDelete;
2540
+ this.fields = options.fields ?? /* @__PURE__ */ new Map([["title", "string"]]);
2541
+ if (!options.fields && !this.node.title)
2542
+ this.node = { ...this.node, title: this.node.id };
2543
+ this.renderBody(options.nodeTypes);
2544
+ this.renderFooter();
2545
+ }
2546
+ renderBody(nodeTypes) {
2547
+ console.log(`renderBody`, this.node);
2548
+ this.body.innerHTML = "";
2549
+ for (const [name, type] of this.fields) {
2550
+ const label = name.charAt(0).toUpperCase() + name.slice(1);
2551
+ const currentValue = this.node[name];
2552
+ const fieldGroup = this.renderField(name, label, type, currentValue);
2553
+ this.body.appendChild(fieldGroup);
2554
+ }
2555
+ if (nodeTypes && nodeTypes.length > 0) {
2556
+ const currentType = this.node.type ?? "";
2557
+ const typeGroup = /* @__PURE__ */ jsxs4("div", { className: "g3p-modal-field", children: [
2558
+ /* @__PURE__ */ jsx5("label", { className: "g3p-modal-label", children: "Type" }),
2559
+ /* @__PURE__ */ jsxs4(
2560
+ "select",
2561
+ {
2562
+ className: "g3p-modal-select",
2563
+ ref: (el) => this.typeSelect = el,
2564
+ children: [
2565
+ /* @__PURE__ */ jsx5("option", { value: "", selected: !currentType, children: "Default" }),
2566
+ nodeTypes.map((type) => /* @__PURE__ */ jsx5("option", { value: type, selected: type === currentType, children: type }))
2567
+ ]
2568
+ }
2569
+ )
2570
+ ] });
2571
+ this.body.appendChild(typeGroup);
2572
+ }
2573
+ }
2574
+ renderField(name, label, type, currentValue) {
2575
+ if (type === "boolean") {
2576
+ return /* @__PURE__ */ jsx5("div", { className: "g3p-modal-field g3p-modal-field-checkbox", children: /* @__PURE__ */ jsxs4("label", { className: "g3p-modal-label", children: [
2577
+ /* @__PURE__ */ jsx5(
2578
+ "input",
2579
+ {
2580
+ type: "checkbox",
2581
+ className: "g3p-modal-checkbox",
2582
+ checked: !!currentValue,
2583
+ ref: (el) => this.fieldInputs.set(name, el)
2584
+ }
2585
+ ),
2586
+ label
2587
+ ] }) });
2588
+ }
2589
+ return /* @__PURE__ */ jsxs4("div", { className: "g3p-modal-field", children: [
2590
+ /* @__PURE__ */ jsx5("label", { className: "g3p-modal-label", children: label }),
2591
+ /* @__PURE__ */ jsx5(
2592
+ "input",
2593
+ {
2594
+ type: type === "number" ? "number" : "text",
2595
+ className: "g3p-modal-input",
2596
+ value: currentValue ?? "",
2597
+ ref: (el) => this.fieldInputs.set(name, el)
2598
+ }
2599
+ )
2600
+ ] });
2601
+ }
2602
+ renderFooter() {
2603
+ this.footer.innerHTML = "";
2604
+ this.footer.appendChild(
2605
+ /* @__PURE__ */ jsxs4("div", { className: "g3p-modal-buttons", children: [
2606
+ this.deleteCallback && /* @__PURE__ */ jsx5(
2607
+ "button",
2608
+ {
2609
+ className: "g3p-modal-btn g3p-modal-btn-danger",
2610
+ onClick: () => this.delete(),
2611
+ children: "Delete"
2612
+ }
2613
+ ),
2614
+ /* @__PURE__ */ jsx5("div", { className: "g3p-modal-spacer" }),
2615
+ /* @__PURE__ */ jsx5(
2616
+ "button",
2617
+ {
2618
+ className: "g3p-modal-btn g3p-modal-btn-secondary",
2619
+ onClick: () => this.close(),
2620
+ children: "Cancel"
2621
+ }
2622
+ ),
2623
+ /* @__PURE__ */ jsx5(
2624
+ "button",
2625
+ {
2626
+ className: "g3p-modal-btn g3p-modal-btn-primary",
2627
+ onClick: () => this.submit(),
2628
+ children: "Save"
2629
+ }
2630
+ )
2631
+ ] })
2632
+ );
2633
+ }
2634
+ submit() {
2635
+ const data = { ...this.node };
2636
+ for (const [name, type] of this.fields) {
2637
+ const input = this.fieldInputs.get(name);
2638
+ if (!input) continue;
2639
+ if (type === "boolean") {
2640
+ data[name] = input.checked;
2641
+ } else if (type === "number") {
2642
+ const val = input.value;
2643
+ data[name] = val ? Number(val) : void 0;
2644
+ } else {
2645
+ const val = input.value.trim();
2646
+ data[name] = val || void 0;
2647
+ }
2648
+ }
2649
+ if (this.typeSelect) {
2650
+ data.type = this.typeSelect.value || void 0;
2651
+ }
2652
+ document.removeEventListener("keydown", this.handleKeyDown);
2653
+ this.container.remove();
2654
+ this.submitCallback(data);
2655
+ }
2656
+ delete() {
2657
+ document.removeEventListener("keydown", this.handleKeyDown);
2658
+ this.container.remove();
2659
+ this.deleteCallback?.();
2660
+ }
2661
+ };
2662
+ var EditEdgeModal = class _EditEdgeModal extends Modal {
2663
+ sourceMarkerSelect;
2664
+ targetMarkerSelect;
2665
+ typeSelect;
2666
+ edge;
2667
+ submitCallback;
2668
+ deleteCallback;
2669
+ static markerTypes = ["none", "arrow", "circle", "diamond", "bar"];
2670
+ constructor(options) {
2671
+ super({
2672
+ title: "Edit Edge",
2673
+ onClose: () => options.onCancel?.()
2674
+ });
2675
+ this.edge = options.edge;
2676
+ this.submitCallback = options.onSubmit;
2677
+ this.deleteCallback = options.onDelete;
2678
+ this.renderBody(options.edgeTypes);
2679
+ this.renderFooter();
2680
+ }
2681
+ renderBody(edgeTypes) {
2682
+ this.body.innerHTML = "";
2683
+ const currentSourceMarker = this.edge.source?.marker ?? "none";
2684
+ const currentTargetMarker = this.edge.target?.marker ?? "arrow";
2685
+ const sourceGroup = /* @__PURE__ */ jsxs4("div", { className: "g3p-modal-field", children: [
2686
+ /* @__PURE__ */ jsx5("label", { className: "g3p-modal-label", children: "Source Marker" }),
2687
+ /* @__PURE__ */ jsx5(
2688
+ "select",
2689
+ {
2690
+ className: "g3p-modal-select",
2691
+ ref: (el) => this.sourceMarkerSelect = el,
2692
+ children: _EditEdgeModal.markerTypes.map((type) => /* @__PURE__ */ jsx5("option", { value: type, selected: type === currentSourceMarker, children: type }))
2693
+ }
2694
+ )
2695
+ ] });
2696
+ this.body.appendChild(sourceGroup);
2697
+ const targetGroup = /* @__PURE__ */ jsxs4("div", { className: "g3p-modal-field", children: [
2698
+ /* @__PURE__ */ jsx5("label", { className: "g3p-modal-label", children: "Target Marker" }),
2699
+ /* @__PURE__ */ jsx5(
2700
+ "select",
2701
+ {
2702
+ className: "g3p-modal-select",
2703
+ ref: (el) => this.targetMarkerSelect = el,
2704
+ children: _EditEdgeModal.markerTypes.map((type) => /* @__PURE__ */ jsx5("option", { value: type, selected: type === currentTargetMarker, children: type }))
2705
+ }
2706
+ )
2707
+ ] });
2708
+ this.body.appendChild(targetGroup);
2709
+ if (edgeTypes && edgeTypes.length > 0) {
2710
+ const currentType = this.edge.type ?? "";
2711
+ const typeGroup = /* @__PURE__ */ jsxs4("div", { className: "g3p-modal-field", children: [
2712
+ /* @__PURE__ */ jsx5("label", { className: "g3p-modal-label", children: "Type" }),
2713
+ /* @__PURE__ */ jsxs4(
2714
+ "select",
2715
+ {
2716
+ className: "g3p-modal-select",
2717
+ ref: (el) => this.typeSelect = el,
2718
+ children: [
2719
+ /* @__PURE__ */ jsx5("option", { value: "", selected: !currentType, children: "Default" }),
2720
+ edgeTypes.map((type) => /* @__PURE__ */ jsx5("option", { value: type, selected: type === currentType, children: type }))
2721
+ ]
2722
+ }
2723
+ )
2724
+ ] });
2725
+ this.body.appendChild(typeGroup);
2726
+ }
2727
+ }
2728
+ renderFooter() {
2729
+ this.footer.innerHTML = "";
2730
+ this.footer.appendChild(
2731
+ /* @__PURE__ */ jsxs4("div", { className: "g3p-modal-buttons", children: [
2732
+ this.deleteCallback && /* @__PURE__ */ jsx5(
2733
+ "button",
2734
+ {
2735
+ className: "g3p-modal-btn g3p-modal-btn-danger",
2736
+ onClick: () => this.delete(),
2737
+ children: "Delete"
2738
+ }
2739
+ ),
2740
+ /* @__PURE__ */ jsx5("div", { className: "g3p-modal-spacer" }),
2741
+ /* @__PURE__ */ jsx5(
2742
+ "button",
2743
+ {
2744
+ className: "g3p-modal-btn g3p-modal-btn-secondary",
2745
+ onClick: () => this.close(),
2746
+ children: "Cancel"
2747
+ }
2748
+ ),
2749
+ /* @__PURE__ */ jsx5(
2750
+ "button",
2751
+ {
2752
+ className: "g3p-modal-btn g3p-modal-btn-primary",
2753
+ onClick: () => this.submit(),
2754
+ children: "Save"
2755
+ }
2756
+ )
2757
+ ] })
2758
+ );
2759
+ }
2760
+ submit() {
2761
+ const data = {
2762
+ ...this.edge,
2763
+ source: {
2764
+ ...this.edge.source,
2765
+ marker: this.sourceMarkerSelect.value === "none" ? void 0 : this.sourceMarkerSelect.value
2766
+ },
2767
+ target: {
2768
+ ...this.edge.target,
2769
+ marker: this.targetMarkerSelect.value === "none" ? void 0 : this.targetMarkerSelect.value
2770
+ }
2771
+ };
2772
+ if (this.typeSelect) {
2773
+ data.type = this.typeSelect.value || void 0;
2774
+ }
2775
+ document.removeEventListener("keydown", this.handleKeyDown);
2776
+ this.container.remove();
2777
+ this.submitCallback(data);
2778
+ }
2779
+ delete() {
2780
+ document.removeEventListener("keydown", this.handleKeyDown);
2781
+ this.container.remove();
2782
+ this.deleteCallback?.();
2783
+ }
2784
+ };
2785
+
2786
+ // src/canvas/canvas.tsx
2787
+ import styles from "./styles.css?raw";
2788
+ import { jsx as jsx6, jsxs as jsxs5 } from "jsx-dom/jsx-runtime";
2789
+ var log10 = logger("canvas");
2249
2790
  var Canvas = class {
2250
2791
  container;
2251
2792
  root;
@@ -2258,19 +2799,26 @@ var Canvas = class {
2258
2799
  curSegs;
2259
2800
  updating;
2260
2801
  // Pan-zoom state
2261
- isPanning = false;
2262
- panStart = null;
2263
- transformStart = null;
2264
2802
  panScale = null;
2265
2803
  zoomControls;
2266
- constructor(options) {
2804
+ // Edit mode state machine
2805
+ editMode;
2806
+ api;
2807
+ // New-edge visual element
2808
+ newEdgeEl;
2809
+ // Pending drag state (for double-click debounce)
2810
+ pendingDrag = null;
2811
+ constructor(api, options) {
2267
2812
  Object.assign(this, options);
2813
+ this.api = api;
2268
2814
  this.allNodes = /* @__PURE__ */ new Map();
2269
2815
  this.curNodes = /* @__PURE__ */ new Map();
2270
2816
  this.curSegs = /* @__PURE__ */ new Map();
2271
2817
  this.updating = false;
2272
2818
  this.bounds = { min: { x: 0, y: 0 }, max: { x: 0, y: 0 } };
2273
2819
  this.transform = { x: 0, y: 0, scale: 1 };
2820
+ this.editMode = new EditMode();
2821
+ this.editMode.editable = this.editable;
2274
2822
  this.createMeasurementContainer();
2275
2823
  this.createCanvasContainer();
2276
2824
  if (this.panZoom) this.setupPanZoom();
@@ -2315,7 +2863,6 @@ var Canvas = class {
2315
2863
  if (gnode.isDummy) {
2316
2864
  node = new Node2(this, gnode, true);
2317
2865
  node.renderContainer();
2318
- node.setPos(gnode.pos);
2319
2866
  this.allNodes.set(key, node);
2320
2867
  } else {
2321
2868
  if (!this.allNodes.has(key))
@@ -2324,6 +2871,7 @@ var Canvas = class {
2324
2871
  }
2325
2872
  this.curNodes.set(gnode.id, node);
2326
2873
  node.append();
2874
+ node.setPos(gnode.pos);
2327
2875
  }
2328
2876
  updateNode(gnode) {
2329
2877
  if (gnode.isDummy) throw new Error("dummy node cannot be updated");
@@ -2345,10 +2893,10 @@ var Canvas = class {
2345
2893
  this.curSegs.set(gseg.id, seg);
2346
2894
  seg.append();
2347
2895
  }
2348
- updateSeg(gseg) {
2896
+ updateSeg(gseg, g) {
2349
2897
  const seg = this.curSegs.get(gseg.id);
2350
2898
  if (!seg) throw new Error("seg not found");
2351
- seg.update(gseg);
2899
+ seg.update(gseg, g);
2352
2900
  }
2353
2901
  deleteSeg(gseg) {
2354
2902
  const seg = this.curSegs.get(gseg.id);
@@ -2368,25 +2916,89 @@ var Canvas = class {
2368
2916
  for (const node of newNodes.values()) {
2369
2917
  node.measure(isVertical);
2370
2918
  const { id, version } = node.data;
2371
- const key = `${id}:${version}`;
2919
+ const key = `k:${id}:${version}`;
2372
2920
  this.allNodes.set(key, node);
2373
2921
  node.renderContainer();
2374
2922
  }
2375
2923
  this.measurement.innerHTML = "";
2376
2924
  return newNodes;
2377
2925
  }
2926
+ // ========== Mouse event handlers ==========
2378
2927
  onClick(e) {
2379
- console.log("click", e);
2380
- }
2381
- onContextMenu(e) {
2382
- console.log("context menu", e);
2928
+ const hit = this.hitTest(e.clientX, e.clientY);
2929
+ if (hit.type === "node") {
2930
+ this.api.handleClickNode(hit.node.data.id);
2931
+ } else if (hit.type === "edge") {
2932
+ this.api.handleClickEdge(hit.segId);
2933
+ }
2383
2934
  }
2384
- groupTransform() {
2385
- return `translate(${this.transform.x}, ${this.transform.y}) scale(${this.transform.scale})`;
2935
+ onDoubleClick(e) {
2936
+ if (this.pendingDrag) {
2937
+ window.clearTimeout(this.pendingDrag.timeout);
2938
+ this.pendingDrag = null;
2939
+ }
2940
+ if (!this.editMode.editable) return;
2941
+ const hit = this.hitTest(e.clientX, e.clientY);
2942
+ if (hit.type === "node") {
2943
+ if (hit.node.isDummy) return;
2944
+ this.api.handleEditNode(hit.node.data.id);
2945
+ } else if (hit.type === "edge") {
2946
+ this.api.handleEditEdge(hit.segId);
2947
+ } else {
2948
+ this.api.handleNewNode();
2949
+ }
2386
2950
  }
2387
- viewBox() {
2388
- const p = this.padding;
2389
- const x = this.bounds.min.x - p;
2951
+ // ========== Built-in Modals ==========
2952
+ /** Show the new node modal */
2953
+ showNewNodeModal(callback) {
2954
+ const nodeTypes = Object.keys(this.nodeTypes);
2955
+ const fields = this.api.getNodeFields();
2956
+ const modal = new NewNodeModal({
2957
+ nodeTypes: nodeTypes.length > 0 ? nodeTypes : void 0,
2958
+ fields: fields.size > 0 ? fields : void 0,
2959
+ onSubmit: (data) => {
2960
+ callback(data);
2961
+ }
2962
+ });
2963
+ modal.show(document.body);
2964
+ }
2965
+ /** Show the edit node modal */
2966
+ showEditNodeModal(node, callback) {
2967
+ const nodeTypes = Object.keys(this.nodeTypes);
2968
+ const fields = this.api.getNodeFields();
2969
+ const modal = new EditNodeModal({
2970
+ node,
2971
+ nodeTypes: nodeTypes.length > 0 ? nodeTypes : void 0,
2972
+ fields: fields.size > 0 ? fields : void 0,
2973
+ onSubmit: (data) => {
2974
+ callback(data);
2975
+ },
2976
+ onDelete: () => {
2977
+ this.api.handleDeleteNode(node.id);
2978
+ }
2979
+ });
2980
+ modal.show(document.body);
2981
+ }
2982
+ /** Show the edit edge modal */
2983
+ showEditEdgeModal(edge, callback) {
2984
+ const modal = new EditEdgeModal({
2985
+ edge,
2986
+ edgeTypes: Object.keys(this.edgeTypes),
2987
+ onSubmit: callback,
2988
+ onDelete: () => {
2989
+ this.api.handleDeleteEdge(edge.id);
2990
+ }
2991
+ });
2992
+ modal.show(document.body);
2993
+ }
2994
+ onContextMenu(e) {
2995
+ }
2996
+ groupTransform() {
2997
+ return `translate(${this.transform.x}, ${this.transform.y}) scale(${this.transform.scale})`;
2998
+ }
2999
+ viewBox() {
3000
+ const p = this.padding;
3001
+ const x = this.bounds.min.x - p;
2390
3002
  const y = this.bounds.min.y - p;
2391
3003
  const w = this.bounds.max.x - this.bounds.min.x + p * 2;
2392
3004
  const h = this.bounds.max.y - this.bounds.min.y + p * 2;
@@ -2394,53 +3006,52 @@ var Canvas = class {
2394
3006
  }
2395
3007
  generateDynamicStyles() {
2396
3008
  let css = "";
2397
- const prefix = this.classPrefix;
2398
- css += themeToCSS(this.theme, `.${prefix}-canvas-container`);
3009
+ css += themeToCSS(this.theme, `.g3p-canvas-container`);
2399
3010
  for (const [type, vars] of Object.entries(this.nodeTypes)) {
2400
- css += themeToCSS(vars, `.${prefix}-node-type-${type}`, "node");
3011
+ css += themeToCSS(vars, `.g3p-node-type-${type}`, "node");
2401
3012
  }
2402
3013
  for (const [type, vars] of Object.entries(this.edgeTypes)) {
2403
- css += themeToCSS(vars, `.${prefix}-edge-type-${type}`);
3014
+ css += themeToCSS(vars, `.g3p-edge-type-${type}`);
2404
3015
  }
2405
3016
  return css;
2406
3017
  }
2407
3018
  createCanvasContainer() {
2408
- const markerStyleEl = document.createElement("style");
2409
- markerStyleEl.textContent = default2;
2410
- document.head.appendChild(markerStyleEl);
2411
- const zoomStyleEl = document.createElement("style");
2412
- zoomStyleEl.textContent = zoomStyles;
2413
- document.head.appendChild(zoomStyleEl);
3019
+ if (!document.getElementById("g3p-styles")) {
3020
+ const baseStyleEl = document.createElement("style");
3021
+ baseStyleEl.id = "g3p-styles";
3022
+ baseStyleEl.textContent = styles;
3023
+ document.head.appendChild(baseStyleEl);
3024
+ }
2414
3025
  const dynamicStyles = this.generateDynamicStyles();
2415
3026
  if (dynamicStyles) {
2416
- const themeStyleEl = document.createElement("style");
2417
- themeStyleEl.textContent = dynamicStyles;
2418
- document.head.appendChild(themeStyleEl);
3027
+ const dynamicStyleEl = document.createElement("style");
3028
+ dynamicStyleEl.textContent = dynamicStyles;
3029
+ document.head.appendChild(dynamicStyleEl);
2419
3030
  }
2420
- const c = styler("canvas", styles3, this.classPrefix);
2421
3031
  const colorModeClass = this.colorMode !== "system" ? `g3p-${this.colorMode}` : "";
2422
- this.container = /* @__PURE__ */ jsx4(
3032
+ this.container = /* @__PURE__ */ jsx6(
2423
3033
  "div",
2424
3034
  {
2425
- className: `${c("container")} ${colorModeClass}`.trim(),
3035
+ className: `g3p-canvas-container ${colorModeClass}`.trim(),
2426
3036
  ref: (el) => this.container = el,
2427
3037
  onContextMenu: this.onContextMenu.bind(this),
2428
- children: /* @__PURE__ */ jsxs3(
3038
+ children: /* @__PURE__ */ jsxs5(
2429
3039
  "svg",
2430
3040
  {
2431
3041
  ref: (el) => this.root = el,
2432
- className: c("root"),
3042
+ className: "g3p-canvas-root",
2433
3043
  width: this.width,
2434
3044
  height: this.height,
2435
3045
  viewBox: this.viewBox(),
2436
3046
  preserveAspectRatio: "xMidYMid meet",
2437
3047
  onClick: this.onClick.bind(this),
3048
+ onDblClick: this.onDoubleClick.bind(this),
2438
3049
  children: [
2439
- /* @__PURE__ */ jsxs3("defs", { children: [
2440
- Object.values(markerDefs).map((marker) => marker(this.markerSize, this.classPrefix, false)),
2441
- Object.values(markerDefs).map((marker) => marker(this.markerSize, this.classPrefix, true))
3050
+ /* @__PURE__ */ jsxs5("defs", { children: [
3051
+ Object.values(markerDefs).map((marker) => marker(this.markerSize, false)),
3052
+ Object.values(markerDefs).map((marker) => marker(this.markerSize, true))
2442
3053
  ] }),
2443
- /* @__PURE__ */ jsx4(
3054
+ /* @__PURE__ */ jsx6(
2444
3055
  "g",
2445
3056
  {
2446
3057
  ref: (el) => this.group = el,
@@ -2459,14 +3070,34 @@ var Canvas = class {
2459
3070
  this.container.addEventListener("mousedown", this.onMouseDown.bind(this));
2460
3071
  document.addEventListener("mousemove", this.onMouseMove.bind(this));
2461
3072
  document.addEventListener("mouseup", this.onMouseUp.bind(this));
3073
+ document.addEventListener("keydown", this.onKeyDown.bind(this));
2462
3074
  this.createZoomControls();
2463
3075
  }
3076
+ onKeyDown(e) {
3077
+ if (e.key === "Escape" && this.editMode.isCreatingEdge) {
3078
+ this.endNewEdge(true);
3079
+ }
3080
+ }
2464
3081
  /** Convert screen coordinates to canvas-relative coordinates */
2465
3082
  screenToCanvas(screen) {
2466
3083
  const rect = this.container.getBoundingClientRect();
2467
3084
  return canvasPos(screen.x - rect.left, screen.y - rect.top);
2468
3085
  }
2469
- /**
3086
+ /** Convert canvas coordinates to graph coordinates */
3087
+ canvasToGraph(canvas) {
3088
+ const vb = this.currentViewBox();
3089
+ const { scale, offsetX, offsetY } = this.getEffectiveScale();
3090
+ return graphPos(
3091
+ vb.x - offsetX + canvas.x * scale,
3092
+ vb.y - offsetY + canvas.y * scale
3093
+ );
3094
+ }
3095
+ /** Convert screen coordinates to graph coordinates */
3096
+ screenToGraph(screen) {
3097
+ const canvas = this.screenToCanvas(screen);
3098
+ return this.canvasToGraph(canvas);
3099
+ }
3100
+ /**
2470
3101
  * Get the effective scale from canvas pixels to graph units,
2471
3102
  * accounting for preserveAspectRatio="xMidYMid meet" which uses
2472
3103
  * the smaller scale (to fit) and centers the content.
@@ -2483,15 +3114,6 @@ var Canvas = class {
2483
3114
  const offsetY = (actualH - vb.h) / 2;
2484
3115
  return { scale, offsetX, offsetY };
2485
3116
  }
2486
- /** Convert canvas coordinates to graph coordinates */
2487
- canvasToGraph(canvas) {
2488
- const vb = this.currentViewBox();
2489
- const { scale, offsetX, offsetY } = this.getEffectiveScale();
2490
- return graphPos(
2491
- vb.x - offsetX + canvas.x * scale,
2492
- vb.y - offsetY + canvas.y * scale
2493
- );
2494
- }
2495
3117
  /** Get current viewBox as an object */
2496
3118
  currentViewBox() {
2497
3119
  const p = this.padding;
@@ -2526,28 +3148,67 @@ var Canvas = class {
2526
3148
  onMouseDown(e) {
2527
3149
  if (e.button !== 0) return;
2528
3150
  if (e.target.closest(".g3p-zoom-controls")) return;
2529
- this.isPanning = true;
2530
- this.panStart = this.screenToCanvas(screenPos(e.clientX, e.clientY));
2531
- this.transformStart = { ...this.transform };
2532
- const { scale } = this.getEffectiveScale();
2533
- this.panScale = { x: scale, y: scale };
2534
- this.container.style.cursor = "grabbing";
2535
- e.preventDefault();
3151
+ const hit = this.hitTest(e.clientX, e.clientY);
3152
+ if (this.editMode.editable && (hit.type === "node" || hit.type === "port")) {
3153
+ const node = hit.node;
3154
+ if (node.isDummy) return;
3155
+ e.preventDefault();
3156
+ e.stopPropagation();
3157
+ const startGraph = this.screenToGraph(hit.center);
3158
+ const portId = hit.type === "port" ? hit.port : void 0;
3159
+ this.pendingDrag = {
3160
+ timeout: window.setTimeout(() => {
3161
+ if (this.pendingDrag) {
3162
+ this.startNewEdge(this.pendingDrag.nodeId, this.pendingDrag.startGraph, this.pendingDrag.portId);
3163
+ this.pendingDrag = null;
3164
+ }
3165
+ }, 200),
3166
+ nodeId: node.data.id,
3167
+ startGraph,
3168
+ portId
3169
+ };
3170
+ return;
3171
+ }
3172
+ if (hit.type === "canvas" || hit.type === "edge") {
3173
+ const startCanvas = this.screenToCanvas(screenPos(e.clientX, e.clientY));
3174
+ this.editMode.startPan(startCanvas, { ...this.transform });
3175
+ const { scale } = this.getEffectiveScale();
3176
+ this.panScale = { x: scale, y: scale };
3177
+ this.container.style.cursor = "grabbing";
3178
+ e.preventDefault();
3179
+ }
2536
3180
  }
2537
3181
  onMouseMove(e) {
2538
- if (!this.isPanning || !this.panStart || !this.transformStart || !this.panScale) return;
3182
+ if (this.editMode.isCreatingEdge) {
3183
+ const screenCursor = screenPos(e.clientX, e.clientY);
3184
+ const canvasCursor = this.screenToCanvas(screenCursor);
3185
+ const graphCursor = this.canvasToGraph(canvasCursor);
3186
+ this.editMode.updateNewEdgePosition(graphCursor);
3187
+ this.updateNewEdgeVisual();
3188
+ this.detectHoverTarget(e.clientX, e.clientY);
3189
+ return;
3190
+ }
3191
+ if (!this.editMode.isPanning || !this.panScale) return;
3192
+ const panState = this.editMode.state;
3193
+ if (panState.type !== "panning") return;
2539
3194
  const current = this.screenToCanvas(screenPos(e.clientX, e.clientY));
2540
- const dx = current.x - this.panStart.x;
2541
- const dy = current.y - this.panStart.y;
2542
- this.transform.x = this.transformStart.x + dx * this.panScale.x;
2543
- this.transform.y = this.transformStart.y + dy * this.panScale.y;
3195
+ const dx = current.x - panState.startCanvas.x;
3196
+ const dy = current.y - panState.startCanvas.y;
3197
+ this.transform.x = panState.startTransform.x + dx * this.panScale.x;
3198
+ this.transform.y = panState.startTransform.y + dy * this.panScale.y;
2544
3199
  this.applyTransform();
2545
3200
  }
2546
3201
  onMouseUp(e) {
2547
- if (!this.isPanning) return;
2548
- this.isPanning = false;
2549
- this.panStart = null;
2550
- this.transformStart = null;
3202
+ if (this.pendingDrag) {
3203
+ window.clearTimeout(this.pendingDrag.timeout);
3204
+ this.pendingDrag = null;
3205
+ }
3206
+ if (this.editMode.isCreatingEdge) {
3207
+ this.endNewEdge(false);
3208
+ return;
3209
+ }
3210
+ if (!this.editMode.isPanning) return;
3211
+ this.editMode.reset();
2551
3212
  this.panScale = null;
2552
3213
  this.container.style.cursor = "";
2553
3214
  }
@@ -2557,12 +3218,11 @@ var Canvas = class {
2557
3218
  this.updateZoomLevel();
2558
3219
  }
2559
3220
  createZoomControls() {
2560
- const c = styler("zoom", zoomStyles, this.classPrefix);
2561
- this.zoomControls = /* @__PURE__ */ jsxs3("div", { className: c("controls"), children: [
2562
- /* @__PURE__ */ jsx4("button", { className: c("btn"), onClick: () => this.zoomIn(), children: "+" }),
2563
- /* @__PURE__ */ jsx4("div", { className: c("level"), id: "g3p-zoom-level", children: "100%" }),
2564
- /* @__PURE__ */ jsx4("button", { className: c("btn"), onClick: () => this.zoomOut(), children: "\u2212" }),
2565
- /* @__PURE__ */ jsx4("button", { className: `${c("btn")} ${c("reset")}`, onClick: () => this.zoomReset(), children: "\u27F2" })
3221
+ this.zoomControls = /* @__PURE__ */ jsxs5("div", { className: "g3p-zoom-controls", children: [
3222
+ /* @__PURE__ */ jsx6("button", { className: "g3p-zoom-btn", onClick: () => this.zoomIn(), children: "+" }),
3223
+ /* @__PURE__ */ jsx6("div", { className: "g3p-zoom-level", id: "g3p-zoom-level", children: "100%" }),
3224
+ /* @__PURE__ */ jsx6("button", { className: "g3p-zoom-btn", onClick: () => this.zoomOut(), children: "\u2212" }),
3225
+ /* @__PURE__ */ jsx6("button", { className: "g3p-zoom-btn g3p-zoom-reset", onClick: () => this.zoomReset(), children: "\u27F2" })
2566
3226
  ] });
2567
3227
  this.container.appendChild(this.zoomControls);
2568
3228
  }
@@ -2584,17 +3244,215 @@ var Canvas = class {
2584
3244
  this.transform = { x: 0, y: 0, scale: 1 };
2585
3245
  this.applyTransform();
2586
3246
  }
3247
+ // ==================== New-Edge Mode ====================
3248
+ /** Start creating a new edge from a node */
3249
+ startNewEdge(sourceNodeId, startGraph, sourcePortId) {
3250
+ this.editMode.startNewEdge(sourceNodeId, startGraph, sourcePortId);
3251
+ this.updateNewEdgeVisual();
3252
+ this.container.style.cursor = "crosshair";
3253
+ }
3254
+ /** Update the new-edge visual during drag */
3255
+ updateNewEdgeVisual() {
3256
+ const state = this.editMode.getNewEdgeState();
3257
+ if (!state) {
3258
+ this.removeNewEdgeVisual();
3259
+ return;
3260
+ }
3261
+ if (this.newEdgeEl) {
3262
+ this.newEdgeEl.remove();
3263
+ }
3264
+ this.newEdgeEl = renderNewEdge({
3265
+ start: state.startGraph,
3266
+ end: state.currentGraph
3267
+ });
3268
+ this.group.appendChild(this.newEdgeEl);
3269
+ }
3270
+ /** Remove the new-edge visual */
3271
+ removeNewEdgeVisual() {
3272
+ if (this.newEdgeEl) {
3273
+ this.newEdgeEl.remove();
3274
+ this.newEdgeEl = void 0;
3275
+ }
3276
+ }
3277
+ /** Complete or cancel the new-edge creation */
3278
+ endNewEdge(cancelled = false) {
3279
+ const state = this.editMode.getNewEdgeState();
3280
+ if (!state) return;
3281
+ if (!cancelled) {
3282
+ const { target, source } = state;
3283
+ if (target?.type == "node") {
3284
+ this.api.handleAddEdge({ id: "", source, target });
3285
+ } else {
3286
+ this.api.handleNewNodeFrom(source);
3287
+ }
3288
+ }
3289
+ this.removeNewEdgeVisual();
3290
+ this.clearDropTargetHighlight();
3291
+ this.editMode.reset();
3292
+ this.container.style.cursor = "";
3293
+ }
3294
+ /** Find node data by internal ID */
3295
+ findNodeDataById(nodeId) {
3296
+ for (const node of this.curNodes.values()) {
3297
+ if (node.data?.id === nodeId) {
3298
+ return node.data.data;
3299
+ }
3300
+ }
3301
+ return null;
3302
+ }
3303
+ /** Set hover target for new-edge mode */
3304
+ setNewEdgeHoverTarget(id, port) {
3305
+ this.clearDropTargetHighlight();
3306
+ this.editMode.setHoverTarget({ type: "node", id, port });
3307
+ if (port) {
3308
+ const portEl = this.container?.querySelector(`.g3p-node-port[data-node-id="${id}"][data-port-id="${port}"]`);
3309
+ if (portEl) portEl.classList.add("g3p-drop-target");
3310
+ } else {
3311
+ const node = this.curNodes.get(id);
3312
+ if (node?.container) node.container.classList.add("g3p-drop-target");
3313
+ }
3314
+ }
3315
+ /** Clear hover target for new-edge mode */
3316
+ clearNewEdgeHoverTarget() {
3317
+ this.clearDropTargetHighlight();
3318
+ this.editMode.setHoverTarget(null);
3319
+ }
3320
+ /** Remove drop target highlight from all elements */
3321
+ clearDropTargetHighlight() {
3322
+ for (const node of this.curNodes.values()) {
3323
+ node.container?.classList.remove("g3p-drop-target");
3324
+ }
3325
+ this.container?.querySelectorAll(".g3p-drop-target").forEach((el) => {
3326
+ el.classList.remove("g3p-drop-target");
3327
+ });
3328
+ }
3329
+ /** Detect hover target during new-edge drag using elementFromPoint */
3330
+ detectHoverTarget(clientX, clientY) {
3331
+ if (this.newEdgeEl) {
3332
+ this.newEdgeEl.style.display = "none";
3333
+ }
3334
+ const el = document.elementFromPoint(clientX, clientY);
3335
+ if (this.newEdgeEl) {
3336
+ this.newEdgeEl.style.display = "";
3337
+ }
3338
+ if (!el) {
3339
+ this.clearNewEdgeHoverTarget();
3340
+ return;
3341
+ }
3342
+ const portEl = el.closest(".g3p-node-port");
3343
+ if (portEl) {
3344
+ const nodeId = portEl.getAttribute("data-node-id");
3345
+ const portId = portEl.getAttribute("data-port-id");
3346
+ if (nodeId && portId) {
3347
+ const node = this.curNodes.get(nodeId);
3348
+ if (node && !node.isDummy) {
3349
+ this.setNewEdgeHoverTarget(nodeId, portId);
3350
+ return;
3351
+ }
3352
+ }
3353
+ }
3354
+ const nodeEl = el.closest(".g3p-node-container");
3355
+ if (nodeEl) {
3356
+ const nodeId = nodeEl.getAttribute("data-node-id");
3357
+ if (nodeId) {
3358
+ const node = this.curNodes.get(nodeId);
3359
+ if (node && !node.isDummy) {
3360
+ this.setNewEdgeHoverTarget(node.data.id);
3361
+ return;
3362
+ }
3363
+ }
3364
+ }
3365
+ this.clearNewEdgeHoverTarget();
3366
+ }
3367
+ // ==================== Hit Testing ====================
3368
+ /** Result of a hit test */
3369
+ hitTest(clientX, clientY) {
3370
+ const el = document.elementFromPoint(clientX, clientY);
3371
+ if (!el) return { type: "canvas" };
3372
+ const getCenter = (el2) => {
3373
+ const rect = el2.getBoundingClientRect();
3374
+ return screenPos(rect.left + rect.width / 2, rect.top + rect.height / 2);
3375
+ };
3376
+ const portEl = el.closest(".g3p-node-port");
3377
+ if (portEl) {
3378
+ const nodeId = portEl.getAttribute("data-node-id");
3379
+ const portId = portEl.getAttribute("data-port-id");
3380
+ if (nodeId && portId) {
3381
+ const center = getCenter(portEl);
3382
+ const node = this.curNodes.get(nodeId);
3383
+ if (node) {
3384
+ return { type: "port", node, port: portId, center };
3385
+ }
3386
+ }
3387
+ }
3388
+ const nodeEl = el.closest(".g3p-node-container");
3389
+ if (nodeEl) {
3390
+ const nodeId = nodeEl.getAttribute("data-node-id");
3391
+ if (nodeId) {
3392
+ const borderEl = el.closest(".g3p-node-border");
3393
+ const center = getCenter(borderEl ?? nodeEl);
3394
+ const node = this.curNodes.get(nodeId);
3395
+ if (node) {
3396
+ return { type: "node", node, center };
3397
+ }
3398
+ }
3399
+ }
3400
+ const edgeEl = el.closest(".g3p-seg-container");
3401
+ if (edgeEl) {
3402
+ const segId = edgeEl.getAttribute("data-edge-id");
3403
+ if (segId) {
3404
+ return { type: "edge", segId };
3405
+ }
3406
+ }
3407
+ return { type: "canvas" };
3408
+ }
3409
+ };
3410
+ var themeVarMap = {
3411
+ // Canvas
3412
+ bg: "--g3p-bg",
3413
+ shadow: "--g3p-shadow",
3414
+ // Node
3415
+ border: "--g3p-border",
3416
+ borderHover: "--g3p-border-hover",
3417
+ borderSelected: "--g3p-border-selected",
3418
+ text: "--g3p-text",
3419
+ textMuted: "--g3p-text-muted",
3420
+ // Port
3421
+ bgHover: "--g3p-port-bg-hover",
3422
+ // Edge
3423
+ color: "--g3p-edge-color"
2587
3424
  };
3425
+ function themeToCSS(theme, selector, prefix = "") {
3426
+ const entries = Object.entries(theme).filter(([_, v]) => v !== void 0);
3427
+ if (!entries.length) return "";
3428
+ let css = `${selector} {
3429
+ `;
3430
+ for (const [key, value] of entries) {
3431
+ let cssVar = themeVarMap[key];
3432
+ if (key === "bg" && prefix === "node") {
3433
+ cssVar = "--g3p-bg-node";
3434
+ } else if (key === "bg" && prefix === "port") {
3435
+ cssVar = "--g3p-port-bg";
3436
+ }
3437
+ if (cssVar) {
3438
+ css += ` ${cssVar}: ${value};
3439
+ `;
3440
+ }
3441
+ }
3442
+ css += "}\n";
3443
+ return css;
3444
+ }
2588
3445
 
2589
3446
  // src/canvas/render-node.tsx
2590
- import { jsx as jsx5, jsxs as jsxs4 } from "jsx-dom/jsx-runtime";
2591
- function renderNode(node) {
3447
+ import { jsx as jsx7, jsxs as jsxs6 } from "jsx-dom/jsx-runtime";
3448
+ function renderNode(node, props) {
2592
3449
  if (typeof node == "string") node = { id: node };
2593
- const title = node?.title ?? node?.label ?? node?.name ?? node?.text ?? node?.id ?? "?";
3450
+ const title = node?.title ?? props?.title ?? node?.label ?? node?.name ?? node?.text ?? props?.text ?? node?.id ?? "?";
2594
3451
  const detail = node?.detail ?? node?.description ?? node?.subtitle;
2595
- return /* @__PURE__ */ jsxs4("div", { className: "g3p-node-default", children: [
2596
- /* @__PURE__ */ jsx5("div", { className: "g3p-node-title", children: title }),
2597
- detail && /* @__PURE__ */ jsx5("div", { className: "g3p-node-detail", children: detail })
3452
+ console.log(`renderNode: ${node.id} ${title} ${detail}`);
3453
+ return /* @__PURE__ */ jsxs6("div", { className: "g3p-node-default", children: [
3454
+ /* @__PURE__ */ jsx7("div", { className: "g3p-node-title", children: title }),
3455
+ detail && /* @__PURE__ */ jsx7("div", { className: "g3p-node-detail", children: detail })
2598
3456
  ] });
2599
3457
  }
2600
3458
 
@@ -2626,7 +3484,6 @@ function defaults() {
2626
3484
  },
2627
3485
  canvas: {
2628
3486
  renderNode,
2629
- classPrefix: "g3p",
2630
3487
  width: "100%",
2631
3488
  height: "100%",
2632
3489
  padding: 20,
@@ -2663,26 +3520,50 @@ var Updater = class _Updater {
2663
3520
  this.update.addNodes.push(node);
2664
3521
  return this;
2665
3522
  }
3523
+ addNodes(...nodes) {
3524
+ this.update.addNodes.push(...nodes);
3525
+ return this;
3526
+ }
2666
3527
  deleteNode(node) {
2667
3528
  this.update.removeNodes.push(node);
2668
3529
  return this;
2669
3530
  }
3531
+ deleteNodes(...nodes) {
3532
+ this.update.removeNodes.push(...nodes);
3533
+ return this;
3534
+ }
2670
3535
  updateNode(node) {
2671
3536
  this.update.updateNodes.push(node);
2672
3537
  return this;
2673
3538
  }
3539
+ updateNodes(...nodes) {
3540
+ this.update.updateNodes.push(...nodes);
3541
+ return this;
3542
+ }
2674
3543
  addEdge(edge) {
2675
3544
  this.update.addEdges.push(edge);
2676
3545
  return this;
2677
3546
  }
3547
+ addEdges(...edges) {
3548
+ this.update.addEdges.push(...edges);
3549
+ return this;
3550
+ }
2678
3551
  deleteEdge(edge) {
2679
3552
  this.update.removeEdges.push(edge);
2680
3553
  return this;
2681
3554
  }
3555
+ deleteEdges(...edges) {
3556
+ this.update.removeEdges.push(...edges);
3557
+ return this;
3558
+ }
2682
3559
  updateEdge(edge) {
2683
3560
  this.update.updateEdges.push(edge);
2684
3561
  return this;
2685
3562
  }
3563
+ updateEdges(...edges) {
3564
+ this.update.updateEdges.push(...edges);
3565
+ return this;
3566
+ }
2686
3567
  static add(nodes, edges) {
2687
3568
  const updater = new _Updater();
2688
3569
  updater.update.addNodes = nodes;
@@ -2703,22 +3584,19 @@ var API = class {
2703
3584
  nodeIds;
2704
3585
  edgeIds;
2705
3586
  nodeVersions;
3587
+ nodeOverrides;
3588
+ edgeOverrides;
3589
+ nodeFields;
2706
3590
  nextNodeId;
2707
3591
  nextEdgeId;
3592
+ events;
2708
3593
  root;
2709
3594
  constructor(args) {
2710
3595
  this.root = args.root;
2711
3596
  this.options = applyDefaults(args.options);
2712
- let graph2 = new Graph({ options: this.options.graph });
2713
- this.state = { graph: graph2, update: null };
2714
- this.seq = [this.state];
2715
- this.index = 0;
2716
- this.nodeIds = /* @__PURE__ */ new Map();
2717
- this.edgeIds = /* @__PURE__ */ new Map();
2718
- this.nodeVersions = /* @__PURE__ */ new Map();
2719
- this.nextNodeId = 1;
2720
- this.nextEdgeId = 1;
2721
- this.canvas = new Canvas({
3597
+ this.events = args.events || {};
3598
+ this.reset();
3599
+ this.canvas = new Canvas(this, {
2722
3600
  ...this.options.canvas,
2723
3601
  dummyNodeSize: this.options.graph.dummyNodeSize,
2724
3602
  orientation: this.options.graph.orientation
@@ -2731,13 +3609,63 @@ var API = class {
2731
3609
  this.history = [];
2732
3610
  }
2733
3611
  }
3612
+ reset() {
3613
+ let graph2 = new Graph({ options: this.options.graph });
3614
+ this.state = { graph: graph2, update: null };
3615
+ this.seq = [this.state];
3616
+ this.index = 0;
3617
+ this.nodeIds = /* @__PURE__ */ new Map();
3618
+ this.edgeIds = /* @__PURE__ */ new Map();
3619
+ this.nodeVersions = /* @__PURE__ */ new Map();
3620
+ this.nodeOverrides = /* @__PURE__ */ new Map();
3621
+ this.edgeOverrides = /* @__PURE__ */ new Map();
3622
+ this.nodeFields = /* @__PURE__ */ new Map();
3623
+ this.nextNodeId = 1;
3624
+ this.nextEdgeId = 1;
3625
+ }
3626
+ /** Initialize the API */
2734
3627
  async init() {
2735
3628
  const root = document.getElementById(this.root);
2736
3629
  if (!root) throw new Error("root element not found");
2737
3630
  root.appendChild(this.canvas.container);
3631
+ await this.applyHistory();
3632
+ }
3633
+ async applyHistory() {
2738
3634
  for (const update of this.history)
2739
3635
  await this.applyUpdate(update);
2740
3636
  }
3637
+ /** Current history index (0-based) */
3638
+ getHistoryIndex() {
3639
+ return this.index;
3640
+ }
3641
+ /** Current history length */
3642
+ getHistoryLength() {
3643
+ return this.seq.length;
3644
+ }
3645
+ /** Toggle canvas editable mode without re-creating the graph */
3646
+ setEditable(editable) {
3647
+ this.canvas.editMode.editable = editable;
3648
+ }
3649
+ /** Replace entire history (clears prior) */
3650
+ async replaceHistory(frames) {
3651
+ this.reset();
3652
+ this.history = frames;
3653
+ await this.applyHistory();
3654
+ }
3655
+ /** Rebuild from snapshot (nodes/edges) */
3656
+ async replaceSnapshot(nodes, edges, description) {
3657
+ this.reset();
3658
+ this.history = [{
3659
+ addNodes: nodes,
3660
+ addEdges: edges,
3661
+ description
3662
+ }];
3663
+ await this.applyHistory();
3664
+ }
3665
+ get graph() {
3666
+ return this.state.graph;
3667
+ }
3668
+ /** Navigate to a different state */
2741
3669
  nav(nav) {
2742
3670
  let newIndex;
2743
3671
  switch (nav) {
@@ -2759,6 +3687,8 @@ var API = class {
2759
3687
  this.applyDiff(this.index, newIndex);
2760
3688
  this.index = newIndex;
2761
3689
  this.state = this.seq[this.index];
3690
+ if (this.events.historyChange)
3691
+ this.events.historyChange(this.index, this.seq.length);
2762
3692
  }
2763
3693
  applyDiff(oldIndex, newIndex) {
2764
3694
  const oldGraph = this.seq[oldIndex].graph;
@@ -2768,36 +3698,72 @@ var API = class {
2768
3698
  if (!newNode) this.canvas.deleteNode(oldNode);
2769
3699
  }
2770
3700
  for (const newNode of newGraph.nodes.values()) {
2771
- if (!oldGraph.nodes.has(newNode.id)) this.canvas.addNode(newNode);
3701
+ const oldNode = oldGraph.nodes.get(newNode.id);
3702
+ if (!oldNode) {
3703
+ this.canvas.addNode(newNode);
3704
+ } else if (oldNode.key !== newNode.key) {
3705
+ this.canvas.deleteNode(oldNode);
3706
+ this.canvas.addNode(newNode);
3707
+ } else if (oldNode.pos !== newNode.pos) {
3708
+ this.canvas.getNode(newNode.key).setPos(newNode.pos);
3709
+ }
2772
3710
  }
2773
3711
  for (const oldSeg of oldGraph.segs.values()) {
2774
3712
  const newSeg = newGraph.segs.get(oldSeg.id);
2775
- if (!newSeg)
3713
+ if (!newSeg) {
2776
3714
  this.canvas.deleteSeg(oldSeg);
2777
- else if (oldSeg.svg != newSeg.svg)
2778
- this.canvas.updateSeg(newSeg);
3715
+ } else if (oldSeg !== newSeg) {
3716
+ this.canvas.updateSeg(newSeg, newGraph);
3717
+ }
2779
3718
  }
2780
3719
  for (const newSeg of newGraph.segs.values()) {
2781
- if (!oldGraph.segs.has(newSeg.id))
3720
+ if (!oldGraph.segs.has(newSeg.id)) {
2782
3721
  this.canvas.addSeg(newSeg, newGraph);
3722
+ }
2783
3723
  }
2784
3724
  this.canvas.update();
2785
3725
  }
3726
+ /** Add a node */
2786
3727
  async addNode(node) {
2787
3728
  await this.update((update) => update.addNode(node));
2788
3729
  }
3730
+ /** Delete a node */
2789
3731
  async deleteNode(node) {
2790
3732
  await this.update((update) => update.deleteNode(node));
2791
3733
  }
3734
+ /** Update a node */
2792
3735
  async updateNode(node) {
2793
3736
  await this.update((update) => update.updateNode(node));
2794
3737
  }
3738
+ /** Add an edge */
2795
3739
  async addEdge(edge) {
2796
3740
  await this.update((update) => update.addEdge(edge));
2797
3741
  }
3742
+ /** Delete an edge */
2798
3743
  async deleteEdge(edge) {
2799
3744
  await this.update((update) => update.deleteEdge(edge));
2800
3745
  }
3746
+ /** Update an edge */
3747
+ async updateEdge(edge) {
3748
+ await this.update((update) => update.updateEdge(edge));
3749
+ }
3750
+ /** Perform a batch of updates */
3751
+ async update(callback) {
3752
+ const updater = new Updater();
3753
+ callback(updater);
3754
+ await this.applyUpdate(updater.update);
3755
+ }
3756
+ /** Rebuild the graph from scratch (removes all then re-adds all nodes/edges) */
3757
+ async rebuild() {
3758
+ const nodes = [...this.nodeIds.keys()];
3759
+ const edges = [...this.edgeIds.keys()];
3760
+ await this.update((updater) => {
3761
+ for (const edge of edges) updater.deleteEdge(edge);
3762
+ for (const node of nodes) updater.deleteNode(node);
3763
+ for (const node of nodes) updater.addNode(node);
3764
+ for (const edge of edges) updater.addEdge(edge);
3765
+ });
3766
+ }
2801
3767
  async applyUpdate(update) {
2802
3768
  log11.info("applyUpdate", update);
2803
3769
  const nodes = await this.measureNodes(update);
@@ -2814,12 +3780,16 @@ var API = class {
2814
3780
  this._addEdge(edge, mut);
2815
3781
  for (const edge of update.updateEdges ?? [])
2816
3782
  this._updateEdge(edge, mut);
3783
+ this.nodeOverrides.clear();
3784
+ this.edgeOverrides.clear();
2817
3785
  });
2818
3786
  this.state = { graph: graph2, update };
2819
3787
  this.setNodePositions();
2820
3788
  this.seq.splice(this.index + 1);
2821
3789
  this.seq.push(this.state);
2822
3790
  this.nav("last");
3791
+ if (this.events.historyChange)
3792
+ this.events.historyChange(this.index, this.seq.length);
2823
3793
  }
2824
3794
  setNodePositions() {
2825
3795
  const { graph: graph2 } = this.state;
@@ -2829,11 +3799,6 @@ var API = class {
2829
3799
  this.canvas.getNode(node.key).setPos(node.pos);
2830
3800
  }
2831
3801
  }
2832
- async update(callback) {
2833
- const updater = new Updater();
2834
- callback(updater);
2835
- await this.applyUpdate(updater.update);
2836
- }
2837
3802
  async measureNodes(update) {
2838
3803
  const data = [];
2839
3804
  for (const set of [update.updateNodes, update.addNodes])
@@ -2848,7 +3813,10 @@ var API = class {
2848
3813
  else if (!data) throw new Error(`invalid node ${data}`);
2849
3814
  else if (typeof data == "string") props = { id: data };
2850
3815
  else if (typeof data == "object") props = data;
2851
- else throw new Error(`invalid node ${data}`);
3816
+ else throw new Error(`invalid node ${JSON.stringify(data)}`);
3817
+ this.detectNodeFields(data);
3818
+ const overrides = this.nodeOverrides.get(data);
3819
+ if (overrides) props = { ...props, ...overrides };
2852
3820
  let { id, title, text, type, render } = props;
2853
3821
  id ??= this.getNodeId(data);
2854
3822
  const ports = this.parsePorts(props.ports);
@@ -2858,6 +3826,21 @@ var API = class {
2858
3826
  this.nodeVersions.set(data, version);
2859
3827
  return { id, data, ports, title, text, type, render, version };
2860
3828
  }
3829
+ detectNodeFields(data) {
3830
+ if (typeof data != "object" || !data) return;
3831
+ const skip = /* @__PURE__ */ new Set(["id", "ports", "render", "version"]);
3832
+ for (const [key, value] of Object.entries(data)) {
3833
+ if (skip.has(key)) continue;
3834
+ if (value === null || value === void 0) continue;
3835
+ const type = typeof value;
3836
+ if (type === "string" || type === "number" || type === "boolean") {
3837
+ this.nodeFields.set(key, type);
3838
+ }
3839
+ }
3840
+ }
3841
+ getNodeFields() {
3842
+ return this.nodeFields;
3843
+ }
2861
3844
  parseEdge(data) {
2862
3845
  const get = this.options.props.edge;
2863
3846
  let props;
@@ -2866,8 +3849,10 @@ var API = class {
2866
3849
  else if (typeof data == "string") props = this.parseStringEdge(data);
2867
3850
  else if (typeof data == "object") props = data;
2868
3851
  else throw new Error(`invalid edge ${data}`);
3852
+ const overrides = this.edgeOverrides.get(data);
3853
+ if (overrides) props = { ...props, ...overrides };
2869
3854
  let { id, source, target, type } = props;
2870
- id ??= this.getEdgeId(data);
3855
+ if (!id) id = this.getEdgeId(data);
2871
3856
  source = this.parseEdgeEnd(source);
2872
3857
  target = this.parseEdgeEnd(target);
2873
3858
  const edge = { id, source, target, type, data };
@@ -2880,14 +3865,14 @@ var API = class {
2880
3865
  const keys = Object.keys(end);
2881
3866
  const pidx = keys.indexOf("port");
2882
3867
  if (pidx != -1) {
2883
- if (typeof end.port != "string") return end;
3868
+ if (end.port !== void 0 && typeof end.port != "string") return end;
2884
3869
  keys.splice(pidx, 1);
2885
3870
  }
2886
3871
  if (keys.length != 1) return end;
2887
3872
  if (keys[0] == "id") return end;
2888
3873
  if (keys[0] != "node") return end;
2889
3874
  const id = this.nodeIds.get(end.node);
2890
- if (!id) throw new Error(`edge end ${end} references unknown node ${end.node}`);
3875
+ if (!id) throw new Error(`edge end references unknown node ${end.node}`);
2891
3876
  return { id, port: end.port };
2892
3877
  }
2893
3878
  throw new Error(`invalid edge end ${end}`);
@@ -2897,13 +3882,19 @@ var API = class {
2897
3882
  return { source, target };
2898
3883
  }
2899
3884
  parsePorts(ports) {
2900
- const fixed = { in: null, out: null };
3885
+ const fixed = {};
2901
3886
  for (const key of ["in", "out"]) {
2902
3887
  if (ports?.[key] && ports[key].length > 0)
2903
3888
  fixed[key] = ports[key].map((port) => typeof port == "string" ? { id: port } : port);
2904
3889
  }
2905
3890
  return fixed;
2906
3891
  }
3892
+ getNode(id) {
3893
+ return this.graph.getNode(id);
3894
+ }
3895
+ getEdge(id) {
3896
+ return this.graph.getEdge(id);
3897
+ }
2907
3898
  getNodeId(node) {
2908
3899
  let id = this.nodeIds.get(node);
2909
3900
  if (!id) {
@@ -2923,7 +3914,6 @@ var API = class {
2923
3914
  _addNode(node, mut) {
2924
3915
  const { data, id: newId } = node.data;
2925
3916
  const oldId = this.nodeIds.get(data);
2926
- console.log("addNode", node, oldId, newId);
2927
3917
  if (oldId && oldId != newId)
2928
3918
  throw new Error(`node id of ${data} changed from ${oldId} to ${newId}`);
2929
3919
  this.nodeIds.set(data, newId);
@@ -2931,36 +3921,1152 @@ var API = class {
2931
3921
  }
2932
3922
  _removeNode(node, mut) {
2933
3923
  const id = this.nodeIds.get(node);
2934
- if (!id) throw new Error(`removing node ${node} which does not exist`);
3924
+ if (!id) throw new Error(`removing node ${JSON.stringify(node)} which does not exist`);
2935
3925
  mut.removeNode({ id });
2936
3926
  }
2937
3927
  _updateNode(node, mut) {
2938
3928
  const { data, id: newId } = node.data;
2939
3929
  const oldId = this.nodeIds.get(data);
2940
- if (!oldId) throw new Error(`updating unknown node ${node}`);
2941
- if (oldId != newId) throw new Error(`node id changed from ${oldId} to ${newId}`);
3930
+ if (!oldId) throw new Error(`updating unknown node ${JSON.stringify(node)} `);
3931
+ if (oldId != newId) throw new Error(`node id changed from ${oldId} to ${newId} `);
2942
3932
  mut.updateNode(node.data);
2943
3933
  }
2944
3934
  _addEdge(edge, mut) {
2945
3935
  const data = this.parseEdge(edge);
2946
3936
  const id = this.edgeIds.get(edge);
2947
3937
  if (id && id != data.id)
2948
- throw new Error(`edge id changed from ${id} to ${data.id}`);
3938
+ throw new Error(`edge id changed from ${id} to ${data.id} `);
2949
3939
  this.edgeIds.set(edge, data.id);
2950
3940
  mut.addEdge(data);
2951
3941
  }
2952
3942
  _removeEdge(edge, mut) {
2953
3943
  const id = this.edgeIds.get(edge);
2954
- if (!id) throw new Error(`removing edge ${edge} which does not exist`);
3944
+ if (!id) throw new Error(`removing edge ${JSON.stringify(edge)} which does not exist`);
2955
3945
  mut.removeEdge(this.parseEdge(edge));
2956
3946
  }
2957
3947
  _updateEdge(edge, mut) {
2958
3948
  const id = this.edgeIds.get(edge);
2959
- if (!id) throw new Error(`updating unknown edge ${edge}`);
3949
+ if (!id) throw new Error(`updating unknown edge ${JSON.stringify(edge)} `);
2960
3950
  const data = this.parseEdge(edge);
2961
- if (data.id !== id) throw new Error(`edge id changed from ${id} to ${data.id}`);
3951
+ if (data.id !== id) throw new Error(`edge id changed from ${id} to ${data.id} `);
2962
3952
  mut.updateEdge(data);
2963
3953
  }
3954
+ // Event Handlers
3955
+ handleClickNode(id) {
3956
+ const handler = this.events.nodeClick;
3957
+ const node = this.graph.getNode(id);
3958
+ if (handler) handler(node.data);
3959
+ }
3960
+ handleClickEdge(id) {
3961
+ const handler = this.events.edgeClick;
3962
+ if (!handler) return;
3963
+ const seg = this.graph.getSeg(id);
3964
+ if (seg.edgeIds.size != 1) return;
3965
+ const edge = this.graph.getEdge(seg.edgeIds.values().next().value);
3966
+ handler(edge.data);
3967
+ }
3968
+ async handleNewNode() {
3969
+ const gotNode = async (node) => {
3970
+ await this.addNode(node);
3971
+ };
3972
+ if (this.events.newNode)
3973
+ this.events.newNode(gotNode);
3974
+ else
3975
+ this.canvas.showNewNodeModal(async (data) => {
3976
+ if (this.events.addNode)
3977
+ this.events.addNode(data, gotNode);
3978
+ else
3979
+ await gotNode(data);
3980
+ });
3981
+ }
3982
+ async handleNewNodeFrom(source) {
3983
+ const gotNode = async (node) => {
3984
+ const gotEdge = async (edge) => {
3985
+ await this.update((u) => {
3986
+ u.addNode(node).addEdge(edge);
3987
+ });
3988
+ };
3989
+ const data = this.graph.getNode(source.id).data;
3990
+ const newEdge = {
3991
+ source: { node: data, port: source.port },
3992
+ target: { node }
3993
+ };
3994
+ if (this.events.addEdge)
3995
+ this.events.addEdge(newEdge, gotEdge);
3996
+ else
3997
+ await gotEdge(newEdge);
3998
+ };
3999
+ if (this.events.newNode)
4000
+ this.events.newNode(gotNode);
4001
+ else
4002
+ this.canvas.showNewNodeModal(async (data) => {
4003
+ if (this.events.addNode)
4004
+ this.events.addNode(data, gotNode);
4005
+ else
4006
+ await gotNode(data);
4007
+ });
4008
+ }
4009
+ async handleEditNode(id) {
4010
+ const node = this.graph.getNode(id);
4011
+ const gotNode = async (node2) => {
4012
+ if (node2) await this.updateNode(node2);
4013
+ };
4014
+ if (this.events.editNode)
4015
+ this.events.editNode(node.data, gotNode);
4016
+ else {
4017
+ this.canvas.showEditNodeModal(node, async (data) => {
4018
+ if (this.events.updateNode)
4019
+ this.events.updateNode(node.data, data, gotNode);
4020
+ else {
4021
+ this.nodeOverrides.set(node.data, data);
4022
+ await gotNode(node.data);
4023
+ }
4024
+ });
4025
+ }
4026
+ }
4027
+ async handleEditEdge(id) {
4028
+ const seg = this.graph.getSeg(id);
4029
+ if (seg.edgeIds.size != 1) return;
4030
+ const edge = this.graph.getEdge(seg.edgeIds.values().next().value);
4031
+ const gotEdge = async (edge2) => {
4032
+ if (edge2) await this.updateEdge(edge2);
4033
+ };
4034
+ if (this.events.editEdge)
4035
+ this.events.editEdge(edge.data, gotEdge);
4036
+ else
4037
+ this.canvas.showEditEdgeModal(edge, async (data) => {
4038
+ const sourceNode = edge.sourceNode(this.graph);
4039
+ const targetNode = edge.targetNode(this.graph);
4040
+ const update = {
4041
+ source: { node: sourceNode.data, port: data.source.port, marker: data.source.marker },
4042
+ target: { node: targetNode.data, port: data.target.port, marker: data.target.marker }
4043
+ };
4044
+ if (this.events.updateEdge)
4045
+ this.events.updateEdge(edge.data, update, gotEdge);
4046
+ else {
4047
+ this.edgeOverrides.set(edge.data, {
4048
+ source: { id: sourceNode.id, port: data.source.port, marker: data.source.marker },
4049
+ target: { id: targetNode.id, port: data.target.port, marker: data.target.marker },
4050
+ type: data.type
4051
+ });
4052
+ await gotEdge(edge.data);
4053
+ }
4054
+ });
4055
+ }
4056
+ async handleAddEdge(data) {
4057
+ const gotEdge = async (edge) => {
4058
+ if (edge) await this.addEdge(edge);
4059
+ };
4060
+ const newEdge = {
4061
+ source: { node: this.graph.getNode(data.source.id).data, port: data.source.port, marker: data.source.marker },
4062
+ target: { node: this.graph.getNode(data.target.id).data, port: data.target.port, marker: data.target.marker }
4063
+ };
4064
+ if (this.events.addEdge)
4065
+ this.events.addEdge(newEdge, gotEdge);
4066
+ else
4067
+ await gotEdge(data);
4068
+ }
4069
+ async handleDeleteNode(id) {
4070
+ const node = this.getNode(id);
4071
+ if (this.events.removeNode)
4072
+ this.events.removeNode(node.data, async (remove) => {
4073
+ if (remove) await this.deleteNode(node.data);
4074
+ });
4075
+ else
4076
+ await this.deleteNode(node.data);
4077
+ }
4078
+ async handleDeleteEdge(id) {
4079
+ const edge = this.getEdge(id);
4080
+ if (this.events.removeEdge)
4081
+ this.events.removeEdge(edge.data, async (remove) => {
4082
+ if (remove) await this.deleteEdge(edge.data);
4083
+ });
4084
+ else
4085
+ await this.deleteEdge(edge.data);
4086
+ }
4087
+ };
4088
+
4089
+ // src/api/ingest.ts
4090
+ var Ingest = class {
4091
+ constructor(api) {
4092
+ this.api = api;
4093
+ }
4094
+ /**
4095
+ * Apply an incoming ingest message to the API.
4096
+ * - snapshot: rebuild state from nodes/edges (clears prior history)
4097
+ * - update: apply incremental update
4098
+ * - history: initialize from a set of frames (clears prior history)
4099
+ */
4100
+ async apply(msg) {
4101
+ switch (msg.type) {
4102
+ case "snapshot": {
4103
+ await this.api.replaceSnapshot(msg.nodes, msg.edges, msg.description);
4104
+ break;
4105
+ }
4106
+ case "update": {
4107
+ await this.api.update((u) => {
4108
+ if (msg.addNodes) u.addNodes(...msg.addNodes);
4109
+ if (msg.removeNodes) u.deleteNodes(...msg.removeNodes);
4110
+ if (msg.updateNodes) u.updateNodes(...msg.updateNodes);
4111
+ if (msg.addEdges) u.addEdges(...msg.addEdges);
4112
+ if (msg.removeEdges) u.deleteEdges(...msg.removeEdges);
4113
+ if (msg.updateEdges) u.updateEdges(...msg.updateEdges);
4114
+ if (msg.description) u.describe(msg.description);
4115
+ });
4116
+ break;
4117
+ }
4118
+ case "history": {
4119
+ await this.api.replaceHistory(msg.frames);
4120
+ break;
4121
+ }
4122
+ }
4123
+ }
4124
+ };
4125
+
4126
+ // src/api/sources/WebSocketSource.ts
4127
+ var WebSocketSource = class {
4128
+ url;
4129
+ ws = null;
4130
+ onMessage;
4131
+ onStatus;
4132
+ reconnectMs;
4133
+ closedByUser = false;
4134
+ connectStartTime = null;
4135
+ totalTimeoutMs = 1e4;
4136
+ totalTimeoutTimer = null;
4137
+ constructor(url, onMessage, onStatus, reconnectMs = 1500) {
4138
+ this.url = url;
4139
+ this.onMessage = onMessage;
4140
+ this.onStatus = onStatus;
4141
+ this.reconnectMs = reconnectMs;
4142
+ }
4143
+ connect() {
4144
+ this.closedByUser = false;
4145
+ this.connectStartTime = Date.now();
4146
+ this.startTotalTimeout();
4147
+ this.open();
4148
+ }
4149
+ disconnect() {
4150
+ this.closedByUser = true;
4151
+ this.clearTotalTimeout();
4152
+ if (this.ws) {
4153
+ try {
4154
+ this.ws.close();
4155
+ } catch {
4156
+ }
4157
+ this.ws = null;
4158
+ }
4159
+ this.onStatus?.("closed");
4160
+ }
4161
+ startTotalTimeout() {
4162
+ this.clearTotalTimeout();
4163
+ this.totalTimeoutTimer = window.setTimeout(() => {
4164
+ if (!this.closedByUser && this.ws?.readyState !== WebSocket.OPEN) {
4165
+ this.closedByUser = true;
4166
+ if (this.ws) {
4167
+ try {
4168
+ this.ws.close();
4169
+ } catch {
4170
+ }
4171
+ this.ws = null;
4172
+ }
4173
+ this.clearTotalTimeout();
4174
+ this.onStatus?.("error", new Error("Connection timeout after 10 seconds"));
4175
+ }
4176
+ }, this.totalTimeoutMs);
4177
+ }
4178
+ clearTotalTimeout() {
4179
+ if (this.totalTimeoutTimer !== null) {
4180
+ clearTimeout(this.totalTimeoutTimer);
4181
+ this.totalTimeoutTimer = null;
4182
+ }
4183
+ this.connectStartTime = null;
4184
+ }
4185
+ open() {
4186
+ if (this.connectStartTime && Date.now() - this.connectStartTime >= this.totalTimeoutMs) {
4187
+ if (!this.closedByUser) {
4188
+ this.closedByUser = true;
4189
+ this.clearTotalTimeout();
4190
+ this.onStatus?.("error", new Error("Connection timeout after 10 seconds"));
4191
+ }
4192
+ return;
4193
+ }
4194
+ this.onStatus?.(this.ws ? "reconnecting" : "connecting");
4195
+ const ws = new WebSocket(this.url);
4196
+ this.ws = ws;
4197
+ ws.onopen = () => {
4198
+ this.clearTotalTimeout();
4199
+ this.onStatus?.("connected");
4200
+ };
4201
+ ws.onerror = (e) => {
4202
+ this.onStatus?.("error", e);
4203
+ };
4204
+ ws.onclose = () => {
4205
+ if (this.closedByUser) {
4206
+ this.onStatus?.("closed");
4207
+ return;
4208
+ }
4209
+ if (this.connectStartTime && Date.now() - this.connectStartTime >= this.totalTimeoutMs) {
4210
+ this.closedByUser = true;
4211
+ this.clearTotalTimeout();
4212
+ this.onStatus?.("error", new Error("Connection timeout after 10 seconds"));
4213
+ return;
4214
+ }
4215
+ this.onStatus?.("reconnecting");
4216
+ setTimeout(() => this.open(), this.reconnectMs);
4217
+ };
4218
+ ws.onmessage = (ev) => {
4219
+ const data = typeof ev.data === "string" ? ev.data : "";
4220
+ const lines = data.split("\n").map((l) => l.trim()).filter(Boolean);
4221
+ for (const line of lines) {
4222
+ try {
4223
+ const obj = JSON.parse(line);
4224
+ this.onMessage(obj);
4225
+ } catch {
4226
+ }
4227
+ }
4228
+ };
4229
+ }
4230
+ };
4231
+
4232
+ // src/api/sources/FileSystemSource.ts
4233
+ var FileSystemSource = class {
4234
+ handle = null;
4235
+ onMessage;
4236
+ onStatus;
4237
+ timer = null;
4238
+ lastSize = 0;
4239
+ filename;
4240
+ intervalMs;
4241
+ constructor(onMessage, onStatus, filename = "graph.ndjson", intervalMs = 1e3) {
4242
+ this.onMessage = onMessage;
4243
+ this.onStatus = onStatus;
4244
+ this.filename = filename;
4245
+ this.intervalMs = intervalMs;
4246
+ }
4247
+ async openDirectory() {
4248
+ try {
4249
+ const dir = await window.showDirectoryPicker?.();
4250
+ if (!dir) throw new Error("File System Access not supported or cancelled");
4251
+ const handle = await dir.getFileHandle(this.filename, { create: false });
4252
+ this.handle = handle;
4253
+ this.onStatus?.("opened", { file: this.filename });
4254
+ this.lastSize = 0;
4255
+ this.startPolling();
4256
+ } catch (e) {
4257
+ this.onStatus?.("error", e);
4258
+ }
4259
+ }
4260
+ close() {
4261
+ if (this.timer) {
4262
+ window.clearInterval(this.timer);
4263
+ this.timer = null;
4264
+ }
4265
+ this.handle = null;
4266
+ this.onStatus?.("closed");
4267
+ }
4268
+ startPolling() {
4269
+ if (this.timer) window.clearInterval(this.timer);
4270
+ this.timer = window.setInterval(() => this.readNewLines(), this.intervalMs);
4271
+ }
4272
+ async readNewLines() {
4273
+ try {
4274
+ if (!this.handle) return;
4275
+ this.onStatus?.("reading");
4276
+ const file = await this.handle.getFile();
4277
+ if (file.size === this.lastSize) return;
4278
+ const slice = await file.slice(this.lastSize).text();
4279
+ this.lastSize = file.size;
4280
+ const lines = slice.split("\n").map((l) => l.trim()).filter(Boolean);
4281
+ for (const line of lines) {
4282
+ try {
4283
+ const obj = JSON.parse(line);
4284
+ this.onMessage(obj);
4285
+ } catch {
4286
+ }
4287
+ }
4288
+ } catch (e) {
4289
+ this.onStatus?.("error", e);
4290
+ }
4291
+ }
4292
+ };
4293
+
4294
+ // src/api/sources/FileSource.ts
4295
+ var FileSource = class {
4296
+ url;
4297
+ onMessage;
4298
+ onStatus;
4299
+ timer = null;
4300
+ lastETag = null;
4301
+ lastContent = "";
4302
+ intervalMs = 1e3;
4303
+ closed = false;
4304
+ constructor(url, onMessage, onStatus, intervalMs = 1e3) {
4305
+ this.url = url;
4306
+ this.onMessage = onMessage;
4307
+ this.onStatus = onStatus;
4308
+ this.intervalMs = intervalMs;
4309
+ }
4310
+ async connect() {
4311
+ this.closed = false;
4312
+ this.lastETag = null;
4313
+ this.lastContent = "";
4314
+ this.onStatus?.("opened");
4315
+ this.startPolling();
4316
+ }
4317
+ close() {
4318
+ this.closed = true;
4319
+ if (this.timer) {
4320
+ window.clearInterval(this.timer);
4321
+ this.timer = null;
4322
+ }
4323
+ this.onStatus?.("closed");
4324
+ }
4325
+ startPolling() {
4326
+ if (this.timer) window.clearInterval(this.timer);
4327
+ this.timer = window.setInterval(() => this.poll(), this.intervalMs);
4328
+ this.poll();
4329
+ }
4330
+ async poll() {
4331
+ if (this.closed) return;
4332
+ try {
4333
+ this.onStatus?.("reading");
4334
+ const headers = {};
4335
+ if (this.lastETag) {
4336
+ headers["If-None-Match"] = this.lastETag;
4337
+ }
4338
+ const response = await fetch(this.url, { headers });
4339
+ if (response.status === 304) {
4340
+ return;
4341
+ }
4342
+ if (!response.ok) {
4343
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
4344
+ }
4345
+ const etag = response.headers.get("ETag");
4346
+ if (etag) {
4347
+ this.lastETag = etag;
4348
+ }
4349
+ const content = await response.text();
4350
+ if (content === this.lastContent) {
4351
+ return;
4352
+ }
4353
+ const lines = content.split("\n").map((l) => l.trim()).filter(Boolean);
4354
+ const lastContentLines = this.lastContent.split("\n").map((l) => l.trim()).filter(Boolean);
4355
+ const newLines = lines.slice(lastContentLines.length);
4356
+ for (const line of newLines) {
4357
+ try {
4358
+ const obj = JSON.parse(line);
4359
+ this.onMessage(obj);
4360
+ } catch {
4361
+ }
4362
+ }
4363
+ this.lastContent = content;
4364
+ } catch (e) {
4365
+ this.onStatus?.("error", e);
4366
+ }
4367
+ }
4368
+ };
4369
+
4370
+ // src/playground/playground.ts
4371
+ import styles2 from "./styles.css?raw";
4372
+ var Playground = class {
4373
+ options;
4374
+ rootElement;
4375
+ currentExample;
4376
+ currentGraph = null;
4377
+ ingest = null;
4378
+ isEditable = false;
4379
+ wsSource = null;
4380
+ fsSource = null;
4381
+ fileSource = null;
4382
+ wsStatus = "disconnected";
4383
+ fsStatus = "disconnected";
4384
+ fileStatus = "disconnected";
4385
+ activeSourceType = null;
4386
+ wsUrl = "ws://localhost:8787";
4387
+ sourceModal = null;
4388
+ helpOverlay = null;
4389
+ exampleList;
4390
+ graphContainerId;
4391
+ constructor(options) {
4392
+ this.options = options;
4393
+ this.exampleList = Object.keys(options.examples);
4394
+ this.currentExample = options.defaultExample || this.exampleList[0];
4395
+ this.graphContainerId = `playground-graph-${Math.random().toString(36).substr(2, 9)}`;
4396
+ if (typeof options.root === "string") {
4397
+ const el = document.getElementById(options.root);
4398
+ if (!el) throw new Error(`Element with id "${options.root}" not found`);
4399
+ this.rootElement = el;
4400
+ } else {
4401
+ this.rootElement = options.root;
4402
+ }
4403
+ }
4404
+ async init() {
4405
+ this.injectStyles();
4406
+ this.createDOM();
4407
+ this.setupEventListeners();
4408
+ await this.renderGraph();
4409
+ this.updateSourceIcon();
4410
+ this.connectExampleSource();
4411
+ const rebuildBtn = this.rootElement.querySelector("#rebuild");
4412
+ if (rebuildBtn) rebuildBtn.disabled = !this.isEditable;
4413
+ }
4414
+ injectStyles() {
4415
+ if (!document.getElementById("g3p-playground-styles")) {
4416
+ const styleEl = document.createElement("style");
4417
+ styleEl.id = "g3p-playground-styles";
4418
+ styleEl.textContent = styles2;
4419
+ document.head.appendChild(styleEl);
4420
+ }
4421
+ }
4422
+ createDOM() {
4423
+ const exampleList = this.exampleList.map((key, i) => {
4424
+ const example = this.options.examples[key];
4425
+ const isActive = i === 0 || key === this.currentExample;
4426
+ return `
4427
+ <button class="example-btn ${isActive ? "active" : ""}" data-example="${key}">
4428
+ ${example.name}
4429
+ </button>
4430
+ `;
4431
+ }).join("");
4432
+ this.rootElement.innerHTML = `
4433
+ <main class="playground">
4434
+ <div class="sidebar">
4435
+ <h2>Examples</h2>
4436
+ <div class="example-list">
4437
+ ${exampleList}
4438
+ </div>
4439
+
4440
+ <h2>Options</h2>
4441
+ <div class="options">
4442
+ <div class="option-group">
4443
+ <label>Orientation</label>
4444
+ <select id="orientation">
4445
+ <option value="TB">Top to Bottom</option>
4446
+ <option value="BT">Bottom to Top</option>
4447
+ <option value="LR">Left to Right</option>
4448
+ <option value="RL">Right to Left</option>
4449
+ </select>
4450
+ </div>
4451
+
4452
+ <div class="option-group">
4453
+ <label>Port Style</label>
4454
+ <select id="portStyle">
4455
+ <option value="outside">Outside</option>
4456
+ <option value="inside">Inside</option>
4457
+ </select>
4458
+ </div>
4459
+
4460
+ <div class="option-group">
4461
+ <label>
4462
+ <input type="checkbox" id="portLabelRotate" />
4463
+ Rotate Port Labels
4464
+ </label>
4465
+ </div>
4466
+
4467
+ <div class="option-group">
4468
+ <label>Theme</label>
4469
+ <select id="colorMode">
4470
+ <option value="system">System</option>
4471
+ <option value="light">Light</option>
4472
+ <option value="dark">Dark</option>
4473
+ </select>
4474
+ </div>
4475
+ </div>
4476
+ </div>
4477
+
4478
+ <div class="graph-area">
4479
+ <div class="graph-toolbar">
4480
+ <div class="nav-controls">
4481
+ <button class="nav-btn" id="nav-first" title="First (Home)">\u23EE</button>
4482
+ <button class="nav-btn" id="nav-prev" title="Previous (\u2190)">\u25C0</button>
4483
+ <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>
4484
+ <button class="nav-btn" id="nav-next" title="Next (\u2192)">\u25B6</button>
4485
+ <button class="nav-btn" id="nav-last" title="Last (End)">\u23ED</button>
4486
+ </div>
4487
+ <div class="connect-controls" style="display:flex; gap:.5rem; align-items:center;">
4488
+ <button class="nav-btn source-icon-btn" id="source-icon" title="Data Source Connection">\u{1F4E1}</button>
4489
+ </div>
4490
+ <button class="nav-btn" id="help-btn" title="How to edit">\u2753</button>
4491
+ <button class="nav-btn" id="edit-toggle" title="Toggle edit mode">\u270E Edit</button>
4492
+ <button class="nav-btn" id="rebuild" title="Rebuild graph from scratch">\u{1F504} Rebuild</button>
4493
+ </div>
4494
+ <div class="graph-container" id="${this.graphContainerId}"></div>
4495
+ </div>
4496
+ </main>
4497
+ `;
4498
+ }
4499
+ setupEventListeners() {
4500
+ this.rootElement.querySelectorAll(".example-btn").forEach((btn) => {
4501
+ btn.addEventListener("click", () => {
4502
+ this.rootElement.querySelectorAll(".example-btn").forEach((b) => b.classList.remove("active"));
4503
+ btn.classList.add("active");
4504
+ this.currentExample = btn.getAttribute("data-example") || this.exampleList[0];
4505
+ this.renderGraph();
4506
+ this.connectExampleSource();
4507
+ });
4508
+ });
4509
+ this.rootElement.querySelectorAll(".options select, .options input").forEach((el) => {
4510
+ el.addEventListener("change", () => this.renderGraph());
4511
+ });
4512
+ this.rootElement.querySelector("#nav-first")?.addEventListener("click", () => {
4513
+ this.currentGraph?.nav("first");
4514
+ this.updateHistoryLabel();
4515
+ });
4516
+ this.rootElement.querySelector("#nav-prev")?.addEventListener("click", () => {
4517
+ this.currentGraph?.nav("prev");
4518
+ this.updateHistoryLabel();
4519
+ });
4520
+ this.rootElement.querySelector("#nav-next")?.addEventListener("click", () => {
4521
+ this.currentGraph?.nav("next");
4522
+ this.updateHistoryLabel();
4523
+ });
4524
+ this.rootElement.querySelector("#nav-last")?.addEventListener("click", () => {
4525
+ this.currentGraph?.nav("last");
4526
+ this.updateHistoryLabel();
4527
+ });
4528
+ this.rootElement.querySelector("#rebuild")?.addEventListener("click", () => {
4529
+ this.currentGraph?.rebuild();
4530
+ });
4531
+ this.rootElement.querySelector("#edit-toggle")?.addEventListener("click", () => {
4532
+ this.isEditable = !this.isEditable;
4533
+ const btn = this.rootElement.querySelector("#edit-toggle");
4534
+ if (btn) btn.textContent = this.isEditable ? "\u2713 Done" : "\u270E Edit";
4535
+ const rebuildBtn = this.rootElement.querySelector("#rebuild");
4536
+ if (rebuildBtn) rebuildBtn.disabled = !this.isEditable;
4537
+ try {
4538
+ this.currentGraph?.setEditable?.(this.isEditable);
4539
+ } catch {
4540
+ }
4541
+ });
4542
+ this.rootElement.querySelector("#help-btn")?.addEventListener("click", () => this.openHelp());
4543
+ const sourceIconBtn = this.rootElement.querySelector("#source-icon");
4544
+ if (sourceIconBtn) {
4545
+ sourceIconBtn.addEventListener("click", (e) => {
4546
+ e.preventDefault();
4547
+ e.stopPropagation();
4548
+ this.openSourceModal();
4549
+ });
4550
+ }
4551
+ document.addEventListener("keydown", (e) => {
4552
+ if (!this.currentGraph) return;
4553
+ if (e.target instanceof HTMLInputElement || e.target instanceof HTMLSelectElement) return;
4554
+ switch (e.key) {
4555
+ case "Home":
4556
+ this.currentGraph.nav("first");
4557
+ this.updateHistoryLabel();
4558
+ break;
4559
+ case "End":
4560
+ this.currentGraph.nav("last");
4561
+ this.updateHistoryLabel();
4562
+ break;
4563
+ case "ArrowLeft":
4564
+ this.currentGraph.nav("prev");
4565
+ this.updateHistoryLabel();
4566
+ break;
4567
+ case "ArrowRight":
4568
+ this.currentGraph.nav("next");
4569
+ this.updateHistoryLabel();
4570
+ break;
4571
+ }
4572
+ });
4573
+ }
4574
+ getResolvedColorMode() {
4575
+ const mode = this.rootElement.querySelector("#colorMode")?.value;
4576
+ if (mode === "system") {
4577
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
4578
+ }
4579
+ return mode;
4580
+ }
4581
+ getOptions(exampleOptions) {
4582
+ const orientation = this.rootElement.querySelector("#orientation")?.value;
4583
+ return {
4584
+ graph: { orientation },
4585
+ canvas: {
4586
+ width: "100%",
4587
+ height: "100%",
4588
+ colorMode: this.getResolvedColorMode(),
4589
+ editable: this.isEditable,
4590
+ ...exampleOptions?.canvas
4591
+ }
4592
+ };
4593
+ }
4594
+ async renderGraph() {
4595
+ const container = this.rootElement.querySelector(`#${this.graphContainerId}`);
4596
+ if (!container) return;
4597
+ container.innerHTML = "";
4598
+ const example = this.options.examples[this.currentExample];
4599
+ const options = this.getOptions(example.options);
4600
+ try {
4601
+ this.currentGraph = await graph({
4602
+ root: this.graphContainerId,
4603
+ nodes: example.nodes,
4604
+ edges: example.edges,
4605
+ options,
4606
+ events: {
4607
+ historyChange: () => this.updateHistoryLabel()
4608
+ }
4609
+ });
4610
+ this.ingest = new Ingest(this.currentGraph);
4611
+ this.updateHistoryLabel();
4612
+ } catch (e) {
4613
+ console.error("Failed to render graph:", e);
4614
+ container.innerHTML = '<p style="padding: 2rem; color: #ef4444;">Failed to load graph</p>';
4615
+ }
4616
+ }
4617
+ updateHistoryLabel() {
4618
+ const label = this.rootElement.querySelector("#history-label");
4619
+ if (!label || !this.currentGraph) return;
4620
+ try {
4621
+ const idx = this.currentGraph.getHistoryIndex?.() ?? 0;
4622
+ const len = this.currentGraph.getHistoryLength?.() ?? 1;
4623
+ label.textContent = `${idx + 1} / ${len}`;
4624
+ } catch {
4625
+ label.textContent = "\u2014 / \u2014";
4626
+ }
4627
+ }
4628
+ connectExampleSource() {
4629
+ const example = this.options.examples[this.currentExample];
4630
+ if (!example.source) {
4631
+ this.disconnectAllSources();
4632
+ return;
4633
+ }
4634
+ this.disconnectAllSources();
4635
+ if (example.source.type === "websocket") {
4636
+ this.wsUrl = example.source.url;
4637
+ this.wsSource = new WebSocketSource(example.source.url, this.handleIngestMessage.bind(this), this.updateWsStatus);
4638
+ this.wsSource.connect();
4639
+ } else if (example.source.type === "file") {
4640
+ this.fileSource = new FileSource(example.source.path, this.handleIngestMessage.bind(this), this.updateFileStatus);
4641
+ this.fileSource.connect();
4642
+ }
4643
+ }
4644
+ disconnectAllSources() {
4645
+ this.wsSource?.disconnect();
4646
+ this.fsSource?.close();
4647
+ this.fileSource?.close();
4648
+ this.wsSource = null;
4649
+ this.fsSource = null;
4650
+ this.fileSource = null;
4651
+ this.activeSourceType = null;
4652
+ this.wsStatus = "disconnected";
4653
+ this.fsStatus = "disconnected";
4654
+ this.fileStatus = "disconnected";
4655
+ this.updateSourceIcon();
4656
+ }
4657
+ openHelp() {
4658
+ if (!this.helpOverlay) {
4659
+ this.helpOverlay = document.createElement("div");
4660
+ this.helpOverlay.className = "modal-overlay";
4661
+ this.helpOverlay.innerHTML = `
4662
+ <div class="modal" role="dialog" aria-modal="true" aria-labelledby="help-title">
4663
+ <div class="modal-header">
4664
+ <h3 id="help-title">Editing the Graph</h3>
4665
+ <button class="modal-close" title="Close" aria-label="Close">\xD7</button>
4666
+ </div>
4667
+ <div class="modal-body">
4668
+ <p>Here's how to edit the graph:</p>
4669
+ <ul>
4670
+ <li><strong>Enable editing</strong>: Click "Edit"</li>
4671
+ <li><strong>Add a node</strong>: Double\u2011click an empty area</li>
4672
+ <li><strong>Edit a node</strong>: Double\u2011click a node</li>
4673
+ <li><strong>Edit an edge</strong>: Double\u2011click an edge</li>
4674
+ <li><strong>Create an edge</strong>: Click and drag from a node (or its port) onto another node; press Esc to cancel</li>
4675
+ <li><strong>Pan</strong>: Drag on canvas or edges; <strong>Zoom</strong>: Mouse wheel or controls</li>
4676
+ <li><strong>Rebuild</strong>: Use "Rebuild" to re-layout from scratch (enabled in edit mode)</li>
4677
+ </ul>
4678
+ <p>When you're done, click "Done" to lock the canvas.</p>
4679
+ </div>
4680
+ </div>
4681
+ `;
4682
+ document.body.appendChild(this.helpOverlay);
4683
+ this.helpOverlay.addEventListener("click", (e) => {
4684
+ const target = e.target;
4685
+ if (target.classList.contains("modal-overlay") || target.classList.contains("modal-close")) {
4686
+ this.closeHelp();
4687
+ }
4688
+ });
4689
+ }
4690
+ this.helpOverlay.style.display = "flex";
4691
+ }
4692
+ closeHelp() {
4693
+ if (this.helpOverlay) this.helpOverlay.style.display = "none";
4694
+ }
4695
+ handleIngestMessage = async (msg) => {
4696
+ if (!this.ingest) return;
4697
+ await this.ingest.apply(msg);
4698
+ };
4699
+ updateSourceIcon() {
4700
+ const iconBtn = this.rootElement.querySelector("#source-icon");
4701
+ if (!iconBtn) return;
4702
+ iconBtn.classList.remove("active", "connecting", "error");
4703
+ const isConnected = this.activeSourceType === "ws" && this.wsStatus === "connected" || this.activeSourceType === "folder" && this.fsStatus === "connected" || this.activeSourceType === "file" && this.fileStatus === "connected";
4704
+ const isConnecting = this.activeSourceType === "ws" && this.wsStatus === "connecting" || this.activeSourceType === "folder" && this.fsStatus === "opening" || this.activeSourceType === "file" && this.fileStatus === "connecting";
4705
+ const hasError = this.activeSourceType === "ws" && this.wsStatus === "error" || this.activeSourceType === "folder" && this.fsStatus === "error" || this.activeSourceType === "file" && this.fileStatus === "error";
4706
+ let icon = "\u{1F4E1}";
4707
+ if (this.activeSourceType === "folder") {
4708
+ icon = "\u{1F4C1}";
4709
+ } else if (this.activeSourceType === "file") {
4710
+ icon = "\u{1F4C4}";
4711
+ }
4712
+ if (isConnected) {
4713
+ iconBtn.classList.add("active");
4714
+ iconBtn.textContent = icon;
4715
+ } else if (isConnecting) {
4716
+ iconBtn.classList.add("connecting");
4717
+ iconBtn.textContent = icon;
4718
+ } else if (hasError) {
4719
+ iconBtn.classList.add("error");
4720
+ iconBtn.textContent = icon;
4721
+ } else {
4722
+ iconBtn.textContent = "\u{1F4E1}";
4723
+ }
4724
+ }
4725
+ updateWsStatus = (status, detail) => {
4726
+ if (status === "connecting" || status === "reconnecting") {
4727
+ this.wsStatus = "connecting";
4728
+ this.activeSourceType = "ws";
4729
+ } else if (status === "connected") {
4730
+ this.wsStatus = "connected";
4731
+ this.activeSourceType = "ws";
4732
+ if (this.fsSource && this.fsStatus === "connected") {
4733
+ this.fsSource.close();
4734
+ }
4735
+ if (this.fileSource && this.fileStatus === "connected") {
4736
+ this.fileSource.close();
4737
+ }
4738
+ } else if (status === "error") {
4739
+ this.wsStatus = "error";
4740
+ } else {
4741
+ this.wsStatus = "disconnected";
4742
+ if (this.activeSourceType === "ws") {
4743
+ this.activeSourceType = null;
4744
+ }
4745
+ }
4746
+ this.updateSourceIcon();
4747
+ this.updateSourceModal();
4748
+ };
4749
+ updateFsStatus = (status, detail) => {
4750
+ if (status === "opened") {
4751
+ this.fsStatus = "opening";
4752
+ this.activeSourceType = "folder";
4753
+ if (this.wsSource && this.wsStatus === "connected") {
4754
+ this.wsSource.disconnect();
4755
+ }
4756
+ } else if (status === "reading") {
4757
+ this.fsStatus = "connected";
4758
+ this.activeSourceType = "folder";
4759
+ } else if (status === "error") {
4760
+ this.fsStatus = "error";
4761
+ } else if (status === "closed") {
4762
+ this.fsStatus = "disconnected";
4763
+ if (this.activeSourceType === "folder") {
4764
+ this.activeSourceType = null;
4765
+ }
4766
+ } else {
4767
+ this.fsStatus = "disconnected";
4768
+ }
4769
+ this.updateSourceIcon();
4770
+ this.updateSourceModal();
4771
+ };
4772
+ updateFileStatus = (status, detail) => {
4773
+ if (status === "opened") {
4774
+ this.fileStatus = "connecting";
4775
+ this.activeSourceType = "file";
4776
+ if (this.wsSource && this.wsStatus === "connected") {
4777
+ this.wsSource.disconnect();
4778
+ }
4779
+ if (this.fsSource && this.fsStatus === "connected") {
4780
+ this.fsSource.close();
4781
+ }
4782
+ } else if (status === "reading") {
4783
+ this.fileStatus = "connected";
4784
+ this.activeSourceType = "file";
4785
+ } else if (status === "error") {
4786
+ this.fileStatus = "error";
4787
+ } else if (status === "closed") {
4788
+ this.fileStatus = "disconnected";
4789
+ if (this.activeSourceType === "file") {
4790
+ this.activeSourceType = null;
4791
+ }
4792
+ } else {
4793
+ this.fileStatus = "disconnected";
4794
+ }
4795
+ this.updateSourceIcon();
4796
+ this.updateSourceModal();
4797
+ };
4798
+ createSourceModal() {
4799
+ if (this.sourceModal) return this.sourceModal;
4800
+ this.sourceModal = document.createElement("div");
4801
+ this.sourceModal.className = "modal-overlay";
4802
+ this.sourceModal.style.display = "none";
4803
+ this.sourceModal.innerHTML = `
4804
+ <div class="modal" role="dialog" aria-modal="true" aria-labelledby="source-modal-title">
4805
+ <div class="modal-header">
4806
+ <h3 id="source-modal-title">Data Source Connection</h3>
4807
+ <button class="modal-close" title="Close" aria-label="Close">\xD7</button>
4808
+ </div>
4809
+ <div class="modal-body">
4810
+ <div class="source-type-selector">
4811
+ <button class="source-type-option" data-source="ws">\u{1F4E1} WebSocket</button>
4812
+ <button class="source-type-option" data-source="folder">\u{1F4C1} Folder</button>
4813
+ </div>
4814
+
4815
+ <div class="source-controls" data-source="ws">
4816
+ <div class="form-group">
4817
+ <label for="source-modal-url">WebSocket URL</label>
4818
+ <input type="text" id="source-modal-url" value="${this.wsUrl}" />
4819
+ </div>
4820
+ <div class="button-group">
4821
+ <button id="source-modal-connect-ws" class="primary">Connect</button>
4822
+ <button id="source-modal-disconnect-ws">Disconnect</button>
4823
+ <button id="source-modal-change-ws">Change Connection</button>
4824
+ </div>
4825
+ </div>
4826
+
4827
+ <div class="source-controls" data-source="folder">
4828
+ <div class="form-group">
4829
+ <label>File System Source</label>
4830
+ <p style="font-size: 0.875rem; color: var(--color-text-muted); margin-top: 0.25rem;">
4831
+ Select a directory containing a graph.ndjson file to watch for changes.
4832
+ </p>
4833
+ </div>
4834
+ <div class="button-group">
4835
+ <button id="source-modal-connect-folder" class="primary">Open Folder</button>
4836
+ <button id="source-modal-disconnect-folder">Disconnect</button>
4837
+ </div>
4838
+ </div>
4839
+
4840
+ <div id="source-modal-status"></div>
4841
+ </div>
4842
+ </div>
4843
+ `;
4844
+ document.body.appendChild(this.sourceModal);
4845
+ this.sourceModal.addEventListener("click", (e) => {
4846
+ const target = e.target;
4847
+ if (target.classList.contains("modal-overlay") || target.classList.contains("modal-close")) {
4848
+ this.closeSourceModal();
4849
+ }
4850
+ });
4851
+ this.sourceModal.querySelectorAll(".source-type-option").forEach((btn) => {
4852
+ btn.addEventListener("click", () => {
4853
+ const sourceType = btn.getAttribute("data-source");
4854
+ if (sourceType) {
4855
+ this.selectSourceType(sourceType);
4856
+ }
4857
+ });
4858
+ });
4859
+ document.getElementById("source-modal-connect-ws")?.addEventListener("click", () => this.handleConnect());
4860
+ document.getElementById("source-modal-disconnect-ws")?.addEventListener("click", () => this.handleDisconnect());
4861
+ document.getElementById("source-modal-change-ws")?.addEventListener("click", () => this.handleChangeConnection());
4862
+ document.getElementById("source-modal-connect-folder")?.addEventListener("click", () => this.handleOpenFolder());
4863
+ document.getElementById("source-modal-disconnect-folder")?.addEventListener("click", () => this.handleCloseFolder());
4864
+ document.getElementById("source-modal-url")?.addEventListener("keydown", (e) => {
4865
+ if (e.key === "Enter" && this.wsStatus !== "connected") {
4866
+ this.handleConnect();
4867
+ }
4868
+ });
4869
+ return this.sourceModal;
4870
+ }
4871
+ selectSourceType(type, skipUpdate = false) {
4872
+ if (!this.sourceModal) return;
4873
+ this.sourceModal.querySelectorAll(".source-type-option").forEach((btn) => {
4874
+ if (btn.getAttribute("data-source") === type) {
4875
+ btn.classList.add("active");
4876
+ } else {
4877
+ btn.classList.remove("active");
4878
+ }
4879
+ });
4880
+ this.sourceModal.querySelectorAll(".source-controls").forEach((controls) => {
4881
+ if (controls.getAttribute("data-source") === type) {
4882
+ controls.classList.add("active");
4883
+ } else {
4884
+ controls.classList.remove("active");
4885
+ }
4886
+ });
4887
+ if (!skipUpdate) {
4888
+ this.updateSourceModalContent();
4889
+ }
4890
+ }
4891
+ updateSourceModalContent() {
4892
+ if (!this.sourceModal) return;
4893
+ const urlInput = document.getElementById("source-modal-url");
4894
+ const connectWsBtn = document.getElementById("source-modal-connect-ws");
4895
+ const disconnectWsBtn = document.getElementById("source-modal-disconnect-ws");
4896
+ const changeWsBtn = document.getElementById("source-modal-change-ws");
4897
+ if (urlInput && connectWsBtn && disconnectWsBtn && changeWsBtn) {
4898
+ const isWsConnected = this.wsStatus === "connected";
4899
+ const isWsConnecting = this.wsStatus === "connecting";
4900
+ connectWsBtn.disabled = isWsConnected || isWsConnecting;
4901
+ disconnectWsBtn.disabled = !isWsConnected || isWsConnecting;
4902
+ changeWsBtn.disabled = !isWsConnected || isWsConnecting;
4903
+ urlInput.disabled = isWsConnecting;
4904
+ }
4905
+ const connectFolderBtn = document.getElementById("source-modal-connect-folder");
4906
+ const disconnectFolderBtn = document.getElementById("source-modal-disconnect-folder");
4907
+ if (connectFolderBtn && disconnectFolderBtn) {
4908
+ const isFolderConnected = this.fsStatus === "connected";
4909
+ const isFolderOpening = this.fsStatus === "opening";
4910
+ connectFolderBtn.disabled = isFolderConnected || isFolderOpening;
4911
+ disconnectFolderBtn.disabled = !isFolderConnected || isFolderOpening;
4912
+ }
4913
+ const statusDiv = document.getElementById("source-modal-status");
4914
+ if (!statusDiv) return;
4915
+ const currentUrl = urlInput?.value || this.wsUrl;
4916
+ statusDiv.innerHTML = "";
4917
+ if (this.activeSourceType === "ws") {
4918
+ if (this.wsStatus === "connecting") {
4919
+ statusDiv.innerHTML = `
4920
+ <div class="status-message info">
4921
+ <span class="loading-spinner"></span>
4922
+ Connecting to ${currentUrl}...
4923
+ </div>
4924
+ `;
4925
+ } else if (this.wsStatus === "connected") {
4926
+ statusDiv.innerHTML = `
4927
+ <div class="status-message success">
4928
+ \u2713 Connected to ${currentUrl}
4929
+ </div>
4930
+ `;
4931
+ } else if (this.wsStatus === "error") {
4932
+ statusDiv.innerHTML = `
4933
+ <div class="status-message error">
4934
+ \u2717 Connection error. Please check the URL and try again.
4935
+ </div>
4936
+ `;
4937
+ } else {
4938
+ statusDiv.innerHTML = `
4939
+ <div class="status-message info">
4940
+ Not connected
4941
+ </div>
4942
+ `;
4943
+ }
4944
+ } else if (this.activeSourceType === "folder") {
4945
+ if (this.fsStatus === "opening") {
4946
+ statusDiv.innerHTML = `
4947
+ <div class="status-message info">
4948
+ <span class="loading-spinner"></span>
4949
+ Opening folder...
4950
+ </div>
4951
+ `;
4952
+ } else if (this.fsStatus === "connected") {
4953
+ statusDiv.innerHTML = `
4954
+ <div class="status-message success">
4955
+ \u2713 Folder connected and watching for changes
4956
+ </div>
4957
+ `;
4958
+ } else if (this.fsStatus === "error") {
4959
+ statusDiv.innerHTML = `
4960
+ <div class="status-message error">
4961
+ \u2717 Error opening folder. Please try again.
4962
+ </div>
4963
+ `;
4964
+ } else {
4965
+ statusDiv.innerHTML = `
4966
+ <div class="status-message info">
4967
+ Not connected
4968
+ </div>
4969
+ `;
4970
+ }
4971
+ } else if (this.activeSourceType === "file") {
4972
+ const example = this.options.examples[this.currentExample];
4973
+ const filePath = example.source?.type === "file" ? example.source.path : "";
4974
+ if (this.fileStatus === "connecting") {
4975
+ statusDiv.innerHTML = `
4976
+ <div class="status-message info">
4977
+ <span class="loading-spinner"></span>
4978
+ Connecting to ${filePath}...
4979
+ </div>
4980
+ `;
4981
+ } else if (this.fileStatus === "connected") {
4982
+ statusDiv.innerHTML = `
4983
+ <div class="status-message success">
4984
+ \u2713 Connected to ${filePath}
4985
+ </div>
4986
+ `;
4987
+ } else if (this.fileStatus === "error") {
4988
+ statusDiv.innerHTML = `
4989
+ <div class="status-message error">
4990
+ \u2717 Error loading file. Please check the path and try again.
4991
+ </div>
4992
+ `;
4993
+ } else {
4994
+ statusDiv.innerHTML = `
4995
+ <div class="status-message info">
4996
+ Not connected
4997
+ </div>
4998
+ `;
4999
+ }
5000
+ } else {
5001
+ statusDiv.innerHTML = `
5002
+ <div class="status-message info">
5003
+ Select a source type to connect
5004
+ </div>
5005
+ `;
5006
+ }
5007
+ }
5008
+ updateSourceModal() {
5009
+ if (!this.sourceModal) return;
5010
+ const activeType = (this.activeSourceType === "file" ? "ws" : this.activeSourceType) || "ws";
5011
+ this.selectSourceType(activeType, true);
5012
+ this.updateSourceModalContent();
5013
+ }
5014
+ openSourceModal() {
5015
+ this.createSourceModal();
5016
+ if (this.sourceModal) {
5017
+ const urlInput = document.getElementById("source-modal-url");
5018
+ if (urlInput) {
5019
+ urlInput.value = this.wsUrl;
5020
+ }
5021
+ const activeType = (this.activeSourceType === "file" ? "ws" : this.activeSourceType) || "ws";
5022
+ this.selectSourceType(activeType);
5023
+ this.updateSourceModal();
5024
+ this.sourceModal.style.display = "flex";
5025
+ }
5026
+ }
5027
+ closeSourceModal() {
5028
+ if (this.sourceModal) {
5029
+ this.sourceModal.style.display = "none";
5030
+ }
5031
+ }
5032
+ handleConnect() {
5033
+ const urlInput = document.getElementById("source-modal-url");
5034
+ if (!urlInput) return;
5035
+ const url = urlInput.value.trim() || "ws://localhost:8787";
5036
+ this.wsUrl = url;
5037
+ if (this.wsSource) {
5038
+ this.wsSource.disconnect();
5039
+ }
5040
+ this.wsSource = new WebSocketSource(url, this.handleIngestMessage.bind(this), this.updateWsStatus);
5041
+ this.wsSource.connect();
5042
+ this.updateSourceModal();
5043
+ }
5044
+ handleDisconnect() {
5045
+ this.wsSource?.disconnect();
5046
+ this.updateSourceModal();
5047
+ }
5048
+ handleChangeConnection() {
5049
+ if (this.wsSource) {
5050
+ this.wsSource.disconnect();
5051
+ }
5052
+ const urlInput = document.getElementById("source-modal-url");
5053
+ if (urlInput) {
5054
+ urlInput.focus();
5055
+ urlInput.select();
5056
+ }
5057
+ this.updateSourceModal();
5058
+ }
5059
+ async handleOpenFolder() {
5060
+ if (!this.fsSource) {
5061
+ this.fsSource = new FileSystemSource(this.handleIngestMessage.bind(this), this.updateFsStatus);
5062
+ }
5063
+ this.updateSourceModal();
5064
+ await this.fsSource.openDirectory();
5065
+ }
5066
+ handleCloseFolder() {
5067
+ this.fsSource?.close();
5068
+ this.updateSourceModal();
5069
+ }
2964
5070
  };
2965
5071
 
2966
5072
  // src/index.ts
@@ -2971,6 +5077,11 @@ async function graph(args = { root: "app" }) {
2971
5077
  }
2972
5078
  var index_default = graph;
2973
5079
  export {
5080
+ FileSource,
5081
+ FileSystemSource,
5082
+ Ingest,
5083
+ Playground,
5084
+ WebSocketSource,
2974
5085
  index_default as default,
2975
5086
  graph
2976
5087
  };