@3plate/graph-core 0.1.6 → 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
@@ -34,6 +34,7 @@ __export(index_exports, {
34
34
  FileSystemSource: () => FileSystemSource,
35
35
  Ingest: () => Ingest,
36
36
  Playground: () => Playground,
37
+ Updater: () => Updater,
37
38
  WebSocketSource: () => WebSocketSource,
38
39
  default: () => index_default,
39
40
  graph: () => graph
@@ -1380,15 +1381,16 @@ var Layout = class _Layout {
1380
1381
 
1381
1382
  // src/canvas/marker.tsx
1382
1383
  var import_jsx_runtime = require("jsx-dom/jsx-runtime");
1383
- function arrow(size, reverse = false) {
1384
+ function arrow(size, reverse = false, prefix = "") {
1384
1385
  const h = size / 1.5;
1385
1386
  const w = size;
1386
1387
  const ry = h / 2;
1387
1388
  const suffix = reverse ? "-reverse" : "";
1389
+ const id = prefix ? `${prefix}-g3p-marker-arrow${suffix}` : `g3p-marker-arrow${suffix}`;
1388
1390
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1389
1391
  "marker",
1390
1392
  {
1391
- id: `g3p-marker-arrow${suffix}`,
1393
+ id,
1392
1394
  className: "g3p-marker g3p-marker-arrow",
1393
1395
  markerWidth: size,
1394
1396
  markerHeight: size,
@@ -1400,14 +1402,15 @@ function arrow(size, reverse = false) {
1400
1402
  }
1401
1403
  );
1402
1404
  }
1403
- function circle(size, reverse = false) {
1405
+ function circle(size, reverse = false, prefix = "") {
1404
1406
  const r = size / 3;
1405
1407
  const cy = size / 2;
1406
1408
  const suffix = reverse ? "-reverse" : "";
1409
+ const id = prefix ? `${prefix}-g3p-marker-circle${suffix}` : `g3p-marker-circle${suffix}`;
1407
1410
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1408
1411
  "marker",
1409
1412
  {
1410
- id: `g3p-marker-circle${suffix}`,
1413
+ id,
1411
1414
  className: "g3p-marker g3p-marker-circle",
1412
1415
  markerWidth: size,
1413
1416
  markerHeight: size,
@@ -1419,15 +1422,16 @@ function circle(size, reverse = false) {
1419
1422
  }
1420
1423
  );
1421
1424
  }
1422
- function diamond(size, reverse = false) {
1425
+ function diamond(size, reverse = false, prefix = "") {
1423
1426
  const w = size * 0.7;
1424
1427
  const h = size / 2;
1425
1428
  const cy = size / 2;
1426
1429
  const suffix = reverse ? "-reverse" : "";
1430
+ const id = prefix ? `${prefix}-g3p-marker-diamond${suffix}` : `g3p-marker-diamond${suffix}`;
1427
1431
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1428
1432
  "marker",
1429
1433
  {
1430
- id: `g3p-marker-diamond${suffix}`,
1434
+ id,
1431
1435
  className: "g3p-marker g3p-marker-diamond",
1432
1436
  markerWidth: size,
1433
1437
  markerHeight: size,
@@ -1439,14 +1443,15 @@ function diamond(size, reverse = false) {
1439
1443
  }
1440
1444
  );
1441
1445
  }
1442
- function bar(size, reverse = false) {
1446
+ function bar(size, reverse = false, prefix = "") {
1443
1447
  const h = size * 0.6;
1444
1448
  const cy = size / 2;
1445
1449
  const suffix = reverse ? "-reverse" : "";
1450
+ const id = prefix ? `${prefix}-g3p-marker-bar${suffix}` : `g3p-marker-bar${suffix}`;
1446
1451
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1447
1452
  "marker",
1448
1453
  {
1449
- id: `g3p-marker-bar${suffix}`,
1454
+ id,
1450
1455
  className: "g3p-marker g3p-marker-bar",
1451
1456
  markerWidth: size,
1452
1457
  markerHeight: size,
@@ -1458,7 +1463,7 @@ function bar(size, reverse = false) {
1458
1463
  }
1459
1464
  );
1460
1465
  }
1461
- function none(size, reverse = false) {
1466
+ function none(size, reverse = false, prefix = "") {
1462
1467
  return void 0;
1463
1468
  }
1464
1469
  function normalize(data) {
@@ -2240,6 +2245,9 @@ var Seg2 = class {
2240
2245
  if (this.source.isDummy) source = void 0;
2241
2246
  if (this.target.isDummy) target = void 0;
2242
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;
2243
2251
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2244
2252
  "g",
2245
2253
  {
@@ -2254,8 +2262,8 @@ var Seg2 = class {
2254
2262
  d: this.svg,
2255
2263
  fill: "none",
2256
2264
  className: "g3p-seg-line",
2257
- markerStart: source ? `url(#g3p-marker-${source}-reverse)` : void 0,
2258
- markerEnd: target ? `url(#g3p-marker-${target})` : void 0
2265
+ markerStart: markerStartId ? `url(#${markerStartId})` : void 0,
2266
+ markerEnd: markerEndId ? `url(#${markerEndId})` : void 0
2259
2267
  }
2260
2268
  ),
2261
2269
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
@@ -2840,6 +2848,10 @@ var Canvas = class {
2840
2848
  curNodes;
2841
2849
  curSegs;
2842
2850
  updating;
2851
+ // Unique marker ID prefix for this canvas instance
2852
+ markerPrefix;
2853
+ // Dynamic style element for this instance (for cleanup)
2854
+ dynamicStyleEl;
2843
2855
  // Pan-zoom state
2844
2856
  panScale = null;
2845
2857
  zoomControls;
@@ -2853,6 +2865,13 @@ var Canvas = class {
2853
2865
  constructor(api, options) {
2854
2866
  Object.assign(this, options);
2855
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() {
2856
2875
  this.allNodes = /* @__PURE__ */ new Map();
2857
2876
  this.curNodes = /* @__PURE__ */ new Map();
2858
2877
  this.curSegs = /* @__PURE__ */ new Map();
@@ -2861,9 +2880,7 @@ var Canvas = class {
2861
2880
  this.transform = { x: 0, y: 0, scale: 1 };
2862
2881
  this.editMode = new EditMode();
2863
2882
  this.editMode.editable = this.editable;
2864
- this.createMeasurementContainer();
2865
- this.createCanvasContainer();
2866
- if (this.panZoom) this.setupPanZoom();
2883
+ if (this.group) this.group.innerHTML = "";
2867
2884
  }
2868
2885
  createMeasurementContainer() {
2869
2886
  this.measurement = document.createElement("div");
@@ -3048,12 +3065,13 @@ var Canvas = class {
3048
3065
  }
3049
3066
  generateDynamicStyles() {
3050
3067
  let css = "";
3051
- css += themeToCSS(this.theme, `.g3p-canvas-container`);
3068
+ const scope = `[data-g3p-instance="${this.markerPrefix}"]`;
3069
+ css += themeToCSS(this.theme, scope);
3052
3070
  for (const [type, vars] of Object.entries(this.nodeTypes)) {
3053
- css += themeToCSS(vars, `.g3p-node-type-${type}`, "node");
3071
+ css += themeToCSS(vars, `${scope} .g3p-node-type-${type}`, "node");
3054
3072
  }
3055
3073
  for (const [type, vars] of Object.entries(this.edgeTypes)) {
3056
- css += themeToCSS(vars, `.g3p-edge-type-${type}`);
3074
+ css += themeToCSS(vars, `${scope} .g3p-edge-type-${type}`);
3057
3075
  }
3058
3076
  return css;
3059
3077
  }
@@ -3066,15 +3084,18 @@ var Canvas = class {
3066
3084
  }
3067
3085
  const dynamicStyles = this.generateDynamicStyles();
3068
3086
  if (dynamicStyles) {
3069
- const dynamicStyleEl = document.createElement("style");
3070
- dynamicStyleEl.textContent = dynamicStyles;
3071
- 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);
3072
3092
  }
3073
3093
  const colorModeClass = this.colorMode !== "system" ? `g3p-${this.colorMode}` : "";
3074
3094
  this.container = /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3075
3095
  "div",
3076
3096
  {
3077
3097
  className: `g3p-canvas-container ${colorModeClass}`.trim(),
3098
+ "data-g3p-instance": this.markerPrefix,
3078
3099
  ref: (el) => this.container = el,
3079
3100
  onContextMenu: this.onContextMenu.bind(this),
3080
3101
  children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
@@ -3090,8 +3111,8 @@ var Canvas = class {
3090
3111
  onDblClick: this.onDoubleClick.bind(this),
3091
3112
  children: [
3092
3113
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("defs", { children: [
3093
- Object.values(markerDefs).map((marker) => marker(this.markerSize, false)),
3094
- Object.values(markerDefs).map((marker) => marker(this.markerSize, true))
3114
+ Object.values(markerDefs).map((marker) => marker(this.markerSize, false, this.markerPrefix)),
3115
+ Object.values(markerDefs).map((marker) => marker(this.markerSize, true, this.markerPrefix))
3095
3116
  ] }),
3096
3117
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3097
3118
  "g",
@@ -3448,6 +3469,40 @@ var Canvas = class {
3448
3469
  }
3449
3470
  return { type: "canvas" };
3450
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
+ }
3451
3506
  };
3452
3507
  var themeVarMap = {
3453
3508
  // Canvas
@@ -3664,6 +3719,7 @@ var API = class {
3664
3719
  this.nodeFields = /* @__PURE__ */ new Map();
3665
3720
  this.nextNodeId = 1;
3666
3721
  this.nextEdgeId = 1;
3722
+ this.canvas?.reset?.();
3667
3723
  }
3668
3724
  /** Initialize the API */
3669
3725
  async init() {
@@ -3671,6 +3727,9 @@ var API = class {
3671
3727
  if (!root) throw new Error("root element not found");
3672
3728
  root.appendChild(this.canvas.container);
3673
3729
  await this.applyHistory();
3730
+ if (this.events.onInit) {
3731
+ this.events.onInit();
3732
+ }
3674
3733
  }
3675
3734
  async applyHistory() {
3676
3735
  for (const update of this.history)
@@ -4126,6 +4185,18 @@ var API = class {
4126
4185
  else
4127
4186
  await this.deleteEdge(edge.data);
4128
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
+ }
4129
4200
  };
4130
4201
 
4131
4202
  // src/api/ingest.ts
@@ -4415,6 +4486,7 @@ var Playground = class {
4415
4486
  options;
4416
4487
  rootElement;
4417
4488
  currentExample;
4489
+ examples;
4418
4490
  currentGraph = null;
4419
4491
  ingest = null;
4420
4492
  isEditable = false;
@@ -4432,7 +4504,8 @@ var Playground = class {
4432
4504
  graphContainerId;
4433
4505
  constructor(options) {
4434
4506
  this.options = options;
4435
- this.exampleList = Object.keys(options.examples);
4507
+ this.examples = { ...options.examples };
4508
+ this.exampleList = Object.keys(this.examples);
4436
4509
  this.currentExample = options.defaultExample || this.exampleList[0];
4437
4510
  this.graphContainerId = `playground-graph-${Math.random().toString(36).substr(2, 9)}`;
4438
4511
  if (typeof options.root === "string") {
@@ -4463,7 +4536,7 @@ var Playground = class {
4463
4536
  }
4464
4537
  createDOM() {
4465
4538
  const exampleList = this.exampleList.map((key, i) => {
4466
- const example = this.options.examples[key];
4539
+ const example = this.examples[key];
4467
4540
  const isActive = i === 0 || key === this.currentExample;
4468
4541
  return `
4469
4542
  <button class="example-btn ${isActive ? "active" : ""}" data-example="${key}">
@@ -4549,7 +4622,14 @@ var Playground = class {
4549
4622
  });
4550
4623
  });
4551
4624
  this.rootElement.querySelectorAll(".options select, .options input").forEach((el) => {
4552
- el.addEventListener("change", () => this.renderGraph());
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
+ });
4553
4633
  });
4554
4634
  this.rootElement.querySelector("#nav-first")?.addEventListener("click", () => {
4555
4635
  this.currentGraph?.nav("first");
@@ -4636,8 +4716,10 @@ var Playground = class {
4636
4716
  async renderGraph() {
4637
4717
  const container = this.rootElement.querySelector(`#${this.graphContainerId}`);
4638
4718
  if (!container) return;
4719
+ this.currentGraph?.destroy();
4720
+ this.currentGraph = null;
4639
4721
  container.innerHTML = "";
4640
- const example = this.options.examples[this.currentExample];
4722
+ const example = this.examples[this.currentExample];
4641
4723
  const options = this.getOptions(example.options);
4642
4724
  try {
4643
4725
  this.currentGraph = await graph({
@@ -4668,7 +4750,7 @@ var Playground = class {
4668
4750
  }
4669
4751
  }
4670
4752
  connectExampleSource() {
4671
- const example = this.options.examples[this.currentExample];
4753
+ const example = this.examples[this.currentExample];
4672
4754
  if (!example.source) {
4673
4755
  this.disconnectAllSources();
4674
4756
  return;
@@ -5011,7 +5093,7 @@ var Playground = class {
5011
5093
  `;
5012
5094
  }
5013
5095
  } else if (this.activeSourceType === "file") {
5014
- const example = this.options.examples[this.currentExample];
5096
+ const example = this.examples[this.currentExample];
5015
5097
  const filePath = example.source?.type === "file" ? example.source.path : "";
5016
5098
  if (this.fileStatus === "connecting") {
5017
5099
  statusDiv.innerHTML = `
@@ -5109,6 +5191,58 @@ var Playground = class {
5109
5191
  this.fsSource?.close();
5110
5192
  this.updateSourceModal();
5111
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
+ }
5112
5246
  };
5113
5247
 
5114
5248
  // src/index.ts
@@ -5124,6 +5258,7 @@ var index_default = graph;
5124
5258
  FileSystemSource,
5125
5259
  Ingest,
5126
5260
  Playground,
5261
+ Updater,
5127
5262
  WebSocketSource,
5128
5263
  graph
5129
5264
  });
package/dist/index.d.cts CHANGED
@@ -75,6 +75,8 @@ type GraphOptions = {
75
75
  * Events that can be handled by the user.
76
76
  */
77
77
  type EventsOptions<N, E> = {
78
+ /** Called when the API has finished initializing */
79
+ onInit?: () => void;
78
80
  /** Called when a node is clicked */
79
81
  nodeClick?: (node: N) => void;
80
82
  /** Called when an edge is clicked */
@@ -368,7 +370,7 @@ declare class API<N, E> {
368
370
  private nextNodeId;
369
371
  private nextEdgeId;
370
372
  private events;
371
- private root;
373
+ root: string;
372
374
  constructor(args: APIArguments<N, E>);
373
375
  reset(): void;
374
376
  /** Initialize the API */
@@ -436,6 +438,16 @@ declare class API<N, E> {
436
438
  handleAddEdge(data: EditEdgeProps): Promise<void>;
437
439
  handleDeleteNode(id: NodeId): Promise<void>;
438
440
  handleDeleteEdge(id: EdgeId): Promise<void>;
441
+ /** Update theme and type styles dynamically */
442
+ updateStyles(options: {
443
+ theme?: ThemeVars;
444
+ nodeTypes?: Record<string, ThemeVars>;
445
+ edgeTypes?: Record<string, ThemeVars>;
446
+ }): void;
447
+ /** Update color mode without recreating the canvas */
448
+ setColorMode(colorMode: 'light' | 'dark' | 'system'): void;
449
+ /** Cleanup resources when the graph is destroyed */
450
+ destroy(): void;
439
451
  }
440
452
 
441
453
  type SnapshotMessage<N, E> = {
@@ -575,6 +587,7 @@ declare class Playground {
575
587
  private options;
576
588
  private rootElement;
577
589
  private currentExample;
590
+ private examples;
578
591
  private currentGraph;
579
592
  private ingest;
580
593
  private isEditable;
@@ -619,8 +632,20 @@ declare class Playground {
619
632
  private handleChangeConnection;
620
633
  private handleOpenFolder;
621
634
  private handleCloseFolder;
635
+ /**
636
+ * Add or update an example
637
+ */
638
+ addExample(key: string, example: Example): void;
639
+ /**
640
+ * Remove an example
641
+ */
642
+ removeExample(key: string): void;
643
+ /**
644
+ * Update the example list in the DOM
645
+ */
646
+ private updateExampleList;
622
647
  }
623
648
 
624
649
  declare function graph<N, E>(args?: APIArguments<N, E>): Promise<API<N, E>>;
625
650
 
626
- export { type Example, type ExampleEdge, type ExampleNode, type ExampleOptions, FileSource, FileSystemSource, type HistoryMessage, Ingest, type IngestMessage, Playground, type PlaygroundOptions, type SnapshotMessage, type UpdateMessage, WebSocketSource, graph as default, graph };
651
+ export { API, type APIArguments, type APIOptions, type EventsOptions, type Example, type ExampleEdge, type ExampleNode, type ExampleOptions, FileSource, FileSystemSource, type HistoryMessage, Ingest, type IngestMessage, Playground, type PlaygroundOptions, type SnapshotMessage, type Update, type UpdateMessage, Updater, WebSocketSource, graph as default, graph };
package/dist/index.d.ts CHANGED
@@ -75,6 +75,8 @@ type GraphOptions = {
75
75
  * Events that can be handled by the user.
76
76
  */
77
77
  type EventsOptions<N, E> = {
78
+ /** Called when the API has finished initializing */
79
+ onInit?: () => void;
78
80
  /** Called when a node is clicked */
79
81
  nodeClick?: (node: N) => void;
80
82
  /** Called when an edge is clicked */
@@ -368,7 +370,7 @@ declare class API<N, E> {
368
370
  private nextNodeId;
369
371
  private nextEdgeId;
370
372
  private events;
371
- private root;
373
+ root: string;
372
374
  constructor(args: APIArguments<N, E>);
373
375
  reset(): void;
374
376
  /** Initialize the API */
@@ -436,6 +438,16 @@ declare class API<N, E> {
436
438
  handleAddEdge(data: EditEdgeProps): Promise<void>;
437
439
  handleDeleteNode(id: NodeId): Promise<void>;
438
440
  handleDeleteEdge(id: EdgeId): Promise<void>;
441
+ /** Update theme and type styles dynamically */
442
+ updateStyles(options: {
443
+ theme?: ThemeVars;
444
+ nodeTypes?: Record<string, ThemeVars>;
445
+ edgeTypes?: Record<string, ThemeVars>;
446
+ }): void;
447
+ /** Update color mode without recreating the canvas */
448
+ setColorMode(colorMode: 'light' | 'dark' | 'system'): void;
449
+ /** Cleanup resources when the graph is destroyed */
450
+ destroy(): void;
439
451
  }
440
452
 
441
453
  type SnapshotMessage<N, E> = {
@@ -575,6 +587,7 @@ declare class Playground {
575
587
  private options;
576
588
  private rootElement;
577
589
  private currentExample;
590
+ private examples;
578
591
  private currentGraph;
579
592
  private ingest;
580
593
  private isEditable;
@@ -619,8 +632,20 @@ declare class Playground {
619
632
  private handleChangeConnection;
620
633
  private handleOpenFolder;
621
634
  private handleCloseFolder;
635
+ /**
636
+ * Add or update an example
637
+ */
638
+ addExample(key: string, example: Example): void;
639
+ /**
640
+ * Remove an example
641
+ */
642
+ removeExample(key: string): void;
643
+ /**
644
+ * Update the example list in the DOM
645
+ */
646
+ private updateExampleList;
622
647
  }
623
648
 
624
649
  declare function graph<N, E>(args?: APIArguments<N, E>): Promise<API<N, E>>;
625
650
 
626
- export { type Example, type ExampleEdge, type ExampleNode, type ExampleOptions, FileSource, FileSystemSource, type HistoryMessage, Ingest, type IngestMessage, Playground, type PlaygroundOptions, type SnapshotMessage, type UpdateMessage, WebSocketSource, graph as default, graph };
651
+ export { API, type APIArguments, type APIOptions, type EventsOptions, type Example, type ExampleEdge, type ExampleNode, type ExampleOptions, FileSource, FileSystemSource, type HistoryMessage, Ingest, type IngestMessage, Playground, type PlaygroundOptions, type SnapshotMessage, type Update, type UpdateMessage, Updater, WebSocketSource, graph as default, graph };
package/dist/index.js CHANGED
@@ -1338,15 +1338,16 @@ var Layout = class _Layout {
1338
1338
 
1339
1339
  // src/canvas/marker.tsx
1340
1340
  import { jsx } from "jsx-dom/jsx-runtime";
1341
- function arrow(size, reverse = false) {
1341
+ function arrow(size, reverse = false, prefix = "") {
1342
1342
  const h = size / 1.5;
1343
1343
  const w = size;
1344
1344
  const ry = h / 2;
1345
1345
  const suffix = reverse ? "-reverse" : "";
1346
+ const id = prefix ? `${prefix}-g3p-marker-arrow${suffix}` : `g3p-marker-arrow${suffix}`;
1346
1347
  return /* @__PURE__ */ jsx(
1347
1348
  "marker",
1348
1349
  {
1349
- id: `g3p-marker-arrow${suffix}`,
1350
+ id,
1350
1351
  className: "g3p-marker g3p-marker-arrow",
1351
1352
  markerWidth: size,
1352
1353
  markerHeight: size,
@@ -1358,14 +1359,15 @@ function arrow(size, reverse = false) {
1358
1359
  }
1359
1360
  );
1360
1361
  }
1361
- function circle(size, reverse = false) {
1362
+ function circle(size, reverse = false, prefix = "") {
1362
1363
  const r = size / 3;
1363
1364
  const cy = size / 2;
1364
1365
  const suffix = reverse ? "-reverse" : "";
1366
+ const id = prefix ? `${prefix}-g3p-marker-circle${suffix}` : `g3p-marker-circle${suffix}`;
1365
1367
  return /* @__PURE__ */ jsx(
1366
1368
  "marker",
1367
1369
  {
1368
- id: `g3p-marker-circle${suffix}`,
1370
+ id,
1369
1371
  className: "g3p-marker g3p-marker-circle",
1370
1372
  markerWidth: size,
1371
1373
  markerHeight: size,
@@ -1377,15 +1379,16 @@ function circle(size, reverse = false) {
1377
1379
  }
1378
1380
  );
1379
1381
  }
1380
- function diamond(size, reverse = false) {
1382
+ function diamond(size, reverse = false, prefix = "") {
1381
1383
  const w = size * 0.7;
1382
1384
  const h = size / 2;
1383
1385
  const cy = size / 2;
1384
1386
  const suffix = reverse ? "-reverse" : "";
1387
+ const id = prefix ? `${prefix}-g3p-marker-diamond${suffix}` : `g3p-marker-diamond${suffix}`;
1385
1388
  return /* @__PURE__ */ jsx(
1386
1389
  "marker",
1387
1390
  {
1388
- id: `g3p-marker-diamond${suffix}`,
1391
+ id,
1389
1392
  className: "g3p-marker g3p-marker-diamond",
1390
1393
  markerWidth: size,
1391
1394
  markerHeight: size,
@@ -1397,14 +1400,15 @@ function diamond(size, reverse = false) {
1397
1400
  }
1398
1401
  );
1399
1402
  }
1400
- function bar(size, reverse = false) {
1403
+ function bar(size, reverse = false, prefix = "") {
1401
1404
  const h = size * 0.6;
1402
1405
  const cy = size / 2;
1403
1406
  const suffix = reverse ? "-reverse" : "";
1407
+ const id = prefix ? `${prefix}-g3p-marker-bar${suffix}` : `g3p-marker-bar${suffix}`;
1404
1408
  return /* @__PURE__ */ jsx(
1405
1409
  "marker",
1406
1410
  {
1407
- id: `g3p-marker-bar${suffix}`,
1411
+ id,
1408
1412
  className: "g3p-marker g3p-marker-bar",
1409
1413
  markerWidth: size,
1410
1414
  markerHeight: size,
@@ -1416,7 +1420,7 @@ function bar(size, reverse = false) {
1416
1420
  }
1417
1421
  );
1418
1422
  }
1419
- function none(size, reverse = false) {
1423
+ function none(size, reverse = false, prefix = "") {
1420
1424
  return void 0;
1421
1425
  }
1422
1426
  function normalize(data) {
@@ -2198,6 +2202,9 @@ var Seg2 = class {
2198
2202
  if (this.source.isDummy) source = void 0;
2199
2203
  if (this.target.isDummy) target = void 0;
2200
2204
  const typeClass = this.type ? `g3p-edge-type-${this.type}` : "";
2205
+ const prefix = this.canvas.markerPrefix;
2206
+ const markerStartId = source ? prefix ? `${prefix}-g3p-marker-${source}-reverse` : `g3p-marker-${source}-reverse` : void 0;
2207
+ const markerEndId = target ? prefix ? `${prefix}-g3p-marker-${target}` : `g3p-marker-${target}` : void 0;
2201
2208
  return /* @__PURE__ */ jsxs2(
2202
2209
  "g",
2203
2210
  {
@@ -2212,8 +2219,8 @@ var Seg2 = class {
2212
2219
  d: this.svg,
2213
2220
  fill: "none",
2214
2221
  className: "g3p-seg-line",
2215
- markerStart: source ? `url(#g3p-marker-${source}-reverse)` : void 0,
2216
- markerEnd: target ? `url(#g3p-marker-${target})` : void 0
2222
+ markerStart: markerStartId ? `url(#${markerStartId})` : void 0,
2223
+ markerEnd: markerEndId ? `url(#${markerEndId})` : void 0
2217
2224
  }
2218
2225
  ),
2219
2226
  /* @__PURE__ */ jsx3(
@@ -2798,6 +2805,10 @@ var Canvas = class {
2798
2805
  curNodes;
2799
2806
  curSegs;
2800
2807
  updating;
2808
+ // Unique marker ID prefix for this canvas instance
2809
+ markerPrefix;
2810
+ // Dynamic style element for this instance (for cleanup)
2811
+ dynamicStyleEl;
2801
2812
  // Pan-zoom state
2802
2813
  panScale = null;
2803
2814
  zoomControls;
@@ -2811,6 +2822,13 @@ var Canvas = class {
2811
2822
  constructor(api, options) {
2812
2823
  Object.assign(this, options);
2813
2824
  this.api = api;
2825
+ this.reset();
2826
+ this.markerPrefix = api.root.replace(/[^a-zA-Z0-9-_]/g, "-");
2827
+ this.createMeasurementContainer();
2828
+ this.createCanvasContainer();
2829
+ if (this.panZoom) this.setupPanZoom();
2830
+ }
2831
+ reset() {
2814
2832
  this.allNodes = /* @__PURE__ */ new Map();
2815
2833
  this.curNodes = /* @__PURE__ */ new Map();
2816
2834
  this.curSegs = /* @__PURE__ */ new Map();
@@ -2819,9 +2837,7 @@ var Canvas = class {
2819
2837
  this.transform = { x: 0, y: 0, scale: 1 };
2820
2838
  this.editMode = new EditMode();
2821
2839
  this.editMode.editable = this.editable;
2822
- this.createMeasurementContainer();
2823
- this.createCanvasContainer();
2824
- if (this.panZoom) this.setupPanZoom();
2840
+ if (this.group) this.group.innerHTML = "";
2825
2841
  }
2826
2842
  createMeasurementContainer() {
2827
2843
  this.measurement = document.createElement("div");
@@ -3006,12 +3022,13 @@ var Canvas = class {
3006
3022
  }
3007
3023
  generateDynamicStyles() {
3008
3024
  let css = "";
3009
- css += themeToCSS(this.theme, `.g3p-canvas-container`);
3025
+ const scope = `[data-g3p-instance="${this.markerPrefix}"]`;
3026
+ css += themeToCSS(this.theme, scope);
3010
3027
  for (const [type, vars] of Object.entries(this.nodeTypes)) {
3011
- css += themeToCSS(vars, `.g3p-node-type-${type}`, "node");
3028
+ css += themeToCSS(vars, `${scope} .g3p-node-type-${type}`, "node");
3012
3029
  }
3013
3030
  for (const [type, vars] of Object.entries(this.edgeTypes)) {
3014
- css += themeToCSS(vars, `.g3p-edge-type-${type}`);
3031
+ css += themeToCSS(vars, `${scope} .g3p-edge-type-${type}`);
3015
3032
  }
3016
3033
  return css;
3017
3034
  }
@@ -3024,15 +3041,18 @@ var Canvas = class {
3024
3041
  }
3025
3042
  const dynamicStyles = this.generateDynamicStyles();
3026
3043
  if (dynamicStyles) {
3027
- const dynamicStyleEl = document.createElement("style");
3028
- dynamicStyleEl.textContent = dynamicStyles;
3029
- document.head.appendChild(dynamicStyleEl);
3044
+ this.dynamicStyleEl?.remove();
3045
+ this.dynamicStyleEl = document.createElement("style");
3046
+ this.dynamicStyleEl.id = `g3p-styles-${this.markerPrefix}`;
3047
+ this.dynamicStyleEl.textContent = dynamicStyles;
3048
+ document.head.appendChild(this.dynamicStyleEl);
3030
3049
  }
3031
3050
  const colorModeClass = this.colorMode !== "system" ? `g3p-${this.colorMode}` : "";
3032
3051
  this.container = /* @__PURE__ */ jsx6(
3033
3052
  "div",
3034
3053
  {
3035
3054
  className: `g3p-canvas-container ${colorModeClass}`.trim(),
3055
+ "data-g3p-instance": this.markerPrefix,
3036
3056
  ref: (el) => this.container = el,
3037
3057
  onContextMenu: this.onContextMenu.bind(this),
3038
3058
  children: /* @__PURE__ */ jsxs5(
@@ -3048,8 +3068,8 @@ var Canvas = class {
3048
3068
  onDblClick: this.onDoubleClick.bind(this),
3049
3069
  children: [
3050
3070
  /* @__PURE__ */ jsxs5("defs", { children: [
3051
- Object.values(markerDefs).map((marker) => marker(this.markerSize, false)),
3052
- Object.values(markerDefs).map((marker) => marker(this.markerSize, true))
3071
+ Object.values(markerDefs).map((marker) => marker(this.markerSize, false, this.markerPrefix)),
3072
+ Object.values(markerDefs).map((marker) => marker(this.markerSize, true, this.markerPrefix))
3053
3073
  ] }),
3054
3074
  /* @__PURE__ */ jsx6(
3055
3075
  "g",
@@ -3406,6 +3426,40 @@ var Canvas = class {
3406
3426
  }
3407
3427
  return { type: "canvas" };
3408
3428
  }
3429
+ /** Update theme and type styles dynamically */
3430
+ updateStyles(options) {
3431
+ if (options.theme !== void 0) this.theme = options.theme;
3432
+ if (options.nodeTypes !== void 0) this.nodeTypes = options.nodeTypes;
3433
+ if (options.edgeTypes !== void 0) this.edgeTypes = options.edgeTypes;
3434
+ const dynamicStyles = this.generateDynamicStyles();
3435
+ if (dynamicStyles) {
3436
+ if (!this.dynamicStyleEl) {
3437
+ this.dynamicStyleEl = document.createElement("style");
3438
+ this.dynamicStyleEl.id = `g3p-styles-${this.markerPrefix}`;
3439
+ document.head.appendChild(this.dynamicStyleEl);
3440
+ }
3441
+ this.dynamicStyleEl.textContent = dynamicStyles;
3442
+ } else if (this.dynamicStyleEl) {
3443
+ this.dynamicStyleEl.remove();
3444
+ this.dynamicStyleEl = void 0;
3445
+ }
3446
+ }
3447
+ /** Update color mode without recreating the canvas */
3448
+ setColorMode(colorMode) {
3449
+ if (!this.container) return;
3450
+ this.colorMode = colorMode;
3451
+ this.container.classList.remove("g3p-light", "g3p-dark");
3452
+ if (colorMode !== "system") {
3453
+ this.container.classList.add(`g3p-${colorMode}`);
3454
+ }
3455
+ }
3456
+ /** Cleanup resources when the canvas is destroyed */
3457
+ destroy() {
3458
+ this.dynamicStyleEl?.remove();
3459
+ this.dynamicStyleEl = void 0;
3460
+ this.measurement?.remove();
3461
+ this.measurement = void 0;
3462
+ }
3409
3463
  };
3410
3464
  var themeVarMap = {
3411
3465
  // Canvas
@@ -3622,6 +3676,7 @@ var API = class {
3622
3676
  this.nodeFields = /* @__PURE__ */ new Map();
3623
3677
  this.nextNodeId = 1;
3624
3678
  this.nextEdgeId = 1;
3679
+ this.canvas?.reset?.();
3625
3680
  }
3626
3681
  /** Initialize the API */
3627
3682
  async init() {
@@ -3629,6 +3684,9 @@ var API = class {
3629
3684
  if (!root) throw new Error("root element not found");
3630
3685
  root.appendChild(this.canvas.container);
3631
3686
  await this.applyHistory();
3687
+ if (this.events.onInit) {
3688
+ this.events.onInit();
3689
+ }
3632
3690
  }
3633
3691
  async applyHistory() {
3634
3692
  for (const update of this.history)
@@ -4084,6 +4142,18 @@ var API = class {
4084
4142
  else
4085
4143
  await this.deleteEdge(edge.data);
4086
4144
  }
4145
+ /** Update theme and type styles dynamically */
4146
+ updateStyles(options) {
4147
+ this.canvas?.updateStyles(options);
4148
+ }
4149
+ /** Update color mode without recreating the canvas */
4150
+ setColorMode(colorMode) {
4151
+ this.canvas?.setColorMode(colorMode);
4152
+ }
4153
+ /** Cleanup resources when the graph is destroyed */
4154
+ destroy() {
4155
+ this.canvas?.destroy();
4156
+ }
4087
4157
  };
4088
4158
 
4089
4159
  // src/api/ingest.ts
@@ -4373,6 +4443,7 @@ var Playground = class {
4373
4443
  options;
4374
4444
  rootElement;
4375
4445
  currentExample;
4446
+ examples;
4376
4447
  currentGraph = null;
4377
4448
  ingest = null;
4378
4449
  isEditable = false;
@@ -4390,7 +4461,8 @@ var Playground = class {
4390
4461
  graphContainerId;
4391
4462
  constructor(options) {
4392
4463
  this.options = options;
4393
- this.exampleList = Object.keys(options.examples);
4464
+ this.examples = { ...options.examples };
4465
+ this.exampleList = Object.keys(this.examples);
4394
4466
  this.currentExample = options.defaultExample || this.exampleList[0];
4395
4467
  this.graphContainerId = `playground-graph-${Math.random().toString(36).substr(2, 9)}`;
4396
4468
  if (typeof options.root === "string") {
@@ -4421,7 +4493,7 @@ var Playground = class {
4421
4493
  }
4422
4494
  createDOM() {
4423
4495
  const exampleList = this.exampleList.map((key, i) => {
4424
- const example = this.options.examples[key];
4496
+ const example = this.examples[key];
4425
4497
  const isActive = i === 0 || key === this.currentExample;
4426
4498
  return `
4427
4499
  <button class="example-btn ${isActive ? "active" : ""}" data-example="${key}">
@@ -4507,7 +4579,14 @@ var Playground = class {
4507
4579
  });
4508
4580
  });
4509
4581
  this.rootElement.querySelectorAll(".options select, .options input").forEach((el) => {
4510
- el.addEventListener("change", () => this.renderGraph());
4582
+ el.addEventListener("change", () => {
4583
+ if (el.id === "colorMode" && this.currentGraph) {
4584
+ const mode = el.value;
4585
+ this.currentGraph.setColorMode(mode);
4586
+ } else {
4587
+ this.renderGraph();
4588
+ }
4589
+ });
4511
4590
  });
4512
4591
  this.rootElement.querySelector("#nav-first")?.addEventListener("click", () => {
4513
4592
  this.currentGraph?.nav("first");
@@ -4594,8 +4673,10 @@ var Playground = class {
4594
4673
  async renderGraph() {
4595
4674
  const container = this.rootElement.querySelector(`#${this.graphContainerId}`);
4596
4675
  if (!container) return;
4676
+ this.currentGraph?.destroy();
4677
+ this.currentGraph = null;
4597
4678
  container.innerHTML = "";
4598
- const example = this.options.examples[this.currentExample];
4679
+ const example = this.examples[this.currentExample];
4599
4680
  const options = this.getOptions(example.options);
4600
4681
  try {
4601
4682
  this.currentGraph = await graph({
@@ -4626,7 +4707,7 @@ var Playground = class {
4626
4707
  }
4627
4708
  }
4628
4709
  connectExampleSource() {
4629
- const example = this.options.examples[this.currentExample];
4710
+ const example = this.examples[this.currentExample];
4630
4711
  if (!example.source) {
4631
4712
  this.disconnectAllSources();
4632
4713
  return;
@@ -4969,7 +5050,7 @@ var Playground = class {
4969
5050
  `;
4970
5051
  }
4971
5052
  } else if (this.activeSourceType === "file") {
4972
- const example = this.options.examples[this.currentExample];
5053
+ const example = this.examples[this.currentExample];
4973
5054
  const filePath = example.source?.type === "file" ? example.source.path : "";
4974
5055
  if (this.fileStatus === "connecting") {
4975
5056
  statusDiv.innerHTML = `
@@ -5067,6 +5148,58 @@ var Playground = class {
5067
5148
  this.fsSource?.close();
5068
5149
  this.updateSourceModal();
5069
5150
  }
5151
+ /**
5152
+ * Add or update an example
5153
+ */
5154
+ addExample(key, example) {
5155
+ this.examples[key] = example;
5156
+ this.updateExampleList();
5157
+ if (this.currentExample === key) {
5158
+ this.renderGraph();
5159
+ this.connectExampleSource();
5160
+ }
5161
+ }
5162
+ /**
5163
+ * Remove an example
5164
+ */
5165
+ removeExample(key) {
5166
+ delete this.examples[key];
5167
+ if (this.currentExample === key) {
5168
+ this.exampleList = Object.keys(this.examples);
5169
+ this.currentExample = this.exampleList[0] || "";
5170
+ if (this.currentExample) {
5171
+ this.renderGraph();
5172
+ this.connectExampleSource();
5173
+ }
5174
+ }
5175
+ this.updateExampleList();
5176
+ }
5177
+ /**
5178
+ * Update the example list in the DOM
5179
+ */
5180
+ updateExampleList() {
5181
+ this.exampleList = Object.keys(this.examples);
5182
+ const exampleListEl = this.rootElement.querySelector(".example-list");
5183
+ if (!exampleListEl) return;
5184
+ exampleListEl.innerHTML = this.exampleList.map((key, i) => {
5185
+ const example = this.examples[key];
5186
+ const isActive = key === this.currentExample;
5187
+ return `
5188
+ <button class="example-btn ${isActive ? "active" : ""}" data-example="${key}">
5189
+ ${example.name}
5190
+ </button>
5191
+ `;
5192
+ }).join("");
5193
+ exampleListEl.querySelectorAll(".example-btn").forEach((btn) => {
5194
+ btn.addEventListener("click", () => {
5195
+ this.rootElement.querySelectorAll(".example-btn").forEach((b) => b.classList.remove("active"));
5196
+ btn.classList.add("active");
5197
+ this.currentExample = btn.getAttribute("data-example") || this.exampleList[0];
5198
+ this.renderGraph();
5199
+ this.connectExampleSource();
5200
+ });
5201
+ });
5202
+ }
5070
5203
  };
5071
5204
 
5072
5205
  // src/index.ts
@@ -5081,6 +5214,7 @@ export {
5081
5214
  FileSystemSource,
5082
5215
  Ingest,
5083
5216
  Playground,
5217
+ Updater,
5084
5218
  WebSocketSource,
5085
5219
  index_default as default,
5086
5220
  graph
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@3plate/graph-core",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "type": "module",
5
5
  "license": "GPL-3.0",
6
6
  "repository": {