@stocksharp/diagram 0.6.0 → 0.7.0

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.
Files changed (57) hide show
  1. package/dist/esm/canvas-renderer.js +185 -34
  2. package/dist/esm/canvas-renderer.js.map +1 -1
  3. package/dist/esm/core/document.js +24 -12
  4. package/dist/esm/core/document.js.map +1 -1
  5. package/dist/esm/core/history.js +12 -3
  6. package/dist/esm/core/history.js.map +1 -1
  7. package/dist/esm/core/json.js +10 -0
  8. package/dist/esm/core/json.js.map +1 -1
  9. package/dist/esm/diagram/context-menu.js +28 -10
  10. package/dist/esm/diagram/context-menu.js.map +1 -1
  11. package/dist/esm/diagram/event-emitter.js +12 -1
  12. package/dist/esm/diagram/event-emitter.js.map +1 -1
  13. package/dist/esm/diagram/palette.js +9 -1
  14. package/dist/esm/diagram/palette.js.map +1 -1
  15. package/dist/esm/diagram/stocksharp-diagram.js +8 -0
  16. package/dist/esm/diagram/stocksharp-diagram.js.map +1 -1
  17. package/dist/esm/diagram/types.js +29 -2
  18. package/dist/esm/diagram/types.js.map +1 -1
  19. package/dist/esm/embed.js +63 -10
  20. package/dist/esm/embed.js.map +1 -1
  21. package/dist/esm/i18n.js.map +1 -1
  22. package/dist/esm/svg-surface.js +10 -2
  23. package/dist/esm/svg-surface.js.map +1 -1
  24. package/dist/ssdiagram.js +253 -74
  25. package/dist/ssdiagram.js.map +3 -3
  26. package/dist/types/canvas-renderer.d.ts +19 -1
  27. package/dist/types/canvas-renderer.d.ts.map +1 -1
  28. package/dist/types/core/document.d.ts.map +1 -1
  29. package/dist/types/core/history.d.ts +1 -0
  30. package/dist/types/core/history.d.ts.map +1 -1
  31. package/dist/types/core/json.d.ts +8 -0
  32. package/dist/types/core/json.d.ts.map +1 -1
  33. package/dist/types/diagram/context-menu.d.ts +6 -0
  34. package/dist/types/diagram/context-menu.d.ts.map +1 -1
  35. package/dist/types/diagram/event-emitter.d.ts.map +1 -1
  36. package/dist/types/diagram/palette.d.ts.map +1 -1
  37. package/dist/types/diagram/stocksharp-diagram.d.ts.map +1 -1
  38. package/dist/types/diagram/types.d.ts +5 -0
  39. package/dist/types/diagram/types.d.ts.map +1 -1
  40. package/dist/types/embed.d.ts.map +1 -1
  41. package/dist/types/i18n.d.ts +2 -0
  42. package/dist/types/i18n.d.ts.map +1 -1
  43. package/dist/types/svg-surface.d.ts +1 -1
  44. package/dist/types/svg-surface.d.ts.map +1 -1
  45. package/package.json +1 -1
  46. package/src/canvas-renderer.ts +173 -34
  47. package/src/core/document.ts +25 -12
  48. package/src/core/history.ts +13 -3
  49. package/src/core/json.ts +11 -0
  50. package/src/diagram/context-menu.ts +30 -10
  51. package/src/diagram/event-emitter.ts +10 -1
  52. package/src/diagram/palette.ts +9 -2
  53. package/src/diagram/stocksharp-diagram.ts +5 -0
  54. package/src/diagram/types.ts +29 -3
  55. package/src/embed.ts +71 -10
  56. package/src/i18n.ts +2 -0
  57. package/src/svg-surface.ts +11 -2
package/dist/ssdiagram.js CHANGED
@@ -67,6 +67,9 @@ var SSDiagram = (() => {
67
67
  }
68
68
  target[key] = value;
69
69
  }
70
+ function getJsonKey(target, key) {
71
+ return Object.prototype.hasOwnProperty.call(target, key) ? target[key] : void 0;
72
+ }
70
73
 
71
74
  // src/core/model.ts
