@3plate/graph-core 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -30,6 +30,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ FileSource: () => FileSource,
34
+ FileSystemSource: () => FileSystemSource,
35
+ Ingest: () => Ingest,
36
+ Playground: () => Playground,
37
+ Updater: () => Updater,
38
+ WebSocketSource: () => WebSocketSource,
33
39
  default: () => index_default,
34
40
  graph: () => graph
35
41
  });
@@ -1375,15 +1381,16 @@ var Layout = class _Layout {
1375
1381
 
1376
1382
  // src/canvas/marker.tsx
1377
1383
  var import_jsx_runtime = require("jsx-dom/jsx-runtime");
1378
- function arrow(size, reverse = false) {
1384
+ function arrow(size, reverse = false, prefix = "") {
1379
1385
  const h = size / 1.5;
1380
1386
  const w = size;
1381
1387
  const ry = h / 2;
1382
1388
  const suffix = reverse ? "-reverse" : "";
1389
+ const id = prefix ? `${prefix}-g3p-marker-arrow${suffix}` : `g3p-marker-arrow${suffix}`;
1383
1390
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1384
1391
  "marker",
1385
1392
  {
1386
- id: `g3p-marker-arrow${suffix}`,
1393
+ id,
1387
1394
  className: "g3p-marker g3p-marker-arrow",
1388
1395
  markerWidth: size,
1389
1396
  markerHeight: size,
@@ -1395,14 +1402,15 @@ function arrow(size, reverse = false) {
1395
1402
  }
1396
1403
  );
1397
1404
  }
1398
- function circle(size, reverse = false) {
1405
+ function circle(size, reverse = false, prefix = "") {
1399
1406
  const r = size / 3;
1400
1407
  const cy = size / 2;
1401
1408
  const suffix = reverse ? "-reverse" : "";
1409
+ const id = prefix ? `${prefix}-g3p-marker-circle${suffix}` : `g3p-marker-circle${suffix}`;
1402
1410
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1403
1411
  "marker",
1404
1412
  {
1405
- id: `g3p-marker-circle${suffix}`,
1413
+ id,
1406
1414
  className: "g3p-marker g3p-marker-circle",
1407
1415
  markerWidth: size,
1408
1416
  markerHeight: size,
@@ -1414,15 +1422,16 @@ function circle(size, reverse = false) {
1414
1422
  }
1415
1423
  );
1416
1424
  }
1417
- function diamond(size, reverse = false) {
1425
+ function diamond(size, reverse = false, prefix = "") {
1418
1426
  const w = size * 0.7;
1419
1427
  const h = size / 2;
1420
1428
  const cy = size / 2;
1421
1429
  const suffix = reverse ? "-reverse" : "";
1430
+ const id = prefix ? `${prefix}-g3p-marker-diamond${suffix}` : `g3p-marker-diamond${suffix}`;
1422
1431
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1423
1432
  "marker",
1424
1433
  {
1425
- id: `g3p-marker-diamond${suffix}`,
1434
+ id,
1426
1435
  className: "g3p-marker g3p-marker-diamond",
1427
1436
  markerWidth: size,
1428
1437
  markerHeight: size,
@@ -1434,14 +1443,15 @@ function diamond(size, reverse = false) {
1434
1443
  }
1435
1444
  );
1436
1445
  }
1437
- function bar(size, reverse = false) {
1446
+ function bar(size, reverse = false, prefix = "") {
1438
1447
  const h = size * 0.6;
1439
1448
  const cy = size / 2;
1440
1449
  const suffix = reverse ? "-reverse" : "";
1450
+ const id = prefix ? `${prefix}-g3p-marker-bar${suffix}` : `g3p-marker-bar${suffix}`;
1441
1451
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1442
1452
  "marker",
1443
1453
  {
1444
- id: `g3p-marker-bar${suffix}`,
1454
+ id,
1445
1455
  className: "g3p-marker g3p-marker-bar",
1446
1456
  markerWidth: size,
1447
1457
  markerHeight: size,
@@ -1453,7 +1463,7 @@ function bar(size, reverse = false) {
1453
1463
  }
1454
1464
  );
1455
1465
  }
1456
- function none(size, reverse = false) {
1466
+ function none(size, reverse = false, prefix = "") {
1457
1467
  return void 0;
1458
1468
  }
1459
1469
  function normalize(data) {
@@ -2235,6 +2245,9 @@ var Seg2 = class {
2235
2245
  if (this.source.isDummy) source = void 0;
2236
2246
  if (this.target.isDummy) target = void 0;
2237
2247
  const typeClass = this.type ? `g3p-edge-type-${this.type}` : "";
2248
+ const prefix = this.canvas.markerPrefix;
2249
+ const markerStartId = source ? prefix ? `${prefix}-g3p-marker-${source}-reverse` : `g3p-marker-${source}-reverse` : void 0;
2250
+ const markerEndId = target ? prefix ? `${prefix}-g3p-marker-${target}` : `g3p-marker-${target}` : void 0;
2238
2251
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2239
2252
  "g",
2240
2253
  {
@@ -2249,8 +2262,8 @@ var Seg2 = class {
2249
2262
  d: this.svg,
2250
2263
  fill: "none",
2251
2264
  className: "g3p-seg-line",
2252
- markerStart: source ? `url(#g3p-marker-${source}-reverse)` : void 0,
2253
- markerEnd: target ? `url(#g3p-marker-${target})` : void 0
2265
+ markerStart: markerStartId ? `url(#${markerStartId})` : void 0,
2266
+ markerEnd: markerEndId ? `url(#${markerEndId})` : void 0
2254
2267
  }
2255
2268
  ),
2256
2269
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
@@ -2835,6 +2848,10 @@ var Canvas = class {
2835
2848
  curNodes;
2836
2849
  curSegs;
2837
2850
  updating;
2851
+ // Unique marker ID prefix for this canvas instance
2852
+ markerPrefix;
2853
+ // Dynamic style element for this instance (for cleanup)
2854
+ dynamicStyleEl;
2838
2855
  // Pan-zoom state
2839
2856
  panScale = null;
2840
2857
  zoomControls;
@@ -2848,6 +2865,13 @@ var Canvas = class {
2848
2865
  constructor(api, options) {
2849
2866
  Object.assign(this, options);
2850
2867
  this.api = api;
2868
+ this.reset();
2869
+ this.markerPrefix = api.root.replace(/[^a-zA-Z0-9-_]/g, "-");
2870
+ this.createMeasurementContainer();
2871
+ this.createCanvasContainer();
2872
+ if (this.panZoom) this.setupPanZoom();
2873
+ }
2874
+ reset() {
2851
2875
  this.allNodes = /* @__PURE__ */ new Map();
2852
2876
  this.curNodes = /* @__PURE__ */ new Map();
2853
2877
  this.curSegs = /* @__PURE__ */ new Map();
@@ -2856,9 +2880,7 @@ var Canvas = class {
2856
2880
  this.transform = { x: 0, y: 0, scale: 1 };
2857
2881
  this.editMode = new EditMode();
2858
2882
  this.editMode.editable = this.editable;
2859
- this.createMeasurementContainer();
2860
- this.createCanvasContainer();
2861
- if (this.panZoom) this.setupPanZoom();
2883
+ if (this.group) this.group.innerHTML = "";
2862
2884
  }
2863
2885
  createMeasurementContainer() {
2864
2886
  this.measurement = document.createElement("div");
@@ -3043,12 +3065,13 @@ var Canvas = class {
3043
3065
  }
3044
3066
  generateDynamicStyles() {
3045
3067
  let css = "";
3046
- css += themeToCSS(this.theme, `.g3p-canvas-container`);
3068
+ const scope = `[data-g3p-instance="${this.markerPrefix}"]`;
3069
+ css += themeToCSS(this.theme, scope);
3047
3070
  for (const [type, vars] of Object.entries(this.nodeTypes)) {
3048
- css += themeToCSS(vars, `.g3p-node-type-${type}`, "node");
3071
+ css += themeToCSS(vars, `${scope} .g3p-node-type-${type}`, "node");
3049
3072
  }
3050
3073
  for (const [type, vars] of Object.entries(this.edgeTypes)) {
3051
- css += themeToCSS(vars, `.g3p-edge-type-${type}`);
3074
+ css += themeToCSS(vars, `${scope} .g3p-edge-type-${type}`);
3052
3075
  }
3053
3076
  return css;
3054
3077
  }
@@ -3061,15 +3084,18 @@ var Canvas = class {
3061
3084
  }
3062
3085
  const dynamicStyles = this.generateDynamicStyles();
3063
3086
  if (dynamicStyles) {
3064
- const dynamicStyleEl = document.createElement("style");
3065
- dynamicStyleEl.textContent = dynamicStyles;
3066
- document.head.appendChild(dynamicStyleEl);
3087
+ this.dynamicStyleEl?.remove();
3088
+ this.dynamicStyleEl = document.createElement("style");
3089
+ this.dynamicStyleEl.id = `g3p-styles-${this.markerPrefix}`;
3090
+ this.dynamicStyleEl.textContent = dynamicStyles;
3091
+ document.head.appendChild(this.dynamicStyleEl);
3067
3092
  }
3068
3093
  const colorModeClass = this.colorMode !== "system" ? `g3p-${this.colorMode}` : "";
3069
3094
  this.container = /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3070
3095
  "div",
3071
3096
  {
3072
3097
  className: `g3p-canvas-container ${colorModeClass}`.trim(),
3098
+ "data-g3p-instance": this.markerPrefix,
3073
3099
  ref: (el) => this.container = el,
3074
3100
  onContextMenu: this.onContextMenu.bind(this),
3075
3101
  children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
@@ -3085,8 +3111,8 @@ var Canvas = class {
3085
3111
  onDblClick: this.onDoubleClick.bind(this),
3086
3112
  children: [
3087
3113
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("defs", { children: [
3088
- Object.values(markerDefs).map((marker) => marker(this.markerSize, false)),
3089
- Object.values(markerDefs).map((marker) => marker(this.markerSize, true))
3114
+ Object.values(markerDefs).map((marker) => marker(this.markerSize, false, this.markerPrefix)),
3115
+ Object.values(markerDefs).map((marker) => marker(this.markerSize, true, this.markerPrefix))
3090
3116
  ] }),
3091
3117
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3092
3118
  "g",
@@ -3443,6 +3469,40 @@ var Canvas = class {
3443
3469
  }
3444
3470
  return { type: "canvas" };
3445
3471
  }
3472
+ /** Update theme and type styles dynamically */
3473
+ updateStyles(options) {
3474
+ if (options.theme !== void 0) this.theme = options.theme;
3475
+ if (options.nodeTypes !== void 0) this.nodeTypes = options.nodeTypes;
3476
+ if (options.edgeTypes !== void 0) this.edgeTypes = options.edgeTypes;
3477
+ const dynamicStyles = this.generateDynamicStyles();
3478
+ if (dynamicStyles) {
3479
+ if (!this.dynamicStyleEl) {
3480
+ this.dynamicStyleEl = document.createElement("style");
3481
+ this.dynamicStyleEl.id = `g3p-styles-${this.markerPrefix}`;
3482
+ document.head.appendChild(this.dynamicStyleEl);
3483
+ }
3484
+ this.dynamicStyleEl.textContent = dynamicStyles;
3485
+ } else if (this.dynamicStyleEl) {
3486
+ this.dynamicStyleEl.remove();
3487
+ this.dynamicStyleEl = void 0;
3488
+ }
3489
+ }
3490
+ /** Update color mode without recreating the canvas */
3491
+ setColorMode(colorMode) {
3492
+ if (!this.container) return;
3493
+ this.colorMode = colorMode;
3494
+ this.container.classList.remove("g3p-light", "g3p-dark");
3495
+ if (colorMode !== "system") {
3496
+ this.container.classList.add(`g3p-${colorMode}`);
3497
+ }
3498
+ }
3499
+ /** Cleanup resources when the canvas is destroyed */
3500
+ destroy() {
3501
+ this.dynamicStyleEl?.remove();
3502
+ this.dynamicStyleEl = void 0;
3503
+ this.measurement?.remove();
3504
+ this.measurement = void 0;
3505
+ }
3446
3506
  };
3447
3507
  var themeVarMap = {
3448
3508
  // Canvas
@@ -3557,26 +3617,50 @@ var Updater = class _Updater {
3557
3617
  this.update.addNodes.push(node);
3558
3618
  return this;
3559
3619
  }
3620
+ addNodes(...nodes) {
3621
+ this.update.addNodes.push(...nodes);
3622
+ return this;
3623
+ }
3560
3624
  deleteNode(node) {
3561
3625
  this.update.removeNodes.push(node);
3562
3626
  return this;
3563
3627
  }
3628
+ deleteNodes(...nodes) {
3629
+ this.update.removeNodes.push(...nodes);
3630
+ return this;
3631
+ }
3564
3632
  updateNode(node) {
3565
3633
  this.update.updateNodes.push(node);
3566
3634
  return this;
3567
3635
  }
3636
+ updateNodes(...nodes) {
3637
+ this.update.updateNodes.push(...nodes);
3638
+ return this;
3639
+ }
3568
3640
  addEdge(edge) {
3569
3641
  this.update.addEdges.push(edge);
3570
3642
  return this;
3571
3643
  }
3644
+ addEdges(...edges) {
3645
+ this.update.addEdges.push(...edges);
3646
+ return this;
3647
+ }
3572
3648
  deleteEdge(edge) {
3573
3649
  this.update.removeEdges.push(edge);
3574
3650
  return this;
3575
3651
  }
3652
+ deleteEdges(...edges) {
3653
+ this.update.removeEdges.push(...edges);
3654
+ return this;
3655
+ }
3576
3656
  updateEdge(edge) {
3577
3657
  this.update.updateEdges.push(edge);
3578
3658
  return this;
3579
3659
  }
3660
+ updateEdges(...edges) {
3661
+ this.update.updateEdges.push(...edges);
3662
+ return this;
3663
+ }
3580
3664
  static add(nodes, edges) {
3581
3665
  const updater = new _Updater();
3582
3666
  updater.update.addNodes = nodes;
@@ -3607,19 +3691,8 @@ var API = class {
3607
3691
  constructor(args) {
3608
3692
  this.root = args.root;
3609
3693
  this.options = applyDefaults(args.options);
3610
- let graph2 = new Graph({ options: this.options.graph });
3611
- this.state = { graph: graph2, update: null };
3612
3694
  this.events = args.events || {};
3613
- this.seq = [this.state];
3614
- this.index = 0;
3615
- this.nodeIds = /* @__PURE__ */ new Map();
3616
- this.edgeIds = /* @__PURE__ */ new Map();
3617
- this.nodeVersions = /* @__PURE__ */ new Map();
3618
- this.nodeOverrides = /* @__PURE__ */ new Map();
3619
- this.edgeOverrides = /* @__PURE__ */ new Map();
3620
- this.nodeFields = /* @__PURE__ */ new Map();
3621
- this.nextNodeId = 1;
3622
- this.nextEdgeId = 1;
3695
+ this.reset();
3623
3696
  this.canvas = new Canvas(this, {
3624
3697
  ...this.options.canvas,
3625
3698
  dummyNodeSize: this.options.graph.dummyNodeSize,
@@ -3633,6 +3706,35 @@ var API = class {
3633
3706
  this.history = [];
3634
3707
  }
3635
3708
  }
3709
+ reset() {
3710
+ let graph2 = new Graph({ options: this.options.graph });
3711
+ this.state = { graph: graph2, update: null };
3712
+ this.seq = [this.state];
3713
+ this.index = 0;
3714
+ this.nodeIds = /* @__PURE__ */ new Map();
3715
+ this.edgeIds = /* @__PURE__ */ new Map();
3716
+ this.nodeVersions = /* @__PURE__ */ new Map();
3717
+ this.nodeOverrides = /* @__PURE__ */ new Map();
3718
+ this.edgeOverrides = /* @__PURE__ */ new Map();
3719
+ this.nodeFields = /* @__PURE__ */ new Map();
3720
+ this.nextNodeId = 1;
3721
+ this.nextEdgeId = 1;
3722
+ this.canvas?.reset?.();
3723
+ }
3724
+ /** Initialize the API */
3725
+ async init() {
3726
+ const root = document.getElementById(this.root);
3727
+ if (!root) throw new Error("root element not found");
3728
+ root.appendChild(this.canvas.container);
3729
+ await this.applyHistory();
3730
+ if (this.events.onInit) {
3731
+ this.events.onInit();
3732
+ }
3733
+ }
3734
+ async applyHistory() {
3735
+ for (const update of this.history)
3736
+ await this.applyUpdate(update);
3737
+ }
3636
3738
  /** Current history index (0-based) */
3637
3739
  getHistoryIndex() {
3638
3740
  return this.index;
@@ -3645,17 +3747,25 @@ var API = class {
3645
3747
  setEditable(editable) {
3646
3748
  this.canvas.editMode.editable = editable;
3647
3749
  }
3750
+ /** Replace entire history (clears prior) */
3751
+ async replaceHistory(frames) {
3752
+ this.reset();
3753
+ this.history = frames;
3754
+ await this.applyHistory();
3755
+ }
3756
+ /** Rebuild from snapshot (nodes/edges) */
3757
+ async replaceSnapshot(nodes, edges, description) {
3758
+ this.reset();
3759
+ this.history = [{
3760
+ addNodes: nodes,
3761
+ addEdges: edges,
3762
+ description
3763
+ }];
3764
+ await this.applyHistory();
3765
+ }
3648
3766
  get graph() {
3649
3767
  return this.state.graph;
3650
3768
  }
3651
- /** Initialize the API */
3652
- async init() {
3653
- const root = document.getElementById(this.root);
3654
- if (!root) throw new Error("root element not found");
3655
- root.appendChild(this.canvas.container);
3656
- for (const update of this.history)
3657
- await this.applyUpdate(update);
3658
- }
3659
3769
  /** Navigate to a different state */
3660
3770
  nav(nav) {
3661
3771
  let newIndex;
@@ -4075,6 +4185,1064 @@ var API = class {
4075
4185
  else
4076
4186
  await this.deleteEdge(edge.data);
4077
4187
  }
4188
+ /** Update theme and type styles dynamically */
4189
+ updateStyles(options) {
4190
+ this.canvas?.updateStyles(options);
4191
+ }
4192
+ /** Update color mode without recreating the canvas */
4193
+ setColorMode(colorMode) {
4194
+ this.canvas?.setColorMode(colorMode);
4195
+ }
4196
+ /** Cleanup resources when the graph is destroyed */
4197
+ destroy() {
4198
+ this.canvas?.destroy();
4199
+ }
4200
+ };
4201
+
4202
+ // src/api/ingest.ts
4203
+ var Ingest = class {
4204
+ constructor(api) {
4205
+ this.api = api;
4206
+ }
4207
+ /**
4208
+ * Apply an incoming ingest message to the API.
4209
+ * - snapshot: rebuild state from nodes/edges (clears prior history)
4210
+ * - update: apply incremental update
4211
+ * - history: initialize from a set of frames (clears prior history)
4212
+ */
4213
+ async apply(msg) {
4214
+ switch (msg.type) {
4215
+ case "snapshot": {
4216
+ await this.api.replaceSnapshot(msg.nodes, msg.edges, msg.description);
4217
+ break;
4218
+ }
4219
+ case "update": {
4220
+ await this.api.update((u) => {
4221
+ if (msg.addNodes) u.addNodes(...msg.addNodes);
4222
+ if (msg.removeNodes) u.deleteNodes(...msg.removeNodes);
4223
+ if (msg.updateNodes) u.updateNodes(...msg.updateNodes);
4224
+ if (msg.addEdges) u.addEdges(...msg.addEdges);
4225
+ if (msg.removeEdges) u.deleteEdges(...msg.removeEdges);
4226
+ if (msg.updateEdges) u.updateEdges(...msg.updateEdges);
4227
+ if (msg.description) u.describe(msg.description);
4228
+ });
4229
+ break;
4230
+ }
4231
+ case "history": {
4232
+ await this.api.replaceHistory(msg.frames);
4233
+ break;
4234
+ }
4235
+ }
4236
+ }
4237
+ };
4238
+
4239
+ // src/api/sources/WebSocketSource.ts
4240
+ var WebSocketSource = class {
4241
+ url;
4242
+ ws = null;
4243
+ onMessage;
4244
+ onStatus;
4245
+ reconnectMs;
4246
+ closedByUser = false;
4247
+ connectStartTime = null;
4248
+ totalTimeoutMs = 1e4;
4249
+ totalTimeoutTimer = null;
4250
+ constructor(url, onMessage, onStatus, reconnectMs = 1500) {
4251
+ this.url = url;
4252
+ this.onMessage = onMessage;
4253
+ this.onStatus = onStatus;
4254
+ this.reconnectMs = reconnectMs;
4255
+ }
4256
+ connect() {
4257
+ this.closedByUser = false;
4258
+ this.connectStartTime = Date.now();
4259
+ this.startTotalTimeout();
4260
+ this.open();
4261
+ }
4262
+ disconnect() {
4263
+ this.closedByUser = true;
4264
+ this.clearTotalTimeout();
4265
+ if (this.ws) {
4266
+ try {
4267
+ this.ws.close();
4268
+ } catch {
4269
+ }
4270
+ this.ws = null;
4271
+ }
4272
+ this.onStatus?.("closed");
4273
+ }
4274
+ startTotalTimeout() {
4275
+ this.clearTotalTimeout();
4276
+ this.totalTimeoutTimer = window.setTimeout(() => {
4277
+ if (!this.closedByUser && this.ws?.readyState !== WebSocket.OPEN) {
4278
+ this.closedByUser = true;
4279
+ if (this.ws) {
4280
+ try {
4281
+ this.ws.close();
4282
+ } catch {
4283
+ }
4284
+ this.ws = null;
4285
+ }
4286
+ this.clearTotalTimeout();
4287
+ this.onStatus?.("error", new Error("Connection timeout after 10 seconds"));
4288
+ }
4289
+ }, this.totalTimeoutMs);
4290
+ }
4291
+ clearTotalTimeout() {
4292
+ if (this.totalTimeoutTimer !== null) {
4293
+ clearTimeout(this.totalTimeoutTimer);
4294
+ this.totalTimeoutTimer = null;
4295
+ }
4296
+ this.connectStartTime = null;
4297
+ }
4298
+ open() {
4299
+ if (this.connectStartTime && Date.now() - this.connectStartTime >= this.totalTimeoutMs) {
4300
+ if (!this.closedByUser) {
4301
+ this.closedByUser = true;
4302
+ this.clearTotalTimeout();
4303
+ this.onStatus?.("error", new Error("Connection timeout after 10 seconds"));
4304
+ }
4305
+ return;
4306
+ }
4307
+ this.onStatus?.(this.ws ? "reconnecting" : "connecting");
4308
+ const ws = new WebSocket(this.url);
4309
+ this.ws = ws;
4310
+ ws.onopen = () => {
4311
+ this.clearTotalTimeout();
4312
+ this.onStatus?.("connected");
4313
+ };
4314
+ ws.onerror = (e) => {
4315
+ this.onStatus?.("error", e);
4316
+ };
4317
+ ws.onclose = () => {
4318
+ if (this.closedByUser) {
4319
+ this.onStatus?.("closed");
4320
+ return;
4321
+ }
4322
+ if (this.connectStartTime && Date.now() - this.connectStartTime >= this.totalTimeoutMs) {
4323
+ this.closedByUser = true;
4324
+ this.clearTotalTimeout();
4325
+ this.onStatus?.("error", new Error("Connection timeout after 10 seconds"));
4326
+ return;
4327
+ }
4328
+ this.onStatus?.("reconnecting");
4329
+ setTimeout(() => this.open(), this.reconnectMs);
4330
+ };
4331
+ ws.onmessage = (ev) => {
4332
+ const data = typeof ev.data === "string" ? ev.data : "";
4333
+ const lines = data.split("\n").map((l) => l.trim()).filter(Boolean);
4334
+ for (const line of lines) {
4335
+ try {
4336
+ const obj = JSON.parse(line);
4337
+ this.onMessage(obj);
4338
+ } catch {
4339
+ }
4340
+ }
4341
+ };
4342
+ }
4343
+ };
4344
+
4345
+ // src/api/sources/FileSystemSource.ts
4346
+ var FileSystemSource = class {
4347
+ handle = null;
4348
+ onMessage;
4349
+ onStatus;
4350
+ timer = null;
4351
+ lastSize = 0;
4352
+ filename;
4353
+ intervalMs;
4354
+ constructor(onMessage, onStatus, filename = "graph.ndjson", intervalMs = 1e3) {
4355
+ this.onMessage = onMessage;
4356
+ this.onStatus = onStatus;
4357
+ this.filename = filename;
4358
+ this.intervalMs = intervalMs;
4359
+ }
4360
+ async openDirectory() {
4361
+ try {
4362
+ const dir = await window.showDirectoryPicker?.();
4363
+ if (!dir) throw new Error("File System Access not supported or cancelled");
4364
+ const handle = await dir.getFileHandle(this.filename, { create: false });
4365
+ this.handle = handle;
4366
+ this.onStatus?.("opened", { file: this.filename });
4367
+ this.lastSize = 0;
4368
+ this.startPolling();
4369
+ } catch (e) {
4370
+ this.onStatus?.("error", e);
4371
+ }
4372
+ }
4373
+ close() {
4374
+ if (this.timer) {
4375
+ window.clearInterval(this.timer);
4376
+ this.timer = null;
4377
+ }
4378
+ this.handle = null;
4379
+ this.onStatus?.("closed");
4380
+ }
4381
+ startPolling() {
4382
+ if (this.timer) window.clearInterval(this.timer);
4383
+ this.timer = window.setInterval(() => this.readNewLines(), this.intervalMs);
4384
+ }
4385
+ async readNewLines() {
4386
+ try {
4387
+ if (!this.handle) return;
4388
+ this.onStatus?.("reading");
4389
+ const file = await this.handle.getFile();
4390
+ if (file.size === this.lastSize) return;
4391
+ const slice = await file.slice(this.lastSize).text();
4392
+ this.lastSize = file.size;
4393
+ const lines = slice.split("\n").map((l) => l.trim()).filter(Boolean);
4394
+ for (const line of lines) {
4395
+ try {
4396
+ const obj = JSON.parse(line);
4397
+ this.onMessage(obj);
4398
+ } catch {
4399
+ }
4400
+ }
4401
+ } catch (e) {
4402
+ this.onStatus?.("error", e);
4403
+ }
4404
+ }
4405
+ };
4406
+
4407
+ // src/api/sources/FileSource.ts
4408
+ var FileSource = class {
4409
+ url;
4410
+ onMessage;
4411
+ onStatus;
4412
+ timer = null;
4413
+ lastETag = null;
4414
+ lastContent = "";
4415
+ intervalMs = 1e3;
4416
+ closed = false;
4417
+ constructor(url, onMessage, onStatus, intervalMs = 1e3) {
4418
+ this.url = url;
4419
+ this.onMessage = onMessage;
4420
+ this.onStatus = onStatus;
4421
+ this.intervalMs = intervalMs;
4422
+ }
4423
+ async connect() {
4424
+ this.closed = false;
4425
+ this.lastETag = null;
4426
+ this.lastContent = "";
4427
+ this.onStatus?.("opened");
4428
+ this.startPolling();
4429
+ }
4430
+ close() {
4431
+ this.closed = true;
4432
+ if (this.timer) {
4433
+ window.clearInterval(this.timer);
4434
+ this.timer = null;
4435
+ }
4436
+ this.onStatus?.("closed");
4437
+ }
4438
+ startPolling() {
4439
+ if (this.timer) window.clearInterval(this.timer);
4440
+ this.timer = window.setInterval(() => this.poll(), this.intervalMs);
4441
+ this.poll();
4442
+ }
4443
+ async poll() {
4444
+ if (this.closed) return;
4445
+ try {
4446
+ this.onStatus?.("reading");
4447
+ const headers = {};
4448
+ if (this.lastETag) {
4449
+ headers["If-None-Match"] = this.lastETag;
4450
+ }
4451
+ const response = await fetch(this.url, { headers });
4452
+ if (response.status === 304) {
4453
+ return;
4454
+ }
4455
+ if (!response.ok) {
4456
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
4457
+ }
4458
+ const etag = response.headers.get("ETag");
4459
+ if (etag) {
4460
+ this.lastETag = etag;
4461
+ }
4462
+ const content = await response.text();
4463
+ if (content === this.lastContent) {
4464
+ return;
4465
+ }
4466
+ const lines = content.split("\n").map((l) => l.trim()).filter(Boolean);
4467
+ const lastContentLines = this.lastContent.split("\n").map((l) => l.trim()).filter(Boolean);
4468
+ const newLines = lines.slice(lastContentLines.length);
4469
+ for (const line of newLines) {
4470
+ try {
4471
+ const obj = JSON.parse(line);
4472
+ this.onMessage(obj);
4473
+ } catch {
4474
+ }
4475
+ }
4476
+ this.lastContent = content;
4477
+ } catch (e) {
4478
+ this.onStatus?.("error", e);
4479
+ }
4480
+ }
4481
+ };
4482
+
4483
+ // src/playground/playground.ts
4484
+ var import_styles2 = __toESM(require("./styles.css?raw"), 1);
4485
+ var Playground = class {
4486
+ options;
4487
+ rootElement;
4488
+ currentExample;
4489
+ examples;
4490
+ currentGraph = null;
4491
+ ingest = null;
4492
+ isEditable = false;
4493
+ wsSource = null;
4494
+ fsSource = null;
4495
+ fileSource = null;
4496
+ wsStatus = "disconnected";
4497
+ fsStatus = "disconnected";
4498
+ fileStatus = "disconnected";
4499
+ activeSourceType = null;
4500
+ wsUrl = "ws://localhost:8787";
4501
+ sourceModal = null;
4502
+ helpOverlay = null;
4503
+ exampleList;
4504
+ graphContainerId;
4505
+ constructor(options) {
4506
+ this.options = options;
4507
+ this.examples = { ...options.examples };
4508
+ this.exampleList = Object.keys(this.examples);
4509
+ this.currentExample = options.defaultExample || this.exampleList[0];
4510
+ this.graphContainerId = `playground-graph-${Math.random().toString(36).substr(2, 9)}`;
4511
+ if (typeof options.root === "string") {
4512
+ const el = document.getElementById(options.root);
4513
+ if (!el) throw new Error(`Element with id "${options.root}" not found`);
4514
+ this.rootElement = el;
4515
+ } else {
4516
+ this.rootElement = options.root;
4517
+ }
4518
+ }
4519
+ async init() {
4520
+ this.injectStyles();
4521
+ this.createDOM();
4522
+ this.setupEventListeners();
4523
+ await this.renderGraph();
4524
+ this.updateSourceIcon();
4525
+ this.connectExampleSource();
4526
+ const rebuildBtn = this.rootElement.querySelector("#rebuild");
4527
+ if (rebuildBtn) rebuildBtn.disabled = !this.isEditable;
4528
+ }
4529
+ injectStyles() {
4530
+ if (!document.getElementById("g3p-playground-styles")) {
4531
+ const styleEl = document.createElement("style");
4532
+ styleEl.id = "g3p-playground-styles";
4533
+ styleEl.textContent = import_styles2.default;
4534
+ document.head.appendChild(styleEl);
4535
+ }
4536
+ }
4537
+ createDOM() {
4538
+ const exampleList = this.exampleList.map((key, i) => {
4539
+ const example = this.examples[key];
4540
+ const isActive = i === 0 || key === this.currentExample;
4541
+ return `
4542
+ <button class="example-btn ${isActive ? "active" : ""}" data-example="${key}">
4543
+ ${example.name}
4544
+ </button>
4545
+ `;
4546
+ }).join("");
4547
+ this.rootElement.innerHTML = `
4548
+ <main class="playground">
4549
+ <div class="sidebar">
4550
+ <h2>Examples</h2>
4551
+ <div class="example-list">
4552
+ ${exampleList}
4553
+ </div>
4554
+
4555
+ <h2>Options</h2>
4556
+ <div class="options">
4557
+ <div class="option-group">
4558
+ <label>Orientation</label>
4559
+ <select id="orientation">
4560
+ <option value="TB">Top to Bottom</option>
4561
+ <option value="BT">Bottom to Top</option>
4562
+ <option value="LR">Left to Right</option>
4563
+ <option value="RL">Right to Left</option>
4564
+ </select>
4565
+ </div>
4566
+
4567
+ <div class="option-group">
4568
+ <label>Port Style</label>
4569
+ <select id="portStyle">
4570
+ <option value="outside">Outside</option>
4571
+ <option value="inside">Inside</option>
4572
+ </select>
4573
+ </div>
4574
+
4575
+ <div class="option-group">
4576
+ <label>
4577
+ <input type="checkbox" id="portLabelRotate" />
4578
+ Rotate Port Labels
4579
+ </label>
4580
+ </div>
4581
+
4582
+ <div class="option-group">
4583
+ <label>Theme</label>
4584
+ <select id="colorMode">
4585
+ <option value="system">System</option>
4586
+ <option value="light">Light</option>
4587
+ <option value="dark">Dark</option>
4588
+ </select>
4589
+ </div>
4590
+ </div>
4591
+ </div>
4592
+
4593
+ <div class="graph-area">
4594
+ <div class="graph-toolbar">
4595
+ <div class="nav-controls">
4596
+ <button class="nav-btn" id="nav-first" title="First (Home)">\u23EE</button>
4597
+ <button class="nav-btn" id="nav-prev" title="Previous (\u2190)">\u25C0</button>
4598
+ <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>
4599
+ <button class="nav-btn" id="nav-next" title="Next (\u2192)">\u25B6</button>
4600
+ <button class="nav-btn" id="nav-last" title="Last (End)">\u23ED</button>
4601
+ </div>
4602
+ <div class="connect-controls" style="display:flex; gap:.5rem; align-items:center;">
4603
+ <button class="nav-btn source-icon-btn" id="source-icon" title="Data Source Connection">\u{1F4E1}</button>
4604
+ </div>
4605
+ <button class="nav-btn" id="help-btn" title="How to edit">\u2753</button>
4606
+ <button class="nav-btn" id="edit-toggle" title="Toggle edit mode">\u270E Edit</button>
4607
+ <button class="nav-btn" id="rebuild" title="Rebuild graph from scratch">\u{1F504} Rebuild</button>
4608
+ </div>
4609
+ <div class="graph-container" id="${this.graphContainerId}"></div>
4610
+ </div>
4611
+ </main>
4612
+ `;
4613
+ }
4614
+ setupEventListeners() {
4615
+ this.rootElement.querySelectorAll(".example-btn").forEach((btn) => {
4616
+ btn.addEventListener("click", () => {
4617
+ this.rootElement.querySelectorAll(".example-btn").forEach((b) => b.classList.remove("active"));
4618
+ btn.classList.add("active");
4619
+ this.currentExample = btn.getAttribute("data-example") || this.exampleList[0];
4620
+ this.renderGraph();
4621
+ this.connectExampleSource();
4622
+ });
4623
+ });
4624
+ this.rootElement.querySelectorAll(".options select, .options input").forEach((el) => {
4625
+ el.addEventListener("change", () => {
4626
+ if (el.id === "colorMode" && this.currentGraph) {
4627
+ const mode = el.value;
4628
+ this.currentGraph.setColorMode(mode);
4629
+ } else {
4630
+ this.renderGraph();
4631
+ }
4632
+ });
4633
+ });
4634
+ this.rootElement.querySelector("#nav-first")?.addEventListener("click", () => {
4635
+ this.currentGraph?.nav("first");
4636
+ this.updateHistoryLabel();
4637
+ });
4638
+ this.rootElement.querySelector("#nav-prev")?.addEventListener("click", () => {
4639
+ this.currentGraph?.nav("prev");
4640
+ this.updateHistoryLabel();
4641
+ });
4642
+ this.rootElement.querySelector("#nav-next")?.addEventListener("click", () => {
4643
+ this.currentGraph?.nav("next");
4644
+ this.updateHistoryLabel();
4645
+ });
4646
+ this.rootElement.querySelector("#nav-last")?.addEventListener("click", () => {
4647
+ this.currentGraph?.nav("last");
4648
+ this.updateHistoryLabel();
4649
+ });
4650
+ this.rootElement.querySelector("#rebuild")?.addEventListener("click", () => {
4651
+ this.currentGraph?.rebuild();
4652
+ });
4653
+ this.rootElement.querySelector("#edit-toggle")?.addEventListener("click", () => {
4654
+ this.isEditable = !this.isEditable;
4655
+ const btn = this.rootElement.querySelector("#edit-toggle");
4656
+ if (btn) btn.textContent = this.isEditable ? "\u2713 Done" : "\u270E Edit";
4657
+ const rebuildBtn = this.rootElement.querySelector("#rebuild");
4658
+ if (rebuildBtn) rebuildBtn.disabled = !this.isEditable;
4659
+ try {
4660
+ this.currentGraph?.setEditable?.(this.isEditable);
4661
+ } catch {
4662
+ }
4663
+ });
4664
+ this.rootElement.querySelector("#help-btn")?.addEventListener("click", () => this.openHelp());
4665
+ const sourceIconBtn = this.rootElement.querySelector("#source-icon");
4666
+ if (sourceIconBtn) {
4667
+ sourceIconBtn.addEventListener("click", (e) => {
4668
+ e.preventDefault();
4669
+ e.stopPropagation();
4670
+ this.openSourceModal();
4671
+ });
4672
+ }
4673
+ document.addEventListener("keydown", (e) => {
4674
+ if (!this.currentGraph) return;
4675
+ if (e.target instanceof HTMLInputElement || e.target instanceof HTMLSelectElement) return;
4676
+ switch (e.key) {
4677
+ case "Home":
4678
+ this.currentGraph.nav("first");
4679
+ this.updateHistoryLabel();
4680
+ break;
4681
+ case "End":
4682
+ this.currentGraph.nav("last");
4683
+ this.updateHistoryLabel();
4684
+ break;
4685
+ case "ArrowLeft":
4686
+ this.currentGraph.nav("prev");
4687
+ this.updateHistoryLabel();
4688
+ break;
4689
+ case "ArrowRight":
4690
+ this.currentGraph.nav("next");
4691
+ this.updateHistoryLabel();
4692
+ break;
4693
+ }
4694
+ });
4695
+ }
4696
+ getResolvedColorMode() {
4697
+ const mode = this.rootElement.querySelector("#colorMode")?.value;
4698
+ if (mode === "system") {
4699
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
4700
+ }
4701
+ return mode;
4702
+ }
4703
+ getOptions(exampleOptions) {
4704
+ const orientation = this.rootElement.querySelector("#orientation")?.value;
4705
+ return {
4706
+ graph: { orientation },
4707
+ canvas: {
4708
+ width: "100%",
4709
+ height: "100%",
4710
+ colorMode: this.getResolvedColorMode(),
4711
+ editable: this.isEditable,
4712
+ ...exampleOptions?.canvas
4713
+ }
4714
+ };
4715
+ }
4716
+ async renderGraph() {
4717
+ const container = this.rootElement.querySelector(`#${this.graphContainerId}`);
4718
+ if (!container) return;
4719
+ this.currentGraph?.destroy();
4720
+ this.currentGraph = null;
4721
+ container.innerHTML = "";
4722
+ const example = this.examples[this.currentExample];
4723
+ const options = this.getOptions(example.options);
4724
+ try {
4725
+ this.currentGraph = await graph({
4726
+ root: this.graphContainerId,
4727
+ nodes: example.nodes,
4728
+ edges: example.edges,
4729
+ options,
4730
+ events: {
4731
+ historyChange: () => this.updateHistoryLabel()
4732
+ }
4733
+ });
4734
+ this.ingest = new Ingest(this.currentGraph);
4735
+ this.updateHistoryLabel();
4736
+ } catch (e) {
4737
+ console.error("Failed to render graph:", e);
4738
+ container.innerHTML = '<p style="padding: 2rem; color: #ef4444;">Failed to load graph</p>';
4739
+ }
4740
+ }
4741
+ updateHistoryLabel() {
4742
+ const label = this.rootElement.querySelector("#history-label");
4743
+ if (!label || !this.currentGraph) return;
4744
+ try {
4745
+ const idx = this.currentGraph.getHistoryIndex?.() ?? 0;
4746
+ const len = this.currentGraph.getHistoryLength?.() ?? 1;
4747
+ label.textContent = `${idx + 1} / ${len}`;
4748
+ } catch {
4749
+ label.textContent = "\u2014 / \u2014";
4750
+ }
4751
+ }
4752
+ connectExampleSource() {
4753
+ const example = this.examples[this.currentExample];
4754
+ if (!example.source) {
4755
+ this.disconnectAllSources();
4756
+ return;
4757
+ }
4758
+ this.disconnectAllSources();
4759
+ if (example.source.type === "websocket") {
4760
+ this.wsUrl = example.source.url;
4761
+ this.wsSource = new WebSocketSource(example.source.url, this.handleIngestMessage.bind(this), this.updateWsStatus);
4762
+ this.wsSource.connect();
4763
+ } else if (example.source.type === "file") {
4764
+ this.fileSource = new FileSource(example.source.path, this.handleIngestMessage.bind(this), this.updateFileStatus);
4765
+ this.fileSource.connect();
4766
+ }
4767
+ }
4768
+ disconnectAllSources() {
4769
+ this.wsSource?.disconnect();
4770
+ this.fsSource?.close();
4771
+ this.fileSource?.close();
4772
+ this.wsSource = null;
4773
+ this.fsSource = null;
4774
+ this.fileSource = null;
4775
+ this.activeSourceType = null;
4776
+ this.wsStatus = "disconnected";
4777
+ this.fsStatus = "disconnected";
4778
+ this.fileStatus = "disconnected";
4779
+ this.updateSourceIcon();
4780
+ }
4781
+ openHelp() {
4782
+ if (!this.helpOverlay) {
4783
+ this.helpOverlay = document.createElement("div");
4784
+ this.helpOverlay.className = "modal-overlay";
4785
+ this.helpOverlay.innerHTML = `
4786
+ <div class="modal" role="dialog" aria-modal="true" aria-labelledby="help-title">
4787
+ <div class="modal-header">
4788
+ <h3 id="help-title">Editing the Graph</h3>
4789
+ <button class="modal-close" title="Close" aria-label="Close">\xD7</button>
4790
+ </div>
4791
+ <div class="modal-body">
4792
+ <p>Here's how to edit the graph:</p>
4793
+ <ul>
4794
+ <li><strong>Enable editing</strong>: Click "Edit"</li>
4795
+ <li><strong>Add a node</strong>: Double\u2011click an empty area</li>
4796
+ <li><strong>Edit a node</strong>: Double\u2011click a node</li>
4797
+ <li><strong>Edit an edge</strong>: Double\u2011click an edge</li>
4798
+ <li><strong>Create an edge</strong>: Click and drag from a node (or its port) onto another node; press Esc to cancel</li>
4799
+ <li><strong>Pan</strong>: Drag on canvas or edges; <strong>Zoom</strong>: Mouse wheel or controls</li>
4800
+ <li><strong>Rebuild</strong>: Use "Rebuild" to re-layout from scratch (enabled in edit mode)</li>
4801
+ </ul>
4802
+ <p>When you're done, click "Done" to lock the canvas.</p>
4803
+ </div>
4804
+ </div>
4805
+ `;
4806
+ document.body.appendChild(this.helpOverlay);
4807
+ this.helpOverlay.addEventListener("click", (e) => {
4808
+ const target = e.target;
4809
+ if (target.classList.contains("modal-overlay") || target.classList.contains("modal-close")) {
4810
+ this.closeHelp();
4811
+ }
4812
+ });
4813
+ }
4814
+ this.helpOverlay.style.display = "flex";
4815
+ }
4816
+ closeHelp() {
4817
+ if (this.helpOverlay) this.helpOverlay.style.display = "none";
4818
+ }
4819
+ handleIngestMessage = async (msg) => {
4820
+ if (!this.ingest) return;
4821
+ await this.ingest.apply(msg);
4822
+ };
4823
+ updateSourceIcon() {
4824
+ const iconBtn = this.rootElement.querySelector("#source-icon");
4825
+ if (!iconBtn) return;
4826
+ iconBtn.classList.remove("active", "connecting", "error");
4827
+ const isConnected = this.activeSourceType === "ws" && this.wsStatus === "connected" || this.activeSourceType === "folder" && this.fsStatus === "connected" || this.activeSourceType === "file" && this.fileStatus === "connected";
4828
+ const isConnecting = this.activeSourceType === "ws" && this.wsStatus === "connecting" || this.activeSourceType === "folder" && this.fsStatus === "opening" || this.activeSourceType === "file" && this.fileStatus === "connecting";
4829
+ const hasError = this.activeSourceType === "ws" && this.wsStatus === "error" || this.activeSourceType === "folder" && this.fsStatus === "error" || this.activeSourceType === "file" && this.fileStatus === "error";
4830
+ let icon = "\u{1F4E1}";
4831
+ if (this.activeSourceType === "folder") {
4832
+ icon = "\u{1F4C1}";
4833
+ } else if (this.activeSourceType === "file") {
4834
+ icon = "\u{1F4C4}";
4835
+ }
4836
+ if (isConnected) {
4837
+ iconBtn.classList.add("active");
4838
+ iconBtn.textContent = icon;
4839
+ } else if (isConnecting) {
4840
+ iconBtn.classList.add("connecting");
4841
+ iconBtn.textContent = icon;
4842
+ } else if (hasError) {
4843
+ iconBtn.classList.add("error");
4844
+ iconBtn.textContent = icon;
4845
+ } else {
4846
+ iconBtn.textContent = "\u{1F4E1}";
4847
+ }
4848
+ }
4849
+ updateWsStatus = (status, detail) => {
4850
+ if (status === "connecting" || status === "reconnecting") {
4851
+ this.wsStatus = "connecting";
4852
+ this.activeSourceType = "ws";
4853
+ } else if (status === "connected") {
4854
+ this.wsStatus = "connected";
4855
+ this.activeSourceType = "ws";
4856
+ if (this.fsSource && this.fsStatus === "connected") {
4857
+ this.fsSource.close();
4858
+ }
4859
+ if (this.fileSource && this.fileStatus === "connected") {
4860
+ this.fileSource.close();
4861
+ }
4862
+ } else if (status === "error") {
4863
+ this.wsStatus = "error";
4864
+ } else {
4865
+ this.wsStatus = "disconnected";
4866
+ if (this.activeSourceType === "ws") {
4867
+ this.activeSourceType = null;
4868
+ }
4869
+ }
4870
+ this.updateSourceIcon();
4871
+ this.updateSourceModal();
4872
+ };
4873
+ updateFsStatus = (status, detail) => {
4874
+ if (status === "opened") {
4875
+ this.fsStatus = "opening";
4876
+ this.activeSourceType = "folder";
4877
+ if (this.wsSource && this.wsStatus === "connected") {
4878
+ this.wsSource.disconnect();
4879
+ }
4880
+ } else if (status === "reading") {
4881
+ this.fsStatus = "connected";
4882
+ this.activeSourceType = "folder";
4883
+ } else if (status === "error") {
4884
+ this.fsStatus = "error";
4885
+ } else if (status === "closed") {
4886
+ this.fsStatus = "disconnected";
4887
+ if (this.activeSourceType === "folder") {
4888
+ this.activeSourceType = null;
4889
+ }
4890
+ } else {
4891
+ this.fsStatus = "disconnected";
4892
+ }
4893
+ this.updateSourceIcon();
4894
+ this.updateSourceModal();
4895
+ };
4896
+ updateFileStatus = (status, detail) => {
4897
+ if (status === "opened") {
4898
+ this.fileStatus = "connecting";
4899
+ this.activeSourceType = "file";
4900
+ if (this.wsSource && this.wsStatus === "connected") {
4901
+ this.wsSource.disconnect();
4902
+ }
4903
+ if (this.fsSource && this.fsStatus === "connected") {
4904
+ this.fsSource.close();
4905
+ }
4906
+ } else if (status === "reading") {
4907
+ this.fileStatus = "connected";
4908
+ this.activeSourceType = "file";
4909
+ } else if (status === "error") {
4910
+ this.fileStatus = "error";
4911
+ } else if (status === "closed") {
4912
+ this.fileStatus = "disconnected";
4913
+ if (this.activeSourceType === "file") {
4914
+ this.activeSourceType = null;
4915
+ }
4916
+ } else {
4917
+ this.fileStatus = "disconnected";
4918
+ }
4919
+ this.updateSourceIcon();
4920
+ this.updateSourceModal();
4921
+ };
4922
+ createSourceModal() {
4923
+ if (this.sourceModal) return this.sourceModal;
4924
+ this.sourceModal = document.createElement("div");
4925
+ this.sourceModal.className = "modal-overlay";
4926
+ this.sourceModal.style.display = "none";
4927
+ this.sourceModal.innerHTML = `
4928
+ <div class="modal" role="dialog" aria-modal="true" aria-labelledby="source-modal-title">
4929
+ <div class="modal-header">
4930
+ <h3 id="source-modal-title">Data Source Connection</h3>
4931
+ <button class="modal-close" title="Close" aria-label="Close">\xD7</button>
4932
+ </div>
4933
+ <div class="modal-body">
4934
+ <div class="source-type-selector">
4935
+ <button class="source-type-option" data-source="ws">\u{1F4E1} WebSocket</button>
4936
+ <button class="source-type-option" data-source="folder">\u{1F4C1} Folder</button>
4937
+ </div>
4938
+
4939
+ <div class="source-controls" data-source="ws">
4940
+ <div class="form-group">
4941
+ <label for="source-modal-url">WebSocket URL</label>
4942
+ <input type="text" id="source-modal-url" value="${this.wsUrl}" />
4943
+ </div>
4944
+ <div class="button-group">
4945
+ <button id="source-modal-connect-ws" class="primary">Connect</button>
4946
+ <button id="source-modal-disconnect-ws">Disconnect</button>
4947
+ <button id="source-modal-change-ws">Change Connection</button>
4948
+ </div>
4949
+ </div>
4950
+
4951
+ <div class="source-controls" data-source="folder">
4952
+ <div class="form-group">
4953
+ <label>File System Source</label>
4954
+ <p style="font-size: 0.875rem; color: var(--color-text-muted); margin-top: 0.25rem;">
4955
+ Select a directory containing a graph.ndjson file to watch for changes.
4956
+ </p>
4957
+ </div>
4958
+ <div class="button-group">
4959
+ <button id="source-modal-connect-folder" class="primary">Open Folder</button>
4960
+ <button id="source-modal-disconnect-folder">Disconnect</button>
4961
+ </div>
4962
+ </div>
4963
+
4964
+ <div id="source-modal-status"></div>
4965
+ </div>
4966
+ </div>
4967
+ `;
4968
+ document.body.appendChild(this.sourceModal);
4969
+ this.sourceModal.addEventListener("click", (e) => {
4970
+ const target = e.target;
4971
+ if (target.classList.contains("modal-overlay") || target.classList.contains("modal-close")) {
4972
+ this.closeSourceModal();
4973
+ }
4974
+ });
4975
+ this.sourceModal.querySelectorAll(".source-type-option").forEach((btn) => {
4976
+ btn.addEventListener("click", () => {
4977
+ const sourceType = btn.getAttribute("data-source");
4978
+ if (sourceType) {
4979
+ this.selectSourceType(sourceType);
4980
+ }
4981
+ });
4982
+ });
4983
+ document.getElementById("source-modal-connect-ws")?.addEventListener("click", () => this.handleConnect());
4984
+ document.getElementById("source-modal-disconnect-ws")?.addEventListener("click", () => this.handleDisconnect());
4985
+ document.getElementById("source-modal-change-ws")?.addEventListener("click", () => this.handleChangeConnection());
4986
+ document.getElementById("source-modal-connect-folder")?.addEventListener("click", () => this.handleOpenFolder());
4987
+ document.getElementById("source-modal-disconnect-folder")?.addEventListener("click", () => this.handleCloseFolder());
4988
+ document.getElementById("source-modal-url")?.addEventListener("keydown", (e) => {
4989
+ if (e.key === "Enter" && this.wsStatus !== "connected") {
4990
+ this.handleConnect();
4991
+ }
4992
+ });
4993
+ return this.sourceModal;
4994
+ }
4995
+ selectSourceType(type, skipUpdate = false) {
4996
+ if (!this.sourceModal) return;
4997
+ this.sourceModal.querySelectorAll(".source-type-option").forEach((btn) => {
4998
+ if (btn.getAttribute("data-source") === type) {
4999
+ btn.classList.add("active");
5000
+ } else {
5001
+ btn.classList.remove("active");
5002
+ }
5003
+ });
5004
+ this.sourceModal.querySelectorAll(".source-controls").forEach((controls) => {
5005
+ if (controls.getAttribute("data-source") === type) {
5006
+ controls.classList.add("active");
5007
+ } else {
5008
+ controls.classList.remove("active");
5009
+ }
5010
+ });
5011
+ if (!skipUpdate) {
5012
+ this.updateSourceModalContent();
5013
+ }
5014
+ }
5015
+ updateSourceModalContent() {
5016
+ if (!this.sourceModal) return;
5017
+ const urlInput = document.getElementById("source-modal-url");
5018
+ const connectWsBtn = document.getElementById("source-modal-connect-ws");
5019
+ const disconnectWsBtn = document.getElementById("source-modal-disconnect-ws");
5020
+ const changeWsBtn = document.getElementById("source-modal-change-ws");
5021
+ if (urlInput && connectWsBtn && disconnectWsBtn && changeWsBtn) {
5022
+ const isWsConnected = this.wsStatus === "connected";
5023
+ const isWsConnecting = this.wsStatus === "connecting";
5024
+ connectWsBtn.disabled = isWsConnected || isWsConnecting;
5025
+ disconnectWsBtn.disabled = !isWsConnected || isWsConnecting;
5026
+ changeWsBtn.disabled = !isWsConnected || isWsConnecting;
5027
+ urlInput.disabled = isWsConnecting;
5028
+ }
5029
+ const connectFolderBtn = document.getElementById("source-modal-connect-folder");
5030
+ const disconnectFolderBtn = document.getElementById("source-modal-disconnect-folder");
5031
+ if (connectFolderBtn && disconnectFolderBtn) {
5032
+ const isFolderConnected = this.fsStatus === "connected";
5033
+ const isFolderOpening = this.fsStatus === "opening";
5034
+ connectFolderBtn.disabled = isFolderConnected || isFolderOpening;
5035
+ disconnectFolderBtn.disabled = !isFolderConnected || isFolderOpening;
5036
+ }
5037
+ const statusDiv = document.getElementById("source-modal-status");
5038
+ if (!statusDiv) return;
5039
+ const currentUrl = urlInput?.value || this.wsUrl;
5040
+ statusDiv.innerHTML = "";
5041
+ if (this.activeSourceType === "ws") {
5042
+ if (this.wsStatus === "connecting") {
5043
+ statusDiv.innerHTML = `
5044
+ <div class="status-message info">
5045
+ <span class="loading-spinner"></span>
5046
+ Connecting to ${currentUrl}...
5047
+ </div>
5048
+ `;
5049
+ } else if (this.wsStatus === "connected") {
5050
+ statusDiv.innerHTML = `
5051
+ <div class="status-message success">
5052
+ \u2713 Connected to ${currentUrl}
5053
+ </div>
5054
+ `;
5055
+ } else if (this.wsStatus === "error") {
5056
+ statusDiv.innerHTML = `
5057
+ <div class="status-message error">
5058
+ \u2717 Connection error. Please check the URL and try again.
5059
+ </div>
5060
+ `;
5061
+ } else {
5062
+ statusDiv.innerHTML = `
5063
+ <div class="status-message info">
5064
+ Not connected
5065
+ </div>
5066
+ `;
5067
+ }
5068
+ } else if (this.activeSourceType === "folder") {
5069
+ if (this.fsStatus === "opening") {
5070
+ statusDiv.innerHTML = `
5071
+ <div class="status-message info">
5072
+ <span class="loading-spinner"></span>
5073
+ Opening folder...
5074
+ </div>
5075
+ `;
5076
+ } else if (this.fsStatus === "connected") {
5077
+ statusDiv.innerHTML = `
5078
+ <div class="status-message success">
5079
+ \u2713 Folder connected and watching for changes
5080
+ </div>
5081
+ `;
5082
+ } else if (this.fsStatus === "error") {
5083
+ statusDiv.innerHTML = `
5084
+ <div class="status-message error">
5085
+ \u2717 Error opening folder. Please try again.
5086
+ </div>
5087
+ `;
5088
+ } else {
5089
+ statusDiv.innerHTML = `
5090
+ <div class="status-message info">
5091
+ Not connected
5092
+ </div>
5093
+ `;
5094
+ }
5095
+ } else if (this.activeSourceType === "file") {
5096
+ const example = this.examples[this.currentExample];
5097
+ const filePath = example.source?.type === "file" ? example.source.path : "";
5098
+ if (this.fileStatus === "connecting") {
5099
+ statusDiv.innerHTML = `
5100
+ <div class="status-message info">
5101
+ <span class="loading-spinner"></span>
5102
+ Connecting to ${filePath}...
5103
+ </div>
5104
+ `;
5105
+ } else if (this.fileStatus === "connected") {
5106
+ statusDiv.innerHTML = `
5107
+ <div class="status-message success">
5108
+ \u2713 Connected to ${filePath}
5109
+ </div>
5110
+ `;
5111
+ } else if (this.fileStatus === "error") {
5112
+ statusDiv.innerHTML = `
5113
+ <div class="status-message error">
5114
+ \u2717 Error loading file. Please check the path and try again.
5115
+ </div>
5116
+ `;
5117
+ } else {
5118
+ statusDiv.innerHTML = `
5119
+ <div class="status-message info">
5120
+ Not connected
5121
+ </div>
5122
+ `;
5123
+ }
5124
+ } else {
5125
+ statusDiv.innerHTML = `
5126
+ <div class="status-message info">
5127
+ Select a source type to connect
5128
+ </div>
5129
+ `;
5130
+ }
5131
+ }
5132
+ updateSourceModal() {
5133
+ if (!this.sourceModal) return;
5134
+ const activeType = (this.activeSourceType === "file" ? "ws" : this.activeSourceType) || "ws";
5135
+ this.selectSourceType(activeType, true);
5136
+ this.updateSourceModalContent();
5137
+ }
5138
+ openSourceModal() {
5139
+ this.createSourceModal();
5140
+ if (this.sourceModal) {
5141
+ const urlInput = document.getElementById("source-modal-url");
5142
+ if (urlInput) {
5143
+ urlInput.value = this.wsUrl;
5144
+ }
5145
+ const activeType = (this.activeSourceType === "file" ? "ws" : this.activeSourceType) || "ws";
5146
+ this.selectSourceType(activeType);
5147
+ this.updateSourceModal();
5148
+ this.sourceModal.style.display = "flex";
5149
+ }
5150
+ }
5151
+ closeSourceModal() {
5152
+ if (this.sourceModal) {
5153
+ this.sourceModal.style.display = "none";
5154
+ }
5155
+ }
5156
+ handleConnect() {
5157
+ const urlInput = document.getElementById("source-modal-url");
5158
+ if (!urlInput) return;
5159
+ const url = urlInput.value.trim() || "ws://localhost:8787";
5160
+ this.wsUrl = url;
5161
+ if (this.wsSource) {
5162
+ this.wsSource.disconnect();
5163
+ }
5164
+ this.wsSource = new WebSocketSource(url, this.handleIngestMessage.bind(this), this.updateWsStatus);
5165
+ this.wsSource.connect();
5166
+ this.updateSourceModal();
5167
+ }
5168
+ handleDisconnect() {
5169
+ this.wsSource?.disconnect();
5170
+ this.updateSourceModal();
5171
+ }
5172
+ handleChangeConnection() {
5173
+ if (this.wsSource) {
5174
+ this.wsSource.disconnect();
5175
+ }
5176
+ const urlInput = document.getElementById("source-modal-url");
5177
+ if (urlInput) {
5178
+ urlInput.focus();
5179
+ urlInput.select();
5180
+ }
5181
+ this.updateSourceModal();
5182
+ }
5183
+ async handleOpenFolder() {
5184
+ if (!this.fsSource) {
5185
+ this.fsSource = new FileSystemSource(this.handleIngestMessage.bind(this), this.updateFsStatus);
5186
+ }
5187
+ this.updateSourceModal();
5188
+ await this.fsSource.openDirectory();
5189
+ }
5190
+ handleCloseFolder() {
5191
+ this.fsSource?.close();
5192
+ this.updateSourceModal();
5193
+ }
5194
+ /**
5195
+ * Add or update an example
5196
+ */
5197
+ addExample(key, example) {
5198
+ this.examples[key] = example;
5199
+ this.updateExampleList();
5200
+ if (this.currentExample === key) {
5201
+ this.renderGraph();
5202
+ this.connectExampleSource();
5203
+ }
5204
+ }
5205
+ /**
5206
+ * Remove an example
5207
+ */
5208
+ removeExample(key) {
5209
+ delete this.examples[key];
5210
+ if (this.currentExample === key) {
5211
+ this.exampleList = Object.keys(this.examples);
5212
+ this.currentExample = this.exampleList[0] || "";
5213
+ if (this.currentExample) {
5214
+ this.renderGraph();
5215
+ this.connectExampleSource();
5216
+ }
5217
+ }
5218
+ this.updateExampleList();
5219
+ }
5220
+ /**
5221
+ * Update the example list in the DOM
5222
+ */
5223
+ updateExampleList() {
5224
+ this.exampleList = Object.keys(this.examples);
5225
+ const exampleListEl = this.rootElement.querySelector(".example-list");
5226
+ if (!exampleListEl) return;
5227
+ exampleListEl.innerHTML = this.exampleList.map((key, i) => {
5228
+ const example = this.examples[key];
5229
+ const isActive = key === this.currentExample;
5230
+ return `
5231
+ <button class="example-btn ${isActive ? "active" : ""}" data-example="${key}">
5232
+ ${example.name}
5233
+ </button>
5234
+ `;
5235
+ }).join("");
5236
+ exampleListEl.querySelectorAll(".example-btn").forEach((btn) => {
5237
+ btn.addEventListener("click", () => {
5238
+ this.rootElement.querySelectorAll(".example-btn").forEach((b) => b.classList.remove("active"));
5239
+ btn.classList.add("active");
5240
+ this.currentExample = btn.getAttribute("data-example") || this.exampleList[0];
5241
+ this.renderGraph();
5242
+ this.connectExampleSource();
5243
+ });
5244
+ });
5245
+ }
4078
5246
  };
4079
5247
 
4080
5248
  // src/index.ts
@@ -4086,5 +5254,11 @@ async function graph(args = { root: "app" }) {
4086
5254
  var index_default = graph;
4087
5255
  // Annotate the CommonJS export names for ESM import in node:
4088
5256
  0 && (module.exports = {
5257
+ FileSource,
5258
+ FileSystemSource,
5259
+ Ingest,
5260
+ Playground,
5261
+ Updater,
5262
+ WebSocketSource,
4089
5263
  graph
4090
5264
  });