72
75
  function toPortDynamicMode(value) {
@@ -110,15 +113,8 @@ var SSDiagram = (() => {
110
113
  return normalizeLink({ ...link, id }, `$.links[${index}]`);
111
114
  });
112
115
  const rawZones = mapElements(input.zones, "$.zones");
113
- const usedZoneIds = /* @__PURE__ */ new Set();
114
- const zones = rawZones.map((zone, index) => {
115
- const normalized = normalizeZone(zone, `$.zones[${index}]`);
116
- if (usedZoneIds.has(normalized.id)) {
117
- throw new DiagramDocumentError(`duplicate zone id "${normalized.id}"`, `$.zones[${index}].id`);
118
- }
119
- usedZoneIds.add(normalized.id);
120
- return normalized;
121
- });
116
+ const zones = rawZones.map((zone, index) => normalizeZone(zone, `$.zones[${index}]`));
117
+ validateUniqueZones(zones);
122
118
  const document2 = {
123
119
  version: DIAGRAM_DOCUMENT_VERSION,
124
120
  nodes,
@@ -239,8 +235,9 @@ var SSDiagram = (() => {
239
235
  return {
240
236
  id: requireIdentifier(input.id, `${path}.id`),
241
237
  name: requireString(input.name, `${path}.name`),
242
- x: requireNumber(input.x, `${path}.x`),
243
- y: requireNumber(input.y, `${path}.y`),
238
+ // Finite, like node coordinates: NaN survives every arithmetic check and writes as null.
239
+ x: requireFiniteNumber(input.x, `${path}.x`),
240
+ y: requireFiniteNumber(input.y, `${path}.y`),
244
241
  width: requirePositiveSize(input.width, `${path}.width`),
245
242
  height: requirePositiveSize(input.height, `${path}.height`),
246
243
  color: input.color === void 0 || input.color === null ? "" : requireString(input.color, `${path}.color`),
@@ -248,7 +245,7 @@ var SSDiagram = (() => {
248
245
  };
249
246
  }
250
247
  function requirePositiveSize(value, path) {
251
- const size = requireNumber(value, path);
248
+ const size = requireFiniteNumber(value, path);
252
249
  if (!(size > 0)) throw new DiagramDocumentError("size must be greater than zero", path);
253
250
  return size;
254
251
  }
@@ -330,6 +327,14 @@ var SSDiagram = (() => {
330
327
  portId: requireIdentifier(endpoint.portId, `${path}.portId`)
331
328
  };
332
329
  }
330
+ function validateUniqueZones(zones) {
331
+ const seen = /* @__PURE__ */ new Set();
332
+ for (let index = 0; index < zones.length; index++) {
333
+ const zone = zones[index];
334
+ if (seen.has(zone.id)) throw new DiagramDocumentError(`duplicate zone id "${zone.id}"`, `$.zones[${index}].id`);
335
+ seen.add(zone.id);
336
+ }
337
+ }
333
338
  function validateDocument(document2) {
334
339
  const nodes = /* @__PURE__ */ new Map();
335
340
  for (let index = 0; index < document2.nodes.length; index++) {
@@ -360,6 +365,7 @@ var SSDiagram = (() => {
360
365
  if (endpoints.has(key)) throw new DiagramDocumentError("duplicate link endpoints", path);
361
366
  endpoints.add(key);
362
367
  }
368
+ validateUniqueZones(document2.zones);
363
369
  }
364
370
  function validateUniquePorts(ports, path) {
365
371
  const ids = /* @__PURE__ */ new Set();
@@ -573,7 +579,13 @@ var SSDiagram = (() => {
573
579
  throw error;
574
580
  }
575
581
  }
582
+ assertNoOpenTransaction(operation) {
583
+ if (this.transactions.length > 0) {
584
+ throw new Error(`Cannot ${operation} command history inside a transaction.`);
585
+ }
586
+ }
576
587
  undo() {
588
+ this.assertNoOpenTransaction("undo");
577
589
  const command = this.undoCommands.pop();
578
590
  if (command === void 0) return false;
579
591
  try {
@@ -587,6 +599,7 @@ var SSDiagram = (() => {
587
599
  return true;
588
600
  }
589
601
  redo() {
602
+ this.assertNoOpenTransaction("redo");
590
603
  const command = this.redoCommands.pop();
591
604
  if (command === void 0) return false;
592
605
  try {
@@ -600,9 +613,7 @@ var SSDiagram = (() => {
600
613
  return true;
601
614
  }
602
615
  clear() {
603
- if (this.transactions.length > 0) {
604
- throw new Error("Cannot clear command history inside a transaction.");
605
- }
616
+ this.assertNoOpenTransaction("clear");
606
617
  this.undoCommands.length = 0;
607
618
  this.redoCommands.length = 0;
608
619
  this.notify();
@@ -1045,12 +1056,16 @@ var SSDiagram = (() => {
1045
1056
  this.emit(`<rect x="${n(x)}" y="${n(y)}" width="${n(w)}" height="${n(h)}" fill="none" ${this.strokeAttrs()}/>`);
1046
1057
  }
1047
1058
  // ===== text ==========================================================
1048
- fillText(text, x, y) {
1059
+ fillText(text, x, y, maxWidth) {
1049
1060
  if (text === "") return;
1050
1061
  const anchor = ANCHOR[this.state.textAlign] ?? "start";
1051
1062
  const baseline = BASELINE[this.state.textBaseline] ?? "alphabetic";
1063
+ let constraint = "";
1064
+ if (maxWidth !== void 0 && maxWidth > 0 && this.measureText(text).width > maxWidth) {
1065
+ constraint = ` textLength="${n(maxWidth)}" lengthAdjust="spacingAndGlyphs"`;
1066
+ }
1052
1067
  this.emit(
1053
- `<text x="${n(x)}" y="${n(y)}" style="font:${escapeXml(this.state.font)}" text-anchor="${anchor}" dominant-baseline="${baseline}" ${this.fillAttrs()}>${escapeXml(text)}</text>`
1068
+ `<text x="${n(x)}" y="${n(y)}" style="font:${escapeXml(this.state.font)}" text-anchor="${anchor}" dominant-baseline="${baseline}"${constraint} ${this.fillAttrs()}>${escapeXml(text)}</text>`
1054
1069
  );
1055
1070
  }
1056
1071
  measureText(text) {
@@ -1358,15 +1373,20 @@ var SSDiagram = (() => {
1358
1373
  this.introStart = null;
1359
1374
  this.overviewVisible = true;
1360
1375
  this.ovDragging = false;
1376
+ this.historyShortcut = (direction) => {
1377
+ if (direction === "undo") this.undo();
1378
+ else this.redo();
1379
+ };
1361
1380
  this.ovGeo = null;
1362
1381
  this.validator = null;
1363
1382
  this.handlers = /* @__PURE__ */ new Map();
1364
1383
  this.opts = opts;
1384
+ if (opts.typeColors !== void 0) this.setTypeColorsInternal(opts.typeColors);
1365
1385
  this.host = opts.host;
1366
1386
  this.gridSnapEnabled = opts.gridSnap ?? false;
1367
1387
  this.gridSize = this.normalizeGridSize(opts.gridSize ?? DEFAULT_GRID_SIZE);
1368
- this.history = new DiagramCommandHistory(({ canUndo, canRedo }) => {
1369
- this.emit("undoStackChanged", { canUndo, canRedo });
1388
+ this.history = new DiagramCommandHistory(() => {
1389
+ this.emit("undoStackChanged", { canUndo: this.canUndo(), canRedo: this.canRedo() });
1370
1390
  });
1371
1391
  this.canvas = document.createElement("canvas");
1372
1392
  this.canvas.style.display = "block";
@@ -1749,6 +1769,8 @@ var SSDiagram = (() => {
1749
1769
  clear() {
1750
1770
  this.nodes = [];
1751
1771
  this.links = [];
1772
+ this.zones = [];
1773
+ const hadSelection = this.selectedNode !== null || this.selectedNodes.size > 0 || this.selectedLink !== null || this.selectedPort !== null;
1752
1774
  this.selectedNode = null;
1753
1775
  this.selectedNodes.clear();
1754
1776
  this.selectedLink = null;
@@ -1758,6 +1780,7 @@ var SSDiagram = (() => {
1758
1780
  this.runtimePulse = 0;
1759
1781
  this.globalErrorFlashStart = null;
1760
1782
  this.history.clear();
1783
+ if (hadSelection) this.emitSelectionChanged();
1761
1784
  this.emitRuntimeStateChanged();
1762
1785
  }
1763
1786
  relink(linkId, next) {
@@ -1976,6 +1999,20 @@ var SSDiagram = (() => {
1976
1999
  this.clear();
1977
2000
  this.idSeq = 1;
1978
2001
  this.linkSeq = 1;
2002
+ const seenNodeIds = /* @__PURE__ */ new Set();
2003
+ for (const node of nodes) {
2004
+ const id = node.id ?? "";
2005
+ if (id === "") continue;
2006
+ if (seenNodeIds.has(id)) throw new Error(`ssdiagram: duplicate node id "${id}"`);
2007
+ seenNodeIds.add(id);
2008
+ }
2009
+ const seenLinkIds = /* @__PURE__ */ new Set();
2010
+ for (const link of links) {
2011
+ const id = link.id ?? "";
2012
+ if (id === "") continue;
2013
+ if (seenLinkIds.has(id)) throw new Error(`ssdiagram: duplicate link id "${id}"`);
2014
+ seenLinkIds.add(id);
2015
+ }
1979
2016
  for (const node of nodes) {
1980
2017
  const id = node.id ?? this.nextNodeId();
1981
2018
  this.doAddNode({ ...node, id });
@@ -1992,7 +2029,7 @@ var SSDiagram = (() => {
1992
2029
  if (node.loadError.length === 0) continue;
1993
2030
  const state = createDiagramNodeRuntimeState();
1994
2031
  state.errors.load = { kind: "load", message: node.loadError, pulse: ++this.runtimePulse };
1995
- this.runtimeState.nodes[node.id] = state;
2032
+ setJsonKey(this.runtimeState.nodes, node.id, state);
1996
2033
  }
1997
2034
  if (Object.keys(this.runtimeState.nodes).length > 0) this.emitRuntimeStateChanged();
1998
2035
  this.history.clear();
@@ -2003,7 +2040,15 @@ var SSDiagram = (() => {
2003
2040
  save() {
2004
2041
  return {
2005
2042
  nodes: this.nodes.map((node) => node.toInit(false)),
2006
- links: this.links.map((l) => ({ from: l.from, fromPort: l.fromPort, to: l.to, toPort: l.toPort }))
2043
+ links: this.links.map((l) => ({
2044
+ id: l.id,
2045
+ from: l.from,
2046
+ fromPort: l.fromPort,
2047
+ to: l.to,
2048
+ toPort: l.toPort,
2049
+ style: l.style,
2050
+ metadata: copyJsonObject(l.metadata)
2051
+ }))
2007
2052
  };
2008
2053
  }
2009
2054
  loadDocument(source) {
@@ -2073,7 +2118,20 @@ var SSDiagram = (() => {
2073
2118
  overviewVisible: this.overviewVisible
2074
2119
  };
2075
2120
  }
2121
+ /**
2122
+ * Replaces what Ctrl+Z / Ctrl+Y do. Passing null restores the built-in behaviour, which is
2123
+ * plain undo()/redo() -- so a renderer used on its own keeps its keyboard history.
2124
+ */
2125
+ setHistoryShortcutHandler(handler) {
2126
+ this.historyShortcut = handler ?? ((direction) => {
2127
+ if (direction === "undo") this.undo();
2128
+ else this.redo();
2129
+ });
2130
+ }
2076
2131
  setViewState(state) {
2132
+ if (!Number.isFinite(state.zoom) || !Number.isFinite(state.panX) || !Number.isFinite(state.panY)) {
2133
+ throw new RangeError("ssdiagram: view state zoom and pan must be finite numbers");
2134
+ }
2077
2135
  this.scale = clamp(state.zoom, ZOOM_MIN, ZOOM_MAX);
2078
2136
  this.offX = state.panX;
2079
2137
  this.offY = state.panY;
@@ -2101,6 +2159,7 @@ var SSDiagram = (() => {
2101
2159
  throw new RangeError(`ssdiagram: unsupported screenshot scope "${String(scope)}"`);
2102
2160
  }
2103
2161
  if (scope === "viewport" && Object.keys(options).length === 0) {
2162
+ if (this.drawScheduled) this.draw();
2104
2163
  const copy = document.createElement("canvas");
2105
2164
  copy.width = this.canvas.width;
2106
2165
  copy.height = this.canvas.height;
@@ -2127,7 +2186,10 @@ var SSDiagram = (() => {
2127
2186
  offX: this.offX,
2128
2187
  offY: this.offY,
2129
2188
  overviewVisible: this.overviewVisible,
2130
- background: this.opts.background
2189
+ background: this.opts.background,
2190
+ // Drawing the overview recomputes this from the export frame. Left behind, the next
2191
+ // click on the on-screen minimap panned to a point in the export's coordinates.
2192
+ ovGeo: this.ovGeo
2131
2193
  };
2132
2194
  try {
2133
2195
  this.ctx = outputContext;
@@ -2157,6 +2219,7 @@ var SSDiagram = (() => {
2157
2219
  this.offY = previous.offY;
2158
2220
  this.overviewVisible = previous.overviewVisible;
2159
2221
  this.opts.background = previous.background;
2222
+ this.ovGeo = previous.ovGeo;
2160
2223
  }
2161
2224
  return output;
2162
2225
  }
@@ -2210,10 +2273,15 @@ var SSDiagram = (() => {
2210
2273
  scale: this.scale,
2211
2274
  offX: this.offX,
2212
2275
  offY: this.offY,
2213
- overviewVisible: this.overviewVisible
2276
+ overviewVisible: this.overviewVisible,
2277
+ background: this.opts.background,
2278
+ // Drawing the overview recomputes this from the export frame. Left behind, the next
2279
+ // click on the on-screen minimap panned to a point in the export's coordinates.
2280
+ ovGeo: this.ovGeo
2214
2281
  };
2215
2282
  try {
2216
2283
  this.ctx = surface;
2284
+ if (options.background !== void 0) this.opts.background = options.background;
2217
2285
  this.width = width;
2218
2286
  this.height = height;
2219
2287
  this.dpr = 1;
@@ -2237,6 +2305,8 @@ var SSDiagram = (() => {
2237
2305
  this.offX = previous.offX;
2238
2306
  this.offY = previous.offY;
2239
2307
  this.overviewVisible = previous.overviewVisible;
2308
+ this.opts.background = previous.background;
2309
+ this.ovGeo = previous.ovGeo;
2240
2310
  }
2241
2311
  return surface.toSvg();
2242
2312
  }
@@ -2269,8 +2339,8 @@ var SSDiagram = (() => {
2269
2339
  ...Object.values(this.runtimeState.nodes).flatMap((node) => [node.errors.runtime?.pulse ?? 0, node.errors.load?.pulse ?? 0])
2270
2340
  );
2271
2341
  for (const node of this.nodes) {
2272
- const errors = this.runtimeState.nodes[node.id]?.errors;
2273
- const previousRuntime = previous.nodes[node.id]?.errors.runtime;
2342
+ const errors = getJsonKey(this.runtimeState.nodes, node.id)?.errors;
2343
+ const previousRuntime = getJsonKey(previous.nodes, node.id)?.errors.runtime;
2274
2344
  const runtimeError = errors?.runtime;
2275
2345
  node.runtimeError = runtimeError?.message ?? "";
2276
2346
  node.loadError = errors?.load?.message ?? "";
@@ -2299,8 +2369,8 @@ var SSDiagram = (() => {
2299
2369
  const port = direction === "in" ? node?.inPorts.find((candidate) => candidate.id === portId) : node?.outPorts.find((candidate) => candidate.id === portId);
2300
2370
  if (node === void 0 || port === void 0) return false;
2301
2371
  const state = this.getRuntimeState();
2302
- const nodeState = state.nodes[nodeId] ?? createDiagramNodeRuntimeState();
2303
- state.nodes[nodeId] = nodeState;
2372
+ const nodeState = getJsonKey(state.nodes, nodeId) ?? createDiagramNodeRuntimeState();
2373
+ setJsonKey(state.nodes, nodeId, nodeState);
2304
2374
  const current = nodeState.ports[direction][portId] ?? createDiagramPortRuntimeState();
2305
2375
  nodeState.ports[direction][portId] = { ...current, ...patch };
2306
2376
  this.setRuntimeState(state);
@@ -2322,8 +2392,8 @@ var SSDiagram = (() => {
2322
2392
  node.errorFlashStart = message.length > 0 && options.animate !== false ? performance.now() : null;
2323
2393
  }
2324
2394
  const state = this.getRuntimeState();
2325
- const nodeState = state.nodes[id] ?? createDiagramNodeRuntimeState();
2326
- state.nodes[id] = nodeState;
2395
+ const nodeState = getJsonKey(state.nodes, id) ?? createDiagramNodeRuntimeState();
2396
+ setJsonKey(state.nodes, id, nodeState);
2327
2397
  if (message.length === 0) delete nodeState.errors[kind];
2328
2398
  else if (kind === "load") nodeState.errors.load = { kind, message, pulse: ++this.runtimePulse };
2329
2399
  else nodeState.errors.runtime = { kind, message, pulse: ++this.runtimePulse };
@@ -2340,7 +2410,7 @@ var SSDiagram = (() => {
2340
2410
  }
2341
2411
  if (kind === void 0 || kind === "load") node.loadError = "";
2342
2412
  const state = this.getRuntimeState();
2343
- const nodeState = state.nodes[id];
2413
+ const nodeState = getJsonKey(state.nodes, id);
2344
2414
  if (nodeState !== void 0) {
2345
2415
  if (kind === void 0 || kind === "runtime") delete nodeState.errors.runtime;
2346
2416
  if (kind === void 0 || kind === "load") delete nodeState.errors.load;
@@ -2364,6 +2434,7 @@ var SSDiagram = (() => {
2364
2434
  this.selectNode(node);
2365
2435
  }
2366
2436
  setZoom(scale) {
2437
+ if (!Number.isFinite(scale)) throw new RangeError("ssdiagram: zoom must be a finite number");
2367
2438
  const cx = this.width / 2;
2368
2439
  const cy = this.height / 2;
2369
2440
  const wx = (cx - this.offX) / this.scale;
@@ -2405,9 +2476,21 @@ var SSDiagram = (() => {
2405
2476
  this.scheduleDraw();
2406
2477
  }
2407
2478
  setTypeColors(colors) {
2408
- this.opts.typeColors = { ...colors };
2479
+ this.setTypeColorsInternal(colors);
2409
2480
  this.scheduleDraw();
2410
2481
  }
2482
+ /**
2483
+ * Keyed the way port types are compared everywhere else. Connection compatibility is case- and
2484
+ * space-insensitive, so an imported scheme spelling a socket "candle " connects to "Candle" and
2485
+ * then fell through to a hash colour, showing one logical type in two colours in one diagram.
2486
+ */
2487
+ setTypeColorsInternal(colors) {
2488
+ const normalized = {};
2489
+ for (const [type, color] of Object.entries(colors)) {
2490
+ setJsonKey(normalized, normalizePortType(type), color);
2491
+ }
2492
+ this.opts.typeColors = normalized;
2493
+ }
2411
2494
  setTheme(t2) {
2412
2495
  if (t2.background !== void 0) this.opts.background = t2.background;
2413
2496
  if (t2.gridColor !== void 0) this.opts.gridColor = t2.gridColor;
@@ -2509,7 +2592,8 @@ var SSDiagram = (() => {
2509
2592
  const light = maxL !== void 0 && maxL < 0.5;
2510
2593
  if (type === "") return light ? "#585c62" : "#9aa0a6";
2511
2594
  const map = this.opts.typeColors ?? {};
2512
- const base = map[type] ?? `hsl(${hashHue(type)}, 62%, 58%)`;
2595
+ const normalized = normalizePortType(type);
2596
+ const base = getJsonKey(map, normalized) ?? `hsl(${hashHue(normalized)}, 62%, 58%)`;
2513
2597
  return maxL !== void 0 ? clampLightness(base, maxL) : base;
2514
2598
  }
2515
2599
  // Selection/hover accent. The dark-canvas blue washes out on a light canvas, so a light theme
@@ -2529,6 +2613,7 @@ var SSDiagram = (() => {
2529
2613
  for (const p of [...n2.inPorts, ...n2.outPorts]) {
2530
2614
  if ((wx - p.cx) ** 2 + (wy - p.cy) ** 2 <= (PORT_R + 4) ** 2) return { node: n2, port: p };
2531
2615
  }
2616
+ if (wx >= n2.x && wx <= n2.x + n2.w && wy >= n2.y && wy <= n2.y + n2.h) return null;
2532
2617
  }
2533
2618
  return null;
2534
2619
  }
@@ -2807,6 +2892,9 @@ var SSDiagram = (() => {
2807
2892
  fromPort: l.from.portId,
2808
2893
  to: t2,
2809
2894
  toPort: l.to.portId,
2895
+ // The clipboard is built from saveDocument(), so it carries the style;
2896
+ // rebuilding without it silently turned every pasted dashed link solid.
2897
+ style: l.style,
2810
2898
  metadata: l.metadata
2811
2899
  });
2812
2900
  }
@@ -2883,14 +2971,16 @@ var SSDiagram = (() => {
2883
2971
  }
2884
2972
  this.cancelLongPress();
2885
2973
  this.relinkCandidate = null;
2886
- this.lpStart = { sx: e.clientX, sy: e.clientY, px: e.clientX, py: e.clientY };
2887
- const downSx = e.clientX, downSy = e.clientY;
2888
- const r0 = this.canvas.getBoundingClientRect();
2889
- const localSx = downSx - r0.left, localSy = downSy - r0.top;
2890
- this.lpTimer = setTimeout(() => {
2891
- this.lpTimer = null;
2892
- this.fireContextMenu(localSx, localSy, downSx, downSy);
2893
- }, this.lpDelayMs);
2974
+ if (e.pointerType !== "mouse") {
2975
+ this.lpStart = { sx: e.clientX, sy: e.clientY, px: e.clientX, py: e.clientY };
2976
+ const downSx = e.clientX, downSy = e.clientY;
2977
+ const r0 = this.canvas.getBoundingClientRect();
2978
+ const localSx = downSx - r0.left, localSy = downSy - r0.top;
2979
+ this.lpTimer = setTimeout(() => {
2980
+ this.lpTimer = null;
2981
+ this.fireContextMenu(localSx, localSy, downSx, downSy);
2982
+ }, this.lpDelayMs);
2983
+ }
2894
2984
  this.canvas.focus();
2895
2985
  const [sx, sy] = localXY(e);
2896
2986
  if (this.ovHit(sx, sy)) {
@@ -2960,13 +3050,13 @@ var SSDiagram = (() => {
2960
3050
  if (node !== null) {
2961
3051
  const add = e.shiftKey || e.ctrlKey || e.metaKey;
2962
3052
  if (this.permissions.select) {
2963
- if (add) {
3053
+ if (add && e.button === 0) {
2964
3054
  this.toggleSelect(node);
2965
3055
  return;
2966
3056
  }
2967
3057
  if (!this.selectedNodes.has(node)) this.selectNode(node);
2968
3058
  }
2969
- if (this.permissions.moveNodes) {
3059
+ if (this.permissions.moveNodes && e.button === 0) {
2970
3060
  this.dragNode = node;
2971
3061
  this.dragAnchor = { wx, wy };
2972
3062
  const moving = this.selectedNodes.has(node) ? [...this.selectedNodes] : [node];
@@ -2981,6 +3071,7 @@ var SSDiagram = (() => {
2981
3071
  if (this.permissions.select) this.selectLink(link);
2982
3072
  return;
2983
3073
  }
3074
+ if (e.button !== 0 && e.button !== 1) return;
2984
3075
  const wantPan = !this.permissions.select || e.button === 1 || e.ctrlKey || e.altKey || e.pointerType === "touch";
2985
3076
  if (wantPan) {
2986
3077
  if (this.permissions.select && !e.shiftKey) this.selectNode(null);
@@ -3104,7 +3195,10 @@ var SSDiagram = (() => {
3104
3195
  this.relinkCandidate = null;
3105
3196
  if (this.dragNode !== null) {
3106
3197
  const moves = this.dragStart.map((it) => ({ id: it.n.id, fromX: it.x, fromY: it.y, toX: it.n.x, toY: it.n.y })).filter((m) => m.fromX !== m.toX || m.fromY !== m.toY);
3107
- for (const it of this.dragStart) this.emit("nodeMoved", { node: it.n });
3198
+ const moved = new Set(moves.map((m) => m.id));
3199
+ for (const it of this.dragStart) {
3200
+ if (moved.has(it.n.id)) this.emit("nodeMoved", { node: it.n });
3201
+ }
3108
3202
  this.dragNode = null;
3109
3203
  this.dragStart = [];
3110
3204
  if (moves.length > 0) {
@@ -3191,6 +3285,7 @@ var SSDiagram = (() => {
3191
3285
  const [sx, sy] = localXY(e);
3192
3286
  const [wx, wy] = this.toWorld(sx, sy);
3193
3287
  if (this.portAt(wx, wy) !== null) return;
3288
+ if (this.linkAt(wx, wy) !== null) return;
3194
3289
  const node = this.nodeAt(wx, wy);
3195
3290
  if (node !== null) {
3196
3291
  if (this.permissions.inspect && node.openAction.length > 0) {
@@ -3233,6 +3328,8 @@ var SSDiagram = (() => {
3233
3328
  this.linking = null;
3234
3329
  this.relinking = null;
3235
3330
  this.relinkCandidate = null;
3331
+ this.ovDragging = false;
3332
+ this.linkSnap = null;
3236
3333
  }
3237
3334
  }, { passive: false });
3238
3335
  this.listen(this.canvas, "touchmove", (e) => {
@@ -3293,12 +3390,12 @@ var SSDiagram = (() => {
3293
3390
  }
3294
3391
  if (mod && this.permissions.history && e.code === "KeyZ" && !e.shiftKey) {
3295
3392
  e.preventDefault();
3296
- this.undo();
3393
+ this.historyShortcut("undo");
3297
3394
  return;
3298
3395
  }
3299
3396
  if (mod && this.permissions.history && (e.code === "KeyY" || e.code === "KeyZ" && e.shiftKey)) {
3300
3397
  e.preventDefault();
3301
- this.redo();
3398
+ this.historyShortcut("redo");
3302
3399
  return;
3303
3400
  }
3304
3401
  });
@@ -3376,17 +3473,23 @@ var SSDiagram = (() => {
3376
3473
  }
3377
3474
  ctx.restore();
3378
3475
  if (options.overview) this.drawOverview();
3379
- if (options.runtime) this.drawGlobalError();
3476
+ if (options.runtime) this.drawGlobalError(options.transient);
3380
3477
  if (options.transient) this.drawTooltip();
3381
3478
  if (options.transient && (this.introStart !== null || this.globalErrorFlashStart !== null || this.nodes.some((n2) => n2.errorFlashStart !== null)))
3382
3479
  this.scheduleDraw();
3383
3480
  }
3384
- drawGlobalError() {
3481
+ /**
3482
+ * `transient` gates the flash exactly as it gates every other animation, which an export must
3483
+ * not see: it would bake in whatever point of the pulse it caught, so the same export taken
3484
+ * twice differed in opacity. The gate also keeps the export from consuming the animation --
3485
+ * clearing globalErrorFlashStart from an export cut the flash short on screen.
3486
+ */
3487
+ drawGlobalError(transient) {
3385
3488
  const error = this.runtimeState.globalError;
3386
3489
  if (error === null) return;
3387
3490
  const ctx = this.ctx;
3388
3491
  let flashAlpha = 1;
3389
- if (this.globalErrorFlashStart !== null) {
3492
+ if (transient && this.globalErrorFlashStart !== null) {
3390
3493
  const elapsed = performance.now() - this.globalErrorFlashStart;
3391
3494
  if (elapsed >= ERROR_FLASH_MS) {
3392
3495
  this.globalErrorFlashStart = null;
@@ -3569,7 +3672,7 @@ ${runtime.error}`;
3569
3672
  const ctx = this.ctx;
3570
3673
  const hasLoadError = options.runtime && n2.loadError.length > 0;
3571
3674
  const hasRuntimeError = options.runtime && n2.runtimeError.length > 0;
3572
- const runtime = options.runtime ? this.runtimeState.nodes[n2.id] : void 0;
3675
+ const runtime = options.runtime ? getJsonKey(this.runtimeState.nodes, n2.id) : void 0;
3573
3676
  const active = options.runtime && (this.runtimeState.activeNodeId === n2.id || runtime?.active === true);
3574
3677
  roundRect(ctx, n2.x, n2.y, n2.w, n2.h, 6);
3575
3678
  ctx.fillStyle = hasLoadError ? ERROR_BACKGROUND : active ? "#ffd1dc" : n2.color;
@@ -3657,7 +3760,7 @@ ${runtime.error}`;
3657
3760
  }
3658
3761
  }
3659
3762
  portRuntimeState(node, port) {
3660
- return this.runtimeState.nodes[node.id]?.ports[port.direction][port.id] ?? null;
3763
+ return getJsonKey(this.runtimeState.nodes, node.id)?.ports[port.direction][port.id] ?? null;
3661
3764
  }
3662
3765
  // Symmetric jump-over: any segment of THIS link hops the perpendicular
3663
3766
  // segments of links drawn before it (H over earlier V, V over earlier
@@ -4245,6 +4348,9 @@ ${runtime.error}`;
4245
4348
  const id = "group" in item ? item.group : item.command;
4246
4349
  if (SEPARATE_AFTER.has(id) && index < items.length - 1) root.appendChild(this.separator(owner));
4247
4350
  });
4351
+ this.on(root, "contextmenu", (event) => {
4352
+ event.preventDefault();
4353
+ });
4248
4354
  this.container.appendChild(root);
4249
4355
  this.root = root;
4250
4356
  this.place(root, x, y);
@@ -4374,6 +4480,16 @@ ${runtime.error}`;
4374
4480
  this.submenuTriggers.get(this.submenu)?.setAttribute("aria-expanded", "false");
4375
4481
  this.submenu = null;
4376
4482
  }
4483
+ /**
4484
+ * The window the menu actually lives in. A component created from one document and mounted
4485
+ * into another -- a same-origin iframe, a popup -- must be measured and dismissed against
4486
+ * that document's window, not the one this module happened to be evaluated in.
4487
+ */
4488
+ get view() {
4489
+ const owner = this.container.ownerDocument ?? (typeof document === "undefined" ? null : document);
4490
+ const view = owner?.defaultView ?? (typeof window === "undefined" ? null : window);
4491
+ return view ?? null;
4492
+ }
4377
4493
  // Reads the resolved writing direction so the submenu's side and its arrow agree with it.
4378
4494
  rtl() {
4379
4495
  if (typeof getComputedStyle !== "function") return false;
@@ -4385,13 +4501,14 @@ ${runtime.error}`;
4385
4501
  }
4386
4502
  /** Keeps the panel inside the viewport, flipping it left when it would run off the edge. */
4387
4503
  keepOnScreen(panel) {
4388
- if (typeof panel.getBoundingClientRect !== "function" || typeof window === "undefined") return;
4504
+ const view = this.view;
4505
+ if (typeof panel.getBoundingClientRect !== "function" || view === null) return;
4389
4506
  const box = panel.getBoundingClientRect();
4390
- if (box.right > window.innerWidth - 4 || box.left < 4) {
4507
+ if (box.right > view.innerWidth - 4 || box.left < 4) {
4391
4508
  panel.style.insetInlineStart = "auto";
4392
4509
  panel.style.insetInlineEnd = "100%";
4393
4510
  }
4394
- if (box.bottom > window.innerHeight - 4) {
4511
+ if (box.bottom > view.innerHeight - 4) {
4395
4512
  panel.style.top = "auto";
4396
4513
  panel.style.bottom = "-5px";
4397
4514
  }
@@ -4399,10 +4516,11 @@ ${runtime.error}`;
4399
4516
  place(root, x, y) {
4400
4517
  root.style.left = `${x}px`;
4401
4518
  root.style.top = `${y}px`;
4402
- if (typeof root.getBoundingClientRect !== "function" || typeof window === "undefined") return;
4519
+ const view = this.view;
4520
+ if (typeof root.getBoundingClientRect !== "function" || view === null) return;
4403
4521
  const box = root.getBoundingClientRect();
4404
- if (box.right > window.innerWidth - 4) root.style.left = `${Math.max(4, x - box.width)}px`;
4405
- if (box.bottom > window.innerHeight - 4) root.style.top = `${Math.max(4, y - box.height)}px`;
4522
+ if (box.right > view.innerWidth - 4) root.style.left = `${Math.max(4, x - box.width)}px`;
4523
+ if (box.bottom > view.innerHeight - 4) root.style.top = `${Math.max(4, y - box.height)}px`;
4406
4524
  }
4407
4525
  watchForDismissal(owner) {
4408
4526
  const dismiss = () => this.hide();
@@ -4416,10 +4534,11 @@ ${runtime.error}`;
4416
4534
  this.on(owner, "keydown", (event) => {
4417
4535
  if (event.key === "Escape") this.hide();
4418
4536
  });
4419
- if (typeof window !== "undefined") {
4420
- this.on(window, "blur", dismiss);
4421
- this.on(window, "resize", dismiss);
4422
- this.on(window, "scroll", dismiss, true);
4537
+ const view = this.view;
4538
+ if (view !== null) {
4539
+ this.on(view, "blur", dismiss);
4540
+ this.on(view, "resize", dismiss);
4541
+ this.on(view, "scroll", dismiss, true);
4423
4542
  }
4424
4543
  }
4425
4544
  on(target, type, listener, capture = false) {
@@ -4458,7 +4577,8 @@ ${runtime.error}`;
4458
4577
  if (set === void 0) {
4459
4578
  return;
4460
4579
  }
4461
- for (const handler of set) {
4580
+ for (const handler of [...set]) {
4581
+ if (!set.has(handler)) continue;
4462
4582
  try {
4463
4583
  handler(payload);
4464
4584
  } catch (err) {
@@ -4474,11 +4594,23 @@ ${runtime.error}`;
4474
4594
  return (this.handlers.get(event)?.size ?? 0) > 0;
4475
4595
  }
4476
4596
  clearEventHandlers() {
4597
+ for (const set of this.handlers.values()) set.clear();
4477
4598
  this.handlers.clear();
4478
4599
  }
4479
4600
  };
4480
4601
 
4481
4602
  // src/diagram/types.ts
4603
+ function copyJsonValue2(value) {
4604
+ if (Array.isArray(value)) return value.map(copyJsonValue2);
4605
+ if (value !== null && typeof value === "object") return copyJsonObject2(value);
4606
+ return value;
4607
+ }
4608
+ function copyJsonObject2(value) {
4609
+ if (value === void 0) return {};
4610
+ const result = {};
4611
+ for (const [key, item] of Object.entries(value)) setJsonKey(result, key, copyJsonValue2(item));
4612
+ return result;
4613
+ }
4482
4614
  var PortType = class {
4483
4615
  constructor(init) {
4484
4616
  this.name = init.name;
@@ -4516,7 +4648,7 @@ ${runtime.error}`;
4516
4648
  this.id = init.id;
4517
4649
  this.name = init.name;
4518
4650
  this.description = init.description ?? "";
4519
- this.groupName = init.groupName ?? "Common";
4651
+ this.groupName = init.groupName ?? DEFAULT_GROUP_NAME;
4520
4652
  this.inPorts = (init.inPorts ?? []).map((p) => p instanceof Port ? p : new Port(p));
4521
4653
  this.outPorts = (init.outPorts ?? []).map((p) => p instanceof Port ? p : new Port(p));
4522
4654
  this.icon = init.icon ?? "";
@@ -4571,6 +4703,7 @@ ${runtime.error}`;
4571
4703
  });
4572
4704
  }
4573
4705
  };
4706
+ var DEFAULT_GROUP_NAME = "Common";
4574
4707
  var Link = class {
4575
4708
  constructor(init) {
4576
4709
  this.id = init.id ?? "";
@@ -4578,7 +4711,7 @@ ${runtime.error}`;
4578
4711
  this.outPort = init.outPort;
4579
4712
  this.inNode = init.inNode;
4580
4713
  this.inPort = init.inPort;
4581
- this.metadata = init.metadata ?? {};
4714
+ this.metadata = copyJsonObject2(init.metadata);
4582
4715
  }
4583
4716
  };
4584
4717
 
@@ -4634,6 +4767,10 @@ ${runtime.error}`;
4634
4767
  this.fullscreenButton = this.createFullscreenButton();
4635
4768
  this.updateFullscreenButton();
4636
4769
  this.setContextMenuEnabled(options.showContextMenu ?? true);
4770
+ this.canvas.setHistoryShortcutHandler((direction) => {
4771
+ if (direction === "undo") this.undo();
4772
+ else this.redo();
4773
+ });
4637
4774
  this.registerContextActions();
4638
4775
  this.bindCanvasEvents();
4639
4776
  this.disposables.push(this.catalog.on("portTypesChanged", () => this.applySocketTheme()));
@@ -5744,7 +5881,8 @@ ${runtime.error}`;
5744
5881
  }
5745
5882
  };
5746
5883
  function groupNameOf(node) {
5747
- return node.groupName.length > 0 ? node.groupName : "Common";
5884
+ const group = node.groupName;
5885
+ return group.length > 0 && group !== DEFAULT_GROUP_NAME ? group : t("paletteCommonGroup", DEFAULT_GROUP_NAME);
5748
5886
  }
5749
5887
  function normalizeTypeId(typeId) {
5750
5888
  return typeId.toLowerCase();
@@ -5762,12 +5900,43 @@ ${runtime.error}`;
5762
5900
  function iconUrl(name) {
5763
5901
  return name ? `/diagram-icons/${name}.svg` : "";
5764
5902
  }
5765
- function luminance(hex) {
5766
- const m = hex.match(/^#?([0-9a-fA-F]{6})$/);
5767
- if (m === null)
5768
- return 0;
5769
- const n2 = parseInt(m[1], 16);
5770
- return 0.299 * (n2 >> 16 & 255) + 0.587 * (n2 >> 8 & 255) + 0.114 * (n2 & 255);
5903
+ var CSS_COLOR_KEYWORDS = {
5904
+ white: [255, 255, 255],
5905
+ black: [0, 0, 0],
5906
+ transparent: [255, 255, 255]
5907
+ };
5908
+ function luminance(color) {
5909
+ const value = color.trim().toLowerCase();
5910
+ if (value === "")
5911
+ return -1;
5912
+ const keyword = CSS_COLOR_KEYWORDS[value];
5913
+ if (keyword !== void 0)
5914
+ return 0.299 * keyword[0] + 0.587 * keyword[1] + 0.114 * keyword[2];
5915
+ const long = value.match(/^#?([0-9a-f]{6})$/);
5916
+ if (long !== null) {
5917
+ const n2 = parseInt(long[1], 16);
5918
+ return 0.299 * (n2 >> 16 & 255) + 0.587 * (n2 >> 8 & 255) + 0.114 * (n2 & 255);
5919
+ }
5920
+ const short = value.match(/^#?([0-9a-f]{3})([0-9a-f])?$/);
5921
+ if (short !== null) {
5922
+ const [r, g, b] = [...short[1]].map((digit) => parseInt(digit + digit, 16));
5923
+ return 0.299 * r + 0.587 * g + 0.114 * b;
5924
+ }
5925
+ const long8 = value.match(/^#?([0-9a-f]{6})[0-9a-f]{2}$/);
5926
+ if (long8 !== null) {
5927
+ const n2 = parseInt(long8[1], 16);
5928
+ return 0.299 * (n2 >> 16 & 255) + 0.587 * (n2 >> 8 & 255) + 0.114 * (n2 & 255);
5929
+ }
5930
+ const rgb = value.match(/^rgba?\(([^)]+)\)$/);
5931
+ if (rgb !== null) {
5932
+ const parts = rgb[1].split(/[\s,/]+/).filter((part) => part !== "");
5933
+ if (parts.length >= 3) {
5934
+ const [r, g, b] = parts.slice(0, 3).map((part) => part.endsWith("%") ? parseFloat(part) / 100 * 255 : parseFloat(part));
5935
+ if ([r, g, b].every((channel) => Number.isFinite(channel)))
5936
+ return 0.299 * r + 0.587 * g + 0.114 * b;
5937
+ }
5938
+ }
5939
+ return -1;
5771
5940
  }
5772
5941
  function makePort(p) {
5773
5942
  return new Port({
@@ -6022,6 +6191,7 @@ ${runtime.error}`;
6022
6191
  diagram.setTheme({
6023
6192
  diagramBackground: bg,
6024
6193
  gridColor,
6194
+ // Unreadable colours keep the dark default rather than being treated as black.
6025
6195
  linkMaxLightness: luminance(bg) > 140 ? 0.42 : 1
6026
6196
  });
6027
6197
  };
@@ -6182,16 +6352,25 @@ ${runtime.error}`;
6182
6352
  root.querySelectorAll(".ss-diagram-host").forEach((host) => {
6183
6353
  if (host.dataset.rendered === "1")
6184
6354
  return;
6355
+ const started = (run) => {
6356
+ host.dataset.rendered = "1";
6357
+ void run.then(
6358
+ (handle) => {
6359
+ if (handle === null) delete host.dataset.rendered;
6360
+ },
6361
+ () => {
6362
+ delete host.dataset.rendered;
6363
+ }
6364
+ );
6365
+ };
6185
6366
  const src = host.dataset.diagramSrc;
6186
6367
  if (src) {
6187
- host.dataset.rendered = "1";
6188
- void renderFromSource(host, PALETTE_URL, src, options);
6368
+ started(renderFromSource(host, PALETTE_URL, src, options));
6189
6369
  return;
6190
6370
  }
6191
6371
  const inline = host.querySelector('script[type="application/json"]');
6192
6372
  if (inline) {
6193
- host.dataset.rendered = "1";
6194
- void renderFromInline(host, PALETTE_URL, inline.textContent ?? "", options);
6373
+ started(renderFromInline(host, PALETTE_URL, inline.textContent ?? "", options));
6195
6374
  }
6196
6375
  });
6197
6376
  }