@stocksharp/diagram 0.4.1 → 0.6.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 (45) hide show
  1. package/README.md +105 -2
  2. package/dist/esm/canvas-renderer.js +116 -36
  3. package/dist/esm/canvas-renderer.js.map +1 -1
  4. package/dist/esm/diagram/context-menu.js +319 -0
  5. package/dist/esm/diagram/context-menu.js.map +1 -0
  6. package/dist/esm/diagram/stocksharp-diagram.js +121 -9
  7. package/dist/esm/diagram/stocksharp-diagram.js.map +1 -1
  8. package/dist/esm/draw-surface.js +2 -0
  9. package/dist/esm/draw-surface.js.map +1 -0
  10. package/dist/esm/embed.js +7 -4
  11. package/dist/esm/embed.js.map +1 -1
  12. package/dist/esm/i18n.js +15 -4
  13. package/dist/esm/i18n.js.map +1 -1
  14. package/dist/esm/index.js.map +1 -1
  15. package/dist/esm/svg-surface.js +341 -0
  16. package/dist/esm/svg-surface.js.map +1 -0
  17. package/dist/ssdiagram.js +978 -188
  18. package/dist/ssdiagram.js.map +4 -4
  19. package/dist/types/canvas-renderer.d.ts +13 -0
  20. package/dist/types/canvas-renderer.d.ts.map +1 -1
  21. package/dist/types/diagram/api.d.ts +42 -2
  22. package/dist/types/diagram/api.d.ts.map +1 -1
  23. package/dist/types/diagram/context-menu.d.ts +33 -0
  24. package/dist/types/diagram/context-menu.d.ts.map +1 -0
  25. package/dist/types/diagram/stocksharp-diagram.d.ts +18 -5
  26. package/dist/types/diagram/stocksharp-diagram.d.ts.map +1 -1
  27. package/dist/types/draw-surface.d.ts +52 -0
  28. package/dist/types/draw-surface.d.ts.map +1 -0
  29. package/dist/types/embed.d.ts.map +1 -1
  30. package/dist/types/i18n.d.ts +12 -0
  31. package/dist/types/i18n.d.ts.map +1 -1
  32. package/dist/types/index.d.ts +2 -1
  33. package/dist/types/index.d.ts.map +1 -1
  34. package/dist/types/svg-surface.d.ts +74 -0
  35. package/dist/types/svg-surface.d.ts.map +1 -0
  36. package/package.json +1 -1
  37. package/src/canvas-renderer.ts +120 -27
  38. package/src/diagram/api.ts +53 -2
  39. package/src/diagram/context-menu.ts +363 -0
  40. package/src/diagram/stocksharp-diagram.ts +134 -10
  41. package/src/draw-surface.ts +61 -0
  42. package/src/embed.ts +7 -4
  43. package/src/i18n.ts +41 -6
  44. package/src/index.ts +10 -0
  45. package/src/svg-surface.ts +430 -0
package/dist/ssdiagram.js CHANGED
@@ -556,8 +556,8 @@ var SSDiagram = (() => {
556
556
  this.redoCommands.length = 0;
557
557
  this.notify();
558
558
  }
559
- transaction(label, action) {
560
- const frame = { label, commands: [] };
559
+ transaction(label2, action) {
560
+ const frame = { label: label2, commands: [] };
561
561
  this.transactions.push(frame);
562
562
  try {
563
563
  const result = action();
@@ -788,6 +788,326 @@ var SSDiagram = (() => {
788
788
  return value;
789
789
  }
790
790
 
791
+ // src/svg-surface.ts
792
+ var IDENTITY = [1, 0, 0, 1, 0, 0];
793
+ function multiply(m, n2) {
794
+ return [
795
+ m[0] * n2[0] + m[2] * n2[1],
796
+ m[1] * n2[0] + m[3] * n2[1],
797
+ m[0] * n2[2] + m[2] * n2[3],
798
+ m[1] * n2[2] + m[3] * n2[3],
799
+ m[0] * n2[4] + m[2] * n2[5] + m[4],
800
+ m[1] * n2[4] + m[3] * n2[5] + m[5]
801
+ ];
802
+ }
803
+ function isIdentity(m) {
804
+ return m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1 && m[4] === 0 && m[5] === 0;
805
+ }
806
+ function n(value) {
807
+ if (!Number.isFinite(value)) return "0";
808
+ return (Math.round(value * 1e3) / 1e3).toString();
809
+ }
810
+ function escapeXml(value) {
811
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
812
+ }
813
+ var ANCHOR = {
814
+ left: "start",
815
+ start: "start",
816
+ center: "middle",
817
+ right: "end",
818
+ end: "end"
819
+ };
820
+ var BASELINE = {
821
+ top: "text-before-edge",
822
+ hanging: "hanging",
823
+ middle: "central",
824
+ alphabetic: "alphabetic",
825
+ ideographic: "ideographic",
826
+ bottom: "text-after-edge"
827
+ };
828
+ var SvgSurface = class {
829
+ constructor(options) {
830
+ this.body = [];
831
+ this.defs = [];
832
+ this.stack = [];
833
+ this.state = {
834
+ fillStyle: "#000000",
835
+ strokeStyle: "#000000",
836
+ lineWidth: 1,
837
+ lineJoin: "miter",
838
+ lineCap: "butt",
839
+ globalAlpha: 1,
840
+ font: "10px sans-serif",
841
+ textAlign: "start",
842
+ textBaseline: "alphabetic",
843
+ dash: [],
844
+ matrix: IDENTITY,
845
+ clipId: null
846
+ };
847
+ /** Path being built by moveTo/lineTo/arc/arcTo, in the units the caller passes. */
848
+ this.path = [];
849
+ this.cursor = null;
850
+ this.subpathStart = null;
851
+ this.clipSeq = 0;
852
+ this.width = options.width;
853
+ this.height = options.height;
854
+ this.background = options.background;
855
+ this.metrics = options.metrics ?? null;
856
+ }
857
+ // ===== state =========================================================
858
+ get fillStyle() {
859
+ return this.state.fillStyle;
860
+ }
861
+ set fillStyle(value) {
862
+ if (typeof value === "string") this.state.fillStyle = value;
863
+ }
864
+ get strokeStyle() {
865
+ return this.state.strokeStyle;
866
+ }
867
+ set strokeStyle(value) {
868
+ if (typeof value === "string") this.state.strokeStyle = value;
869
+ }
870
+ get lineWidth() {
871
+ return this.state.lineWidth;
872
+ }
873
+ set lineWidth(value) {
874
+ this.state.lineWidth = value;
875
+ }
876
+ get lineJoin() {
877
+ return this.state.lineJoin;
878
+ }
879
+ set lineJoin(value) {
880
+ this.state.lineJoin = value;
881
+ }
882
+ get lineCap() {
883
+ return this.state.lineCap;
884
+ }
885
+ set lineCap(value) {
886
+ this.state.lineCap = value;
887
+ }
888
+ get globalAlpha() {
889
+ return this.state.globalAlpha;
890
+ }
891
+ set globalAlpha(value) {
892
+ this.state.globalAlpha = value;
893
+ }
894
+ get font() {
895
+ return this.state.font;
896
+ }
897
+ set font(value) {
898
+ this.state.font = value;
899
+ if (this.metrics !== null) this.metrics.font = value;
900
+ }
901
+ get textAlign() {
902
+ return this.state.textAlign;
903
+ }
904
+ set textAlign(value) {
905
+ this.state.textAlign = value;
906
+ }
907
+ get textBaseline() {
908
+ return this.state.textBaseline;
909
+ }
910
+ set textBaseline(value) {
911
+ this.state.textBaseline = value;
912
+ }
913
+ save() {
914
+ this.stack.push({ ...this.state, dash: [...this.state.dash], matrix: [...this.state.matrix] });
915
+ }
916
+ restore() {
917
+ const previous = this.stack.pop();
918
+ if (previous !== void 0) this.state = previous;
919
+ }
920
+ setLineDash(segments) {
921
+ this.state.dash = [...segments];
922
+ }
923
+ // ===== transform =====================================================
924
+ setTransform(a, b, c, d, e, f) {
925
+ this.state.matrix = [a, b, c, d, e, f];
926
+ }
927
+ translate(x, y) {
928
+ this.state.matrix = multiply(this.state.matrix, [1, 0, 0, 1, x, y]);
929
+ }
930
+ rotate(angle) {
931
+ const cos = Math.cos(angle);
932
+ const sin = Math.sin(angle);
933
+ this.state.matrix = multiply(this.state.matrix, [cos, sin, -sin, cos, 0, 0]);
934
+ }
935
+ // ===== paths =========================================================
936
+ beginPath() {
937
+ this.path = [];
938
+ this.cursor = null;
939
+ this.subpathStart = null;
940
+ }
941
+ closePath() {
942
+ if (this.cursor === null) return;
943
+ this.path.push("Z");
944
+ if (this.subpathStart !== null) this.cursor = [...this.subpathStart];
945
+ }
946
+ moveTo(x, y) {
947
+ this.path.push(`M${n(x)} ${n(y)}`);
948
+ this.cursor = [x, y];
949
+ this.subpathStart = [x, y];
950
+ }
951
+ lineTo(x, y) {
952
+ if (this.cursor === null) {
953
+ this.moveTo(x, y);
954
+ return;
955
+ }
956
+ this.path.push(`L${n(x)} ${n(y)}`);
957
+ this.cursor = [x, y];
958
+ }
959
+ arc(cx, cy, r, start, end, ccw = false) {
960
+ const sx = cx + r * Math.cos(start);
961
+ const sy = cy + r * Math.sin(start);
962
+ if (this.cursor === null) this.moveTo(sx, sy);
963
+ else this.lineTo(sx, sy);
964
+ let delta = end - start;
965
+ if (ccw) {
966
+ while (delta > 0) delta -= Math.PI * 2;
967
+ if (delta <= -Math.PI * 2) delta = -Math.PI * 2;
968
+ } else {
969
+ while (delta < 0) delta += Math.PI * 2;
970
+ if (delta >= Math.PI * 2) delta = Math.PI * 2;
971
+ }
972
+ const sweep = ccw ? 0 : 1;
973
+ if (Math.abs(delta) >= Math.PI * 2 - 1e-9) {
974
+ const mid = start + delta / 2;
975
+ const mx = cx + r * Math.cos(mid);
976
+ const my = cy + r * Math.sin(mid);
977
+ this.path.push(`A${n(r)} ${n(r)} 0 0 ${sweep} ${n(mx)} ${n(my)}`);
978
+ this.path.push(`A${n(r)} ${n(r)} 0 0 ${sweep} ${n(sx)} ${n(sy)}`);
979
+ this.cursor = [sx, sy];
980
+ return;
981
+ }
982
+ const ex = cx + r * Math.cos(start + delta);
983
+ const ey = cy + r * Math.sin(start + delta);
984
+ const large = Math.abs(delta) > Math.PI ? 1 : 0;
985
+ this.path.push(`A${n(r)} ${n(r)} 0 ${large} ${sweep} ${n(ex)} ${n(ey)}`);
986
+ this.cursor = [ex, ey];
987
+ }
988
+ arcTo(x1, y1, x2, y2, r) {
989
+ if (this.cursor === null) {
990
+ this.moveTo(x1, y1);
991
+ return;
992
+ }
993
+ const [x0, y0] = this.cursor;
994
+ const a = [x0 - x1, y0 - y1];
995
+ const b = [x2 - x1, y2 - y1];
996
+ const la = Math.hypot(a[0], a[1]);
997
+ const lb = Math.hypot(b[0], b[1]);
998
+ if (la === 0 || lb === 0 || r === 0) {
999
+ this.lineTo(x1, y1);
1000
+ return;
1001
+ }
1002
+ const ua = [a[0] / la, a[1] / la];
1003
+ const ub = [b[0] / lb, b[1] / lb];
1004
+ const cross = ua[0] * ub[1] - ua[1] * ub[0];
1005
+ if (Math.abs(cross) < 1e-9) {
1006
+ this.lineTo(x1, y1);
1007
+ return;
1008
+ }
1009
+ const angle = Math.acos(Math.max(-1, Math.min(1, ua[0] * ub[0] + ua[1] * ub[1])));
1010
+ const tangent = r / Math.tan(angle / 2);
1011
+ const t2 = Math.min(tangent, la, lb);
1012
+ const radius = t2 * Math.tan(angle / 2);
1013
+ const t1 = [x1 + ua[0] * t2, y1 + ua[1] * t2];
1014
+ const t22 = [x1 + ub[0] * t2, y1 + ub[1] * t2];
1015
+ this.lineTo(t1[0], t1[1]);
1016
+ const sweep = cross < 0 ? 1 : 0;
1017
+ this.path.push(`A${n(radius)} ${n(radius)} 0 0 ${sweep} ${n(t22[0])} ${n(t22[1])}`);
1018
+ this.cursor = [t22[0], t22[1]];
1019
+ }
1020
+ fill() {
1021
+ if (this.path.length === 0) return;
1022
+ this.emit(`<path d="${this.path.join(" ")}" ${this.fillAttrs()}/>`);
1023
+ }
1024
+ stroke() {
1025
+ if (this.path.length === 0) return;
1026
+ this.emit(`<path d="${this.path.join(" ")}" fill="none" ${this.strokeAttrs()}/>`);
1027
+ }
1028
+ clip() {
1029
+ if (this.path.length === 0) return;
1030
+ const id = `clip${++this.clipSeq}`;
1031
+ this.defs.push(`<clipPath id="${id}"><path d="${this.path.join(" ")}"/></clipPath>`);
1032
+ this.state.clipId = id;
1033
+ }
1034
+ // ===== rectangles ====================================================
1035
+ clearRect(x, y, w, h) {
1036
+ void x;
1037
+ void y;
1038
+ void w;
1039
+ void h;
1040
+ }
1041
+ fillRect(x, y, w, h) {
1042
+ this.emit(`<rect x="${n(x)}" y="${n(y)}" width="${n(w)}" height="${n(h)}" ${this.fillAttrs()}/>`);
1043
+ }
1044
+ strokeRect(x, y, w, h) {
1045
+ this.emit(`<rect x="${n(x)}" y="${n(y)}" width="${n(w)}" height="${n(h)}" fill="none" ${this.strokeAttrs()}/>`);
1046
+ }
1047
+ // ===== text ==========================================================
1048
+ fillText(text, x, y) {
1049
+ if (text === "") return;
1050
+ const anchor = ANCHOR[this.state.textAlign] ?? "start";
1051
+ const baseline = BASELINE[this.state.textBaseline] ?? "alphabetic";
1052
+ 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>`
1054
+ );
1055
+ }
1056
+ measureText(text) {
1057
+ if (this.metrics !== null) return { width: this.metrics.measureText(text).width };
1058
+ const size = parseFloat(this.state.font) || 10;
1059
+ return { width: text.length * size * 0.55 };
1060
+ }
1061
+ // ===== images ========================================================
1062
+ drawImage(image, dx, dy, dw, dh) {
1063
+ const href = this.imageHref(image);
1064
+ if (href === null) return;
1065
+ this.emit(`<image x="${n(dx)}" y="${n(dy)}" width="${n(dw)}" height="${n(dh)}" href="${escapeXml(href)}"${this.commonAttrs()}/>`);
1066
+ }
1067
+ imageHref(image) {
1068
+ try {
1069
+ if (typeof HTMLCanvasElement !== "undefined" && image instanceof HTMLCanvasElement) return image.toDataURL();
1070
+ if (typeof HTMLImageElement !== "undefined" && image instanceof HTMLImageElement) return image.src;
1071
+ } catch {
1072
+ return null;
1073
+ }
1074
+ return null;
1075
+ }
1076
+ // ===== output ========================================================
1077
+ /** The finished document. Safe to call more than once; it does not consume the recording. */
1078
+ toSvg() {
1079
+ const background = this.background === void 0 ? "" : `<rect width="100%" height="100%" fill="${escapeXml(this.background)}"/>`;
1080
+ const defs = this.defs.length === 0 ? "" : `<defs>${this.defs.join("")}</defs>`;
1081
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${n(this.width)}" height="${n(this.height)}" viewBox="0 0 ${n(this.width)} ${n(this.height)}">${defs}${background}${this.body.join("")}</svg>`;
1082
+ }
1083
+ // ===== internals =====================================================
1084
+ emit(element) {
1085
+ this.body.push(element);
1086
+ }
1087
+ commonAttrs() {
1088
+ const parts = [];
1089
+ if (!isIdentity(this.state.matrix)) {
1090
+ parts.push(` transform="matrix(${this.state.matrix.map(n).join(" ")})"`);
1091
+ }
1092
+ if (this.state.globalAlpha !== 1) parts.push(` opacity="${n(this.state.globalAlpha)}"`);
1093
+ if (this.state.clipId !== null) parts.push(` clip-path="url(#${this.state.clipId})"`);
1094
+ return parts.join("");
1095
+ }
1096
+ fillAttrs() {
1097
+ return `fill="${escapeXml(this.state.fillStyle)}"${this.commonAttrs()}`;
1098
+ }
1099
+ strokeAttrs() {
1100
+ const parts = [
1101
+ `stroke="${escapeXml(this.state.strokeStyle)}"`,
1102
+ `stroke-width="${n(this.state.lineWidth)}"`
1103
+ ];
1104
+ if (this.state.lineJoin !== "miter") parts.push(`stroke-linejoin="${this.state.lineJoin}"`);
1105
+ if (this.state.lineCap !== "butt") parts.push(`stroke-linecap="${this.state.lineCap}"`);
1106
+ if (this.state.dash.length > 0) parts.push(`stroke-dasharray="${this.state.dash.map(n).join(" ")}"`);
1107
+ return parts.join(" ") + this.commonAttrs();
1108
+ }
1109
+ };
1110
+
791
1111
  // src/canvas-renderer.ts
792
1112
  var SCREEN_RENDER_OPTIONS = {
793
1113
  grid: true,
@@ -1142,7 +1462,7 @@ var SSDiagram = (() => {
1142
1462
  this.scheduleDraw();
1143
1463
  }
1144
1464
  removeDiagramNode(id) {
1145
- const node = this.nodes.find((n) => n.id === id);
1465
+ const node = this.nodes.find((n2) => n2.id === id);
1146
1466
  if (node === void 0) return;
1147
1467
  const snapshot = node.toInit(false);
1148
1468
  const cascaded = this.links.map((link, index) => ({ link, index })).filter(({ link }) => link.from === id || link.to === id).map(({ link, index }) => ({ init: link.toInit(), index }));
@@ -1178,11 +1498,11 @@ var SSDiagram = (() => {
1178
1498
  }
1179
1499
  }
1180
1500
  doRemoveNode(id) {
1181
- const node = this.nodes.find((n) => n.id === id);
1501
+ const node = this.nodes.find((n2) => n2.id === id);
1182
1502
  if (node === void 0) return;
1183
1503
  const removedLinks = this.links.filter((l) => l.from === id || l.to === id);
1184
1504
  this.links = this.links.filter((l) => l.from !== id && l.to !== id);
1185
- this.nodes = this.nodes.filter((n) => n !== node);
1505
+ this.nodes = this.nodes.filter((n2) => n2 !== node);
1186
1506
  let selectionChanged = this.selectedNodes.delete(node);
1187
1507
  if (this.selectedNode === node) {
1188
1508
  const remaining = [...this.selectedNodes];
@@ -1203,7 +1523,7 @@ var SSDiagram = (() => {
1203
1523
  this.scheduleDraw();
1204
1524
  }
1205
1525
  moveNode(id, x, y) {
1206
- const node = this.nodes.find((n) => n.id === id);
1526
+ const node = this.nodes.find((n2) => n2.id === id);
1207
1527
  if (node === void 0) return;
1208
1528
  const fromX = node.x, fromY = node.y;
1209
1529
  this.doMoveNode(id, x, y);
@@ -1242,7 +1562,7 @@ var SSDiagram = (() => {
1242
1562
  return true;
1243
1563
  }
1244
1564
  doMoveNode(id, x, y) {
1245
- const node = this.nodes.find((n) => n.id === id);
1565
+ const node = this.nodes.find((n2) => n2.id === id);
1246
1566
  if (node === void 0) return;
1247
1567
  node.x = x;
1248
1568
  node.y = y;
@@ -1298,8 +1618,8 @@ var SSDiagram = (() => {
1298
1618
  return ok;
1299
1619
  }
1300
1620
  doAddLink(init, validate = true, index = this.links.length) {
1301
- const fn = this.nodes.find((n) => n.id === init.from);
1302
- const tn = this.nodes.find((n) => n.id === init.to);
1621
+ const fn = this.nodes.find((n2) => n2.id === init.from);
1622
+ const tn = this.nodes.find((n2) => n2.id === init.to);
1303
1623
  if (fn === void 0 || tn === void 0) return false;
1304
1624
  const fp = fn.outPorts.find((p) => p.id === init.fromPort);
1305
1625
  const tp = tn.inPorts.find((p) => p.id === init.toPort);
@@ -1402,8 +1722,8 @@ var SSDiagram = (() => {
1402
1722
  // Group multiple mutations into one undo step (paste of N nodes,
1403
1723
  // bulk delete, multi-drag, etc.). Re-entrant: nested calls flatten
1404
1724
  // into the outer batch.
1405
- withTransaction(label, fn) {
1406
- return this.history.transaction(label, fn);
1725
+ withTransaction(label2, fn) {
1726
+ return this.history.transaction(label2, fn);
1407
1727
  }
1408
1728
  record(action) {
1409
1729
  this.history.recordApplied({
@@ -1416,7 +1736,7 @@ var SSDiagram = (() => {
1416
1736
  if (!this.permissions.deleteSelection) return;
1417
1737
  if (this.selectedNodes.size > 0) {
1418
1738
  this.withTransaction("delete selection", () => {
1419
- for (const id of [...this.selectedNodes].map((n) => n.id)) this.removeDiagramNode(id);
1739
+ for (const id of [...this.selectedNodes].map((n2) => n2.id)) this.removeDiagramNode(id);
1420
1740
  });
1421
1741
  this.selectedNodes.clear();
1422
1742
  this.selectedNode = null;
@@ -1595,7 +1915,7 @@ var SSDiagram = (() => {
1595
1915
  this.showNodeMessages = show;
1596
1916
  this.scheduleDraw();
1597
1917
  }
1598
- updateNodeState(nodeId, label, mutate) {
1918
+ updateNodeState(nodeId, label2, mutate) {
1599
1919
  const node = this.findNode(nodeId);
1600
1920
  if (node === void 0) return false;
1601
1921
  const before = node.toInit(false);
@@ -1613,7 +1933,7 @@ var SSDiagram = (() => {
1613
1933
  undo: () => {
1614
1934
  this.applyNodeSnapshot(nodeId, before);
1615
1935
  },
1616
- label
1936
+ label: label2
1617
1937
  });
1618
1938
  return true;
1619
1939
  }
@@ -1763,7 +2083,7 @@ var SSDiagram = (() => {
1763
2083
  }
1764
2084
  // Renderer operations consumed by the high-level component.
1765
2085
  findNode(id) {
1766
- return this.nodes.find((n) => n.id === id);
2086
+ return this.nodes.find((n2) => n2.id === id);
1767
2087
  }
1768
2088
  requestRedraw() {
1769
2089
  this.relayout();
@@ -1789,17 +2109,8 @@ var SSDiagram = (() => {
1789
2109
  copyContext.drawImage(this.canvas, 0, 0);
1790
2110
  return copy;
1791
2111
  }
1792
- const pixelRatio = this.positiveScreenshotNumber(options.pixelRatio ?? this.dpr, "pixelRatio");
1793
- const exportScale = scope === "content" ? this.positiveScreenshotNumber(options.scale ?? 1, "scale") : this.scale;
1794
- const padding = scope === "content" ? this.nonNegativeScreenshotNumber(options.padding ?? 32, "padding") : 0;
1795
- const bounds = scope === "content" ? this.graphBounds() : null;
1796
- const width = scope === "content" ? Math.max(1, Math.ceil(((bounds?.maxX ?? 1) - (bounds?.minX ?? 0)) * exportScale + padding * 2)) : this.width;
1797
- const height = scope === "content" ? Math.max(1, Math.ceil(((bounds?.maxY ?? 1) - (bounds?.minY ?? 0)) * exportScale + padding * 2)) : this.height;
1798
- const pixelWidth = Math.ceil(width * pixelRatio);
1799
- const pixelHeight = Math.ceil(height * pixelRatio);
1800
- if (pixelWidth > 16384 || pixelHeight > 16384 || pixelWidth * pixelHeight > 268435456) {
1801
- throw new RangeError(`ssdiagram: screenshot is too large (${pixelWidth}x${pixelHeight})`);
1802
- }
2112
+ const layout = this.exportLayout(scope, options);
2113
+ const { pixelRatio, exportScale, padding, bounds, width, height, pixelWidth, pixelHeight } = layout;
1803
2114
  const output = document.createElement("canvas");
1804
2115
  output.width = pixelWidth;
1805
2116
  output.height = pixelHeight;
@@ -1849,6 +2160,86 @@ var SSDiagram = (() => {
1849
2160
  }
1850
2161
  return output;
1851
2162
  }
2163
+ // Everything an export needs to know about size and placement, for either output. Shared on
2164
+ // purpose: a raster and a vector export of the same scope must frame the picture identically,
2165
+ // and two copies of this arithmetic would eventually disagree about padding or where the origin
2166
+ // sits. A block comment here would be lifted onto the next public member in the .d.ts.
2167
+ exportLayout(scope, options) {
2168
+ const pixelRatio = this.positiveScreenshotNumber(options.pixelRatio ?? this.dpr, "pixelRatio");
2169
+ const exportScale = scope === "content" ? this.positiveScreenshotNumber(options.scale ?? 1, "scale") : this.scale;
2170
+ const padding = scope === "content" ? this.nonNegativeScreenshotNumber(options.padding ?? 32, "padding") : 0;
2171
+ const bounds = scope === "content" ? this.graphBounds() : null;
2172
+ const width = scope === "content" ? Math.max(1, Math.ceil(((bounds?.maxX ?? 1) - (bounds?.minX ?? 0)) * exportScale + padding * 2)) : this.width;
2173
+ const height = scope === "content" ? Math.max(1, Math.ceil(((bounds?.maxY ?? 1) - (bounds?.minY ?? 0)) * exportScale + padding * 2)) : this.height;
2174
+ const pixelWidth = Math.ceil(width * pixelRatio);
2175
+ const pixelHeight = Math.ceil(height * pixelRatio);
2176
+ if (pixelWidth > 16384 || pixelHeight > 16384 || pixelWidth * pixelHeight > 268435456) {
2177
+ throw new RangeError(`ssdiagram: screenshot is too large (${pixelWidth}x${pixelHeight})`);
2178
+ }
2179
+ return { pixelRatio, exportScale, padding, bounds, width, height, pixelWidth, pixelHeight };
2180
+ }
2181
+ /**
2182
+ * The same picture takeScreenshot produces, as SVG.
2183
+ *
2184
+ * It is the renderer that draws it -- the surface underneath simply records instead of paints --
2185
+ * so the vector output cannot drift from what the screen and the raster export show. Text keeps
2186
+ * the on-screen metrics by measuring through a real 2D context.
2187
+ *
2188
+ * Scale and pixelRatio still frame the picture (they decide the viewBox), but nothing is
2189
+ * resampled: the result is resolution-independent, which is the point of asking for it.
2190
+ */
2191
+ takeSvg(options = {}) {
2192
+ if (this.destroyed) throw new Error("ssdiagram: cannot export a destroyed diagram");
2193
+ const scope = options.scope ?? "viewport";
2194
+ if (scope !== "viewport" && scope !== "content") {
2195
+ throw new RangeError(`ssdiagram: unsupported screenshot scope "${String(scope)}"`);
2196
+ }
2197
+ const { exportScale, padding, bounds, width, height } = this.exportLayout(scope, options);
2198
+ const metrics = this.canvas.getContext("2d");
2199
+ const surface = new SvgSurface({
2200
+ width,
2201
+ height,
2202
+ background: options.background ?? this.opts.background ?? "#1b1b1f",
2203
+ metrics: metrics ?? void 0
2204
+ });
2205
+ const previous = {
2206
+ ctx: this.ctx,
2207
+ width: this.width,
2208
+ height: this.height,
2209
+ dpr: this.dpr,
2210
+ scale: this.scale,
2211
+ offX: this.offX,
2212
+ offY: this.offY,
2213
+ overviewVisible: this.overviewVisible
2214
+ };
2215
+ try {
2216
+ this.ctx = surface;
2217
+ this.width = width;
2218
+ this.height = height;
2219
+ this.dpr = 1;
2220
+ this.scale = exportScale;
2221
+ this.offX = scope === "content" && bounds !== null ? padding - bounds.minX * exportScale : previous.offX;
2222
+ this.offY = scope === "content" && bounds !== null ? padding - bounds.minY * exportScale : previous.offY;
2223
+ this.overviewVisible = options.includeOverview ?? (scope === "viewport" && previous.overviewVisible);
2224
+ this.draw({
2225
+ grid: options.includeGrid ?? true,
2226
+ overview: this.overviewVisible,
2227
+ selection: options.includeSelection ?? scope === "viewport",
2228
+ runtime: options.includeRuntimeState ?? true,
2229
+ transient: false
2230
+ });
2231
+ } finally {
2232
+ this.ctx = previous.ctx;
2233
+ this.width = previous.width;
2234
+ this.height = previous.height;
2235
+ this.dpr = previous.dpr;
2236
+ this.scale = previous.scale;
2237
+ this.offX = previous.offX;
2238
+ this.offY = previous.offY;
2239
+ this.overviewVisible = previous.overviewVisible;
2240
+ }
2241
+ return surface.toSvg();
2242
+ }
1852
2243
  positiveScreenshotNumber(value, name) {
1853
2244
  if (!Number.isFinite(value) || value <= 0) {
1854
2245
  throw new RangeError(`ssdiagram: screenshot ${name} must be a positive finite number`);
@@ -1969,7 +2360,7 @@ var SSDiagram = (() => {
1969
2360
  return this.toScreen(wx, wy);
1970
2361
  }
1971
2362
  selectNodeById(id) {
1972
- const node = id === null ? null : this.nodes.find((n) => n.id === id) ?? null;
2363
+ const node = id === null ? null : this.nodes.find((n2) => n2.id === id) ?? null;
1973
2364
  this.selectNode(node);
1974
2365
  }
1975
2366
  setZoom(scale) {
@@ -2017,25 +2408,25 @@ var SSDiagram = (() => {
2017
2408
  this.opts.typeColors = { ...colors };
2018
2409
  this.scheduleDraw();
2019
2410
  }
2020
- setTheme(t) {
2021
- if (t.background !== void 0) this.opts.background = t.background;
2022
- if (t.gridColor !== void 0) this.opts.gridColor = t.gridColor;
2023
- if (t.linkMaxLightness !== void 0) this.opts.linkMaxLightness = t.linkMaxLightness;
2024
- if (t.typedLinkColors !== void 0) this.opts.typedLinkColors = t.typedLinkColors;
2025
- if (t.overviewBackground !== void 0) this.opts.overviewBackground = t.overviewBackground;
2026
- if (t.overviewBorderColor !== void 0) this.opts.overviewBorderColor = t.overviewBorderColor;
2027
- if (t.overviewViewportColor !== void 0) this.opts.overviewViewportColor = t.overviewViewportColor;
2028
- if (t.overviewViewportFill !== void 0) this.opts.overviewViewportFill = t.overviewViewportFill;
2411
+ setTheme(t2) {
2412
+ if (t2.background !== void 0) this.opts.background = t2.background;
2413
+ if (t2.gridColor !== void 0) this.opts.gridColor = t2.gridColor;
2414
+ if (t2.linkMaxLightness !== void 0) this.opts.linkMaxLightness = t2.linkMaxLightness;
2415
+ if (t2.typedLinkColors !== void 0) this.opts.typedLinkColors = t2.typedLinkColors;
2416
+ if (t2.overviewBackground !== void 0) this.opts.overviewBackground = t2.overviewBackground;
2417
+ if (t2.overviewBorderColor !== void 0) this.opts.overviewBorderColor = t2.overviewBorderColor;
2418
+ if (t2.overviewViewportColor !== void 0) this.opts.overviewViewportColor = t2.overviewViewportColor;
2419
+ if (t2.overviewViewportFill !== void 0) this.opts.overviewViewportFill = t2.overviewViewportFill;
2029
2420
  this.scheduleDraw();
2030
2421
  }
2031
2422
  graphBounds() {
2032
2423
  if (this.nodes.length === 0) return null;
2033
2424
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
2034
- for (const n of this.nodes) {
2035
- minX = Math.min(minX, n.x);
2036
- minY = Math.min(minY, n.y);
2037
- maxX = Math.max(maxX, n.x + n.w);
2038
- maxY = Math.max(maxY, n.y + n.h);
2425
+ for (const n2 of this.nodes) {
2426
+ minX = Math.min(minX, n2.x);
2427
+ minY = Math.min(minY, n2.y);
2428
+ maxX = Math.max(maxX, n2.x + n2.w);
2429
+ maxY = Math.max(maxY, n2.y + n2.h);
2039
2430
  }
2040
2431
  return { minX, minY, maxX, maxY };
2041
2432
  }
@@ -2086,26 +2477,26 @@ var SSDiagram = (() => {
2086
2477
  this.canvas.remove();
2087
2478
  }
2088
2479
  // ---- geometry ---------------------------------------------------
2089
- layoutNode(n) {
2480
+ layoutNode(n2) {
2090
2481
  const ctx = this.ctx;
2091
2482
  ctx.font = "600 12px Segoe UI, Tahoma, sans-serif";
2092
- const titleW = ctx.measureText(n.name).width;
2093
- const rows = Math.max(n.inPorts.length, n.outPorts.length, 1);
2094
- n.w = Math.max(170, Math.ceil(titleW) + 28);
2095
- n.h = Math.max(48, rows * PORT_ROW_H + 16);
2483
+ const titleW = ctx.measureText(n2.name).width;
2484
+ const rows = Math.max(n2.inPorts.length, n2.outPorts.length, 1);
2485
+ n2.w = Math.max(170, Math.ceil(titleW) + 28);
2486
+ n2.h = Math.max(48, rows * PORT_ROW_H + 16);
2096
2487
  const off = PORT_SQ / 2;
2097
2488
  const place = (ports, x) => {
2098
- const startY = n.y + (n.h - ports.length * PORT_ROW_H) / 2 + PORT_ROW_H / 2;
2489
+ const startY = n2.y + (n2.h - ports.length * PORT_ROW_H) / 2 + PORT_ROW_H / 2;
2099
2490
  ports.forEach((p, i) => {
2100
2491
  p.cx = x;
2101
2492
  p.cy = startY + i * PORT_ROW_H;
2102
2493
  });
2103
2494
  };
2104
- place(n.inPorts, n.x - off);
2105
- place(n.outPorts, n.x + n.w + off);
2495
+ place(n2.inPorts, n2.x - off);
2496
+ place(n2.outPorts, n2.x + n2.w + off);
2106
2497
  }
2107
2498
  relayout() {
2108
- for (const n of this.nodes) this.layoutNode(n);
2499
+ for (const n2 of this.nodes) this.layoutNode(n2);
2109
2500
  }
2110
2501
  toScreen(wx, wy) {
2111
2502
  return [wx * this.scale + this.offX, wy * this.scale + this.offY];
@@ -2134,9 +2525,9 @@ var SSDiagram = (() => {
2134
2525
  // ---- hit testing (world coords) ---------------------------------
2135
2526
  portAt(wx, wy) {
2136
2527
  for (let i = this.nodes.length - 1; i >= 0; i -= 1) {
2137
- const n = this.nodes[i];
2138
- for (const p of [...n.inPorts, ...n.outPorts]) {
2139
- if ((wx - p.cx) ** 2 + (wy - p.cy) ** 2 <= (PORT_R + 4) ** 2) return { node: n, port: p };
2528
+ const n2 = this.nodes[i];
2529
+ for (const p of [...n2.inPorts, ...n2.outPorts]) {
2530
+ if ((wx - p.cx) ** 2 + (wy - p.cy) ** 2 <= (PORT_R + 4) ** 2) return { node: n2, port: p };
2140
2531
  }
2141
2532
  }
2142
2533
  return null;
@@ -2157,8 +2548,8 @@ var SSDiagram = (() => {
2157
2548
  }
2158
2549
  nodeAt(wx, wy) {
2159
2550
  for (let i = this.nodes.length - 1; i >= 0; i -= 1) {
2160
- const n = this.nodes[i];
2161
- if (wx >= n.x && wx <= n.x + n.w && wy >= n.y && wy <= n.y + n.h) return n;
2551
+ const n2 = this.nodes[i];
2552
+ if (wx >= n2.x && wx <= n2.x + n2.w && wy >= n2.y && wy <= n2.y + n2.h) return n2;
2162
2553
  }
2163
2554
  return null;
2164
2555
  }
@@ -2167,9 +2558,9 @@ var SSDiagram = (() => {
2167
2558
  obstaclesFor(exclude) {
2168
2559
  const inf = 12;
2169
2560
  const out = [];
2170
- for (const n of this.nodes) {
2171
- if (exclude.has(n.id)) continue;
2172
- out.push([n.x - inf, n.y - inf, n.w + inf * 2, n.h + inf * 2]);
2561
+ for (const n2 of this.nodes) {
2562
+ if (exclude.has(n2.id)) continue;
2563
+ out.push([n2.x - inf, n2.y - inf, n2.w + inf * 2, n2.h + inf * 2]);
2173
2564
  }
2174
2565
  return out;
2175
2566
  }
@@ -2191,7 +2582,7 @@ var SSDiagram = (() => {
2191
2582
  return null;
2192
2583
  }
2193
2584
  endpoint(l, end) {
2194
- const node = this.nodes.find((n) => n.id === (end === "from" ? l.from : l.to));
2585
+ const node = this.nodes.find((n2) => n2.id === (end === "from" ? l.from : l.to));
2195
2586
  if (node === void 0) return null;
2196
2587
  const port = end === "from" ? node.outPorts.find((p) => p.id === l.fromPort) : node.inPorts.find((p) => p.id === l.toPort);
2197
2588
  if (port === void 0) return null;
@@ -2289,39 +2680,39 @@ var SSDiagram = (() => {
2289
2680
  }
2290
2681
  let best = null;
2291
2682
  let bestD = SNAP_PX;
2292
- for (const n of this.nodes) {
2293
- const ports = direction === "in" ? n.inPorts : n.outPorts;
2683
+ for (const n2 of this.nodes) {
2684
+ const ports = direction === "in" ? n2.inPorts : n2.outPorts;
2294
2685
  for (const p of ports) {
2295
- if (!accepts(n, p)) continue;
2686
+ if (!accepts(n2, p)) continue;
2296
2687
  const [sx, sy] = this.toScreen(p.cx, p.cy);
2297
2688
  const dpx = Math.hypot(sx - this.cursor.x, sy - this.cursor.y);
2298
2689
  if (dpx <= bestD) {
2299
2690
  bestD = dpx;
2300
- best = { node: n, port: p };
2691
+ best = { node: n2, port: p };
2301
2692
  }
2302
2693
  }
2303
2694
  }
2304
2695
  return best;
2305
2696
  }
2306
- selectNode(n) {
2697
+ selectNode(n2) {
2307
2698
  this.selectedLink = null;
2308
2699
  this.selectedPort = null;
2309
2700
  this.selectedNodes.clear();
2310
- if (n !== null) this.selectedNodes.add(n);
2311
- this.selectedNode = n;
2312
- this.emit("nodeSelected", { node: n, selected: n !== null });
2701
+ if (n2 !== null) this.selectedNodes.add(n2);
2702
+ this.selectedNode = n2;
2703
+ this.emit("nodeSelected", { node: n2, selected: n2 !== null });
2313
2704
  this.emitSelectionChanged();
2314
2705
  this.scheduleDraw();
2315
2706
  }
2316
- toggleSelect(n) {
2707
+ toggleSelect(n2) {
2317
2708
  this.selectedLink = null;
2318
2709
  this.selectedPort = null;
2319
- if (this.selectedNodes.has(n)) {
2320
- this.selectedNodes.delete(n);
2321
- if (this.selectedNode === n) this.selectedNode = null;
2710
+ if (this.selectedNodes.has(n2)) {
2711
+ this.selectedNodes.delete(n2);
2712
+ if (this.selectedNode === n2) this.selectedNode = null;
2322
2713
  } else {
2323
- this.selectedNodes.add(n);
2324
- this.selectedNode = n;
2714
+ this.selectedNodes.add(n2);
2715
+ this.selectedNode = n2;
2325
2716
  }
2326
2717
  this.emit("nodeSelected", { node: this.selectedNode, selected: this.selectedNodes.size > 0 });
2327
2718
  this.emitSelectionChanged();
@@ -2365,10 +2756,10 @@ var SSDiagram = (() => {
2365
2756
  this.clip = null;
2366
2757
  return null;
2367
2758
  }
2368
- const ids = new Set([...this.selectedNodes].map((n) => n.id));
2759
+ const ids = new Set([...this.selectedNodes].map((n2) => n2.id));
2369
2760
  const all = this.saveDocument();
2370
2761
  this.clip = createDiagramDocument({
2371
- nodes: all.nodes.filter((n) => ids.has(n.id)),
2762
+ nodes: all.nodes.filter((n2) => ids.has(n2.id)),
2372
2763
  links: all.links.filter((link) => ids.has(link.from.nodeId) && ids.has(link.to.nodeId))
2373
2764
  });
2374
2765
  return parseDiagramDocument(this.clip);
@@ -2409,12 +2800,12 @@ var SSDiagram = (() => {
2409
2800
  }
2410
2801
  for (const l of clipboard.links) {
2411
2802
  const f = map.get(l.from.nodeId);
2412
- const t = map.get(l.to.nodeId);
2413
- if (f !== void 0 && t !== void 0)
2803
+ const t2 = map.get(l.to.nodeId);
2804
+ if (f !== void 0 && t2 !== void 0)
2414
2805
  this.addLink({
2415
2806
  from: f,
2416
2807
  fromPort: l.from.portId,
2417
- to: t,
2808
+ to: t2,
2418
2809
  toPort: l.to.portId,
2419
2810
  metadata: l.metadata
2420
2811
  });
@@ -2424,6 +2815,23 @@ var SSDiagram = (() => {
2424
2815
  return added.map((node) => node.id);
2425
2816
  }
2426
2817
  // ---- input ------------------------------------------------------
2818
+ // Ends hover and everything hanging off it: highlight, cursor and the delayed tooltip.
2819
+ clearHover() {
2820
+ if (this.hoverPort !== null) this.emit("portHover", { node: this.hoverPort.node, port: this.hoverPort.port, hovering: false });
2821
+ if (this.hoverNode !== null) this.emit("nodeHover", { node: this.hoverNode, hovering: false });
2822
+ if (this.hoveredLink !== null) this.emit("linkHover", { link: this.hoveredLink, hovering: false });
2823
+ this.hoverPort = null;
2824
+ this.hoverNode = null;
2825
+ this.hoveredLink = null;
2826
+ this.tipTarget = null;
2827
+ this.tipShow = false;
2828
+ if (this.tipTimer !== null) {
2829
+ clearTimeout(this.tipTimer);
2830
+ this.tipTimer = null;
2831
+ }
2832
+ this.canvas.style.cursor = "default";
2833
+ this.scheduleDraw();
2834
+ }
2427
2835
  cancelLongPress() {
2428
2836
  if (this.lpTimer !== null) {
2429
2837
  clearTimeout(this.lpTimer);
@@ -2456,7 +2864,7 @@ var SSDiagram = (() => {
2456
2864
  this.relinking = null;
2457
2865
  this.relinkCandidate = null;
2458
2866
  this.linkSnap = null;
2459
- this.scheduleDraw();
2867
+ this.clearHover();
2460
2868
  this.emit("contextMenu", { x: pageX, y: pageY, link, node, port });
2461
2869
  }
2462
2870
  listen(target, type, listener, options) {
@@ -2718,7 +3126,7 @@ var SSDiagram = (() => {
2718
3126
  const ry1 = Math.max(this.rubber.y0, this.rubber.y);
2719
3127
  this.rubber = null;
2720
3128
  if (rx1 - rx0 > 3 || ry1 - ry0 > 3) {
2721
- const inRect = this.nodes.filter((n) => n.x < rx1 && n.x + n.w > rx0 && n.y < ry1 && n.y + n.h > ry0);
3129
+ const inRect = this.nodes.filter((n2) => n2.x < rx1 && n2.x + n2.w > rx0 && n2.y < ry1 && n2.y + n2.h > ry0);
2722
3130
  const merge = e.shiftKey ? [...this.selectedNodes, ...inRect] : inRect;
2723
3131
  this.setSelection([...new Set(merge)]);
2724
3132
  }
@@ -2778,22 +3186,7 @@ var SSDiagram = (() => {
2778
3186
  this.emitViewChanged(true);
2779
3187
  this.scheduleDraw();
2780
3188
  }, { passive: false });
2781
- this.listen(this.canvas, "pointerleave", () => {
2782
- if (this.hoverPort !== null) this.emit("portHover", { node: this.hoverPort.node, port: this.hoverPort.port, hovering: false });
2783
- if (this.hoverNode !== null) this.emit("nodeHover", { node: this.hoverNode, hovering: false });
2784
- if (this.hoveredLink !== null) this.emit("linkHover", { link: this.hoveredLink, hovering: false });
2785
- this.hoverPort = null;
2786
- this.hoverNode = null;
2787
- this.hoveredLink = null;
2788
- this.tipTarget = null;
2789
- this.tipShow = false;
2790
- if (this.tipTimer !== null) {
2791
- clearTimeout(this.tipTimer);
2792
- this.tipTimer = null;
2793
- }
2794
- this.canvas.style.cursor = "default";
2795
- this.scheduleDraw();
2796
- });
3189
+ this.listen(this.canvas, "pointerleave", () => this.clearHover());
2797
3190
  this.listen(this.canvas, "dblclick", (e) => {
2798
3191
  const [sx, sy] = localXY(e);
2799
3192
  const [wx, wy] = this.toWorld(sx, sy);
@@ -2951,7 +3344,7 @@ var SSDiagram = (() => {
2951
3344
  const a = this.endpoint(l, "from");
2952
3345
  const b = this.endpoint(l, "to");
2953
3346
  if (a === null || b === null) continue;
2954
- const fp = this.nodes.find((n) => n.id === l.from)?.outPorts.find((p) => p.id === l.fromPort);
3347
+ const fp = this.nodes.find((n2) => n2.id === l.from)?.outPorts.find((p) => p.id === l.fromPort);
2955
3348
  const baseColor = (this.opts.typedLinkColors ?? false) && fp !== void 0 ? this.portColor(fp.type) : this.portColor("");
2956
3349
  const state = options.selection && l === this.selectedLink ? "sel" : options.transient && (l === this.hoveredLink || hoveredNode !== null && (l.from === hoveredNode.id || l.to === hoveredNode.id)) ? "hov" : "norm";
2957
3350
  const color = state === "sel" ? this.selectionColor() : state === "hov" ? this.linkHoverColor() : baseColor;
@@ -2967,7 +3360,7 @@ var SSDiagram = (() => {
2967
3360
  }
2968
3361
  }
2969
3362
  if (options.transient && (this.linking !== null || this.relinking !== null)) this.drawPendingLink();
2970
- for (const n of this.nodes) this.drawNode(n, options.selection && this.selectedNodes.has(n), options);
3363
+ for (const n2 of this.nodes) this.drawNode(n2, options.selection && this.selectedNodes.has(n2), options);
2971
3364
  if (options.selection && this.permissions.createLinks && this.relinking === null)
2972
3365
  this.drawSelectedLinkEndpoints();
2973
3366
  if (options.transient && this.rubber !== null) {
@@ -2985,7 +3378,7 @@ var SSDiagram = (() => {
2985
3378
  if (options.overview) this.drawOverview();
2986
3379
  if (options.runtime) this.drawGlobalError();
2987
3380
  if (options.transient) this.drawTooltip();
2988
- if (options.transient && (this.introStart !== null || this.globalErrorFlashStart !== null || this.nodes.some((n) => n.errorFlashStart !== null)))
3381
+ if (options.transient && (this.introStart !== null || this.globalErrorFlashStart !== null || this.nodes.some((n2) => n2.errorFlashStart !== null)))
2989
3382
  this.scheduleDraw();
2990
3383
  }
2991
3384
  drawGlobalError() {
@@ -3049,17 +3442,17 @@ ${runtime.error}`;
3049
3442
  isError = true;
3050
3443
  }
3051
3444
  } else if (this.hoverNode !== null) {
3052
- const n = this.hoverNode;
3053
- const errors = [n.runtimeError, n.loadError].filter((value) => value.length > 0);
3445
+ const n2 = this.hoverNode;
3446
+ const errors = [n2.runtimeError, n2.loadError].filter((value) => value.length > 0);
3054
3447
  if (errors.length > 0) {
3055
3448
  text = errors.join("\n");
3056
3449
  isError = true;
3057
- } else if (this.showNodeMessages && n.message.length > 0) {
3058
- text = n.message;
3059
- } else if (n.description.length > 0) {
3060
- text = n.description;
3450
+ } else if (this.showNodeMessages && n2.message.length > 0) {
3451
+ text = n2.message;
3452
+ } else if (n2.description.length > 0) {
3453
+ text = n2.description;
3061
3454
  } else {
3062
- text = n.typeId && n.typeId !== n.name ? `${n.name} [${n.typeId}]` : n.name;
3455
+ text = n2.typeId && n2.typeId !== n2.name ? `${n2.name} [${n2.typeId}]` : n2.name;
3063
3456
  }
3064
3457
  } else {
3065
3458
  return;
@@ -3172,24 +3565,24 @@ ${runtime.error}`;
3172
3565
  img.src = url;
3173
3566
  return null;
3174
3567
  }
3175
- drawNode(n, selected, options) {
3568
+ drawNode(n2, selected, options) {
3176
3569
  const ctx = this.ctx;
3177
- const hasLoadError = options.runtime && n.loadError.length > 0;
3178
- const hasRuntimeError = options.runtime && n.runtimeError.length > 0;
3179
- const runtime = options.runtime ? this.runtimeState.nodes[n.id] : void 0;
3180
- const active = options.runtime && (this.runtimeState.activeNodeId === n.id || runtime?.active === true);
3181
- roundRect(ctx, n.x, n.y, n.w, n.h, 6);
3182
- ctx.fillStyle = hasLoadError ? ERROR_BACKGROUND : active ? "#ffd1dc" : n.color;
3570
+ const hasLoadError = options.runtime && n2.loadError.length > 0;
3571
+ const hasRuntimeError = options.runtime && n2.runtimeError.length > 0;
3572
+ const runtime = options.runtime ? this.runtimeState.nodes[n2.id] : void 0;
3573
+ const active = options.runtime && (this.runtimeState.activeNodeId === n2.id || runtime?.active === true);
3574
+ roundRect(ctx, n2.x, n2.y, n2.w, n2.h, 6);
3575
+ ctx.fillStyle = hasLoadError ? ERROR_BACKGROUND : active ? "#ffd1dc" : n2.color;
3183
3576
  ctx.fill();
3184
3577
  ctx.lineWidth = selected ? 2 : 1.5;
3185
- ctx.strokeStyle = selected ? this.selectionColor() : n.border;
3578
+ ctx.strokeStyle = selected ? this.selectionColor() : n2.border;
3186
3579
  ctx.stroke();
3187
3580
  if (hasLoadError || hasRuntimeError) {
3188
3581
  let alpha = 1;
3189
- if (options.transient && hasRuntimeError && n.errorFlashStart !== null) {
3190
- const elapsed = performance.now() - n.errorFlashStart;
3582
+ if (options.transient && hasRuntimeError && n2.errorFlashStart !== null) {
3583
+ const elapsed = performance.now() - n2.errorFlashStart;
3191
3584
  if (elapsed >= ERROR_FLASH_MS) {
3192
- n.errorFlashStart = null;
3585
+ n2.errorFlashStart = null;
3193
3586
  } else {
3194
3587
  alpha = 0.12 + 0.88 * (0.5 + 0.5 * Math.sin(
3195
3588
  elapsed / ERROR_FLASH_MS * Math.PI * 6 - Math.PI / 2
@@ -3198,30 +3591,30 @@ ${runtime.error}`;
3198
3591
  }
3199
3592
  ctx.save();
3200
3593
  ctx.globalAlpha *= alpha;
3201
- roundRect(ctx, n.x, n.y, n.w, n.h, 6);
3594
+ roundRect(ctx, n2.x, n2.y, n2.w, n2.h, 6);
3202
3595
  ctx.lineWidth = hasRuntimeError ? 3 : 2;
3203
3596
  ctx.strokeStyle = ERROR_RED;
3204
3597
  ctx.stroke();
3205
3598
  ctx.restore();
3206
3599
  }
3207
3600
  const iconW = 18;
3208
- if (n.icon) {
3209
- const img = this.getIcon(n.icon);
3601
+ if (n2.icon) {
3602
+ const img = this.getIcon(n2.icon);
3210
3603
  if (img) {
3211
3604
  try {
3212
- ctx.drawImage(img, n.x + 8, n.y + (n.h - iconW) / 2, iconW, iconW);
3605
+ ctx.drawImage(img, n2.x + 8, n2.y + (n2.h - iconW) / 2, iconW, iconW);
3213
3606
  } catch {
3214
3607
  }
3215
3608
  }
3216
3609
  }
3217
- const titleShift = n.icon ? iconW + 4 : 0;
3610
+ const titleShift = n2.icon ? iconW + 4 : 0;
3218
3611
  ctx.fillStyle = hasLoadError ? "#ffffff" : "#1b1b1b";
3219
3612
  ctx.font = "600 12px Segoe UI, Tahoma, sans-serif";
3220
3613
  ctx.textAlign = "center";
3221
3614
  ctx.textBaseline = "middle";
3222
- ctx.fillText(n.name, n.x + titleShift + (n.w - titleShift) / 2, n.y + n.h / 2, n.w - titleShift - 14);
3223
- for (const p of n.inPorts) this.drawPort(n, p, options);
3224
- for (const p of n.outPorts) this.drawPort(n, p, options);
3615
+ ctx.fillText(n2.name, n2.x + titleShift + (n2.w - titleShift) / 2, n2.y + n2.h / 2, n2.w - titleShift - 14);
3616
+ for (const p of n2.inPorts) this.drawPort(n2, p, options);
3617
+ for (const p of n2.outPorts) this.drawPort(n2, p, options);
3225
3618
  }
3226
3619
  drawPort(node, p, options) {
3227
3620
  const ctx = this.ctx;
@@ -3319,9 +3712,9 @@ ${runtime.error}`;
3319
3712
  ctx.setLineDash([]);
3320
3713
  }
3321
3714
  drawArrow(pts, color) {
3322
- const n = pts.length;
3323
- const [px, py] = pts[n - 2];
3324
- const [ex, ey] = pts[n - 1];
3715
+ const n2 = pts.length;
3716
+ const [px, py] = pts[n2 - 2];
3717
+ const [ex, ey] = pts[n2 - 1];
3325
3718
  const ang = Math.atan2(ey - py, ex - px);
3326
3719
  const ctx = this.ctx;
3327
3720
  ctx.save();
@@ -3450,12 +3843,12 @@ ${runtime.error}`;
3450
3843
  ctx.lineWidth = 1;
3451
3844
  ctx.stroke();
3452
3845
  ctx.clip();
3453
- for (const n of this.nodes) {
3454
- ctx.fillStyle = n.color;
3455
- ctx.fillRect(mx(n.x), my(n.y), Math.max(2, n.w * s), Math.max(2, n.h * s));
3456
- ctx.strokeStyle = n.border;
3846
+ for (const n2 of this.nodes) {
3847
+ ctx.fillStyle = n2.color;
3848
+ ctx.fillRect(mx(n2.x), my(n2.y), Math.max(2, n2.w * s), Math.max(2, n2.h * s));
3849
+ ctx.strokeStyle = n2.border;
3457
3850
  ctx.lineWidth = 1;
3458
- ctx.strokeRect(mx(n.x), my(n.y), Math.max(2, n.w * s), Math.max(2, n.h * s));
3851
+ ctx.strokeRect(mx(n2.x), my(n2.y), Math.max(2, n2.w * s), Math.max(2, n2.h * s));
3459
3852
  }
3460
3853
  const [vx0, vy0] = this.toWorld(0, 0);
3461
3854
  const [vx1, vy1] = this.toWorld(this.width, this.height);
@@ -3614,11 +4007,11 @@ ${runtime.error}`;
3614
4007
  cols = Math.ceil((maxX - minX) / cell);
3615
4008
  rows = Math.ceil((maxY - minY) / cell);
3616
4009
  }
3617
- const n = cols * rows;
3618
- if (n <= 0 || n > 8e4) return null;
4010
+ const n2 = cols * rows;
4011
+ if (n2 <= 0 || n2 > 8e4) return null;
3619
4012
  const cx = (c) => minX + (c + 0.5) * cell;
3620
4013
  const cy = (r) => minY + (r + 0.5) * cell;
3621
- const blocked = new Uint8Array(n);
4014
+ const blocked = new Uint8Array(n2);
3622
4015
  for (const r of rects) {
3623
4016
  const c0 = Math.max(0, Math.floor((r[0] - minX) / cell));
3624
4017
  const c1 = Math.min(cols - 1, Math.floor((r[0] + r[2] - minX) / cell));
@@ -3635,17 +4028,17 @@ ${runtime.error}`;
3635
4028
  const gi = gr * cols + gc;
3636
4029
  blocked[si] = 0;
3637
4030
  blocked[gi] = 0;
3638
- const g = new Float64Array(n).fill(Infinity);
3639
- const prev = new Int32Array(n).fill(-1);
3640
- const dir = new Int8Array(n).fill(-1);
4031
+ const g = new Float64Array(n2).fill(Infinity);
4032
+ const prev = new Int32Array(n2).fill(-1);
4033
+ const dir = new Int8Array(n2).fill(-1);
3641
4034
  g[si] = 0;
3642
4035
  const heap = [si];
3643
- const f = new Float64Array(n);
4036
+ const f = new Float64Array(n2);
3644
4037
  f[si] = (Math.abs(gc - sc) + Math.abs(gr - sr)) * A_STEP;
3645
4038
  const swap = (i, j) => {
3646
- const t = heap[i];
4039
+ const t2 = heap[i];
3647
4040
  heap[i] = heap[j];
3648
- heap[j] = t;
4041
+ heap[j] = t2;
3649
4042
  };
3650
4043
  const push = (v) => {
3651
4044
  heap.push(v);
@@ -3676,7 +4069,7 @@ ${runtime.error}`;
3676
4069
  }
3677
4070
  return top;
3678
4071
  };
3679
- const seen = new Uint8Array(n);
4072
+ const seen = new Uint8Array(n2);
3680
4073
  while (heap.length > 0) {
3681
4074
  const cur = pop();
3682
4075
  if (cur === gi) break;
@@ -3734,13 +4127,308 @@ ${runtime.error}`;
3734
4127
  const dx = x2 - x1;
3735
4128
  const dy = y2 - y1;
3736
4129
  const len2 = dx * dx + dy * dy;
3737
- let t = len2 === 0 ? 0 : ((px - x1) * dx + (py - y1) * dy) / len2;
3738
- t = clamp(t, 0, 1);
3739
- const cx = x1 + t * dx;
3740
- const cy = y1 + t * dy;
4130
+ let t2 = len2 === 0 ? 0 : ((px - x1) * dx + (py - y1) * dy) / len2;
4131
+ t2 = clamp(t2, 0, 1);
4132
+ const cx = x1 + t2 * dx;
4133
+ const cy = y1 + t2 * dy;
3741
4134
  return Math.hypot(px - cx, py - cy);
3742
4135
  }
3743
4136
 
4137
+ // src/i18n.ts
4138
+ function t(key, fallback) {
4139
+ if (typeof window === "undefined") return fallback;
4140
+ const value = window.__designerI18n?.[key];
4141
+ return value === void 0 || value === "" ? fallback : value;
4142
+ }
4143
+
4144
+ // src/diagram/context-menu.ts
4145
+ var ROOT_CLASS = "ssdiagram-context-menu";
4146
+ var ITEM_CLASS = "ssdiagram-context-menu-item";
4147
+ var SUBMENU_CLASS = "ssdiagram-context-menu-submenu";
4148
+ var SEPARATOR_CLASS = "ssdiagram-context-menu-separator";
4149
+ var LABEL_CLASS = "ssdiagram-context-menu-label";
4150
+ var ARROW_CLASS = "ssdiagram-context-menu-arrow";
4151
+ var CHECK_CLASS = "ssdiagram-context-menu-check";
4152
+ var SEPARATE_AFTER = /* @__PURE__ */ new Set(["redo", "paste", "delete", "overview"]);
4153
+ function label(id) {
4154
+ switch (id) {
4155
+ case "undo":
4156
+ return t("undo", "Undo");
4157
+ case "redo":
4158
+ return t("redo", "Redo");
4159
+ case "cut":
4160
+ return t("cut", "Cut");
4161
+ case "copy":
4162
+ return t("copy", "Copy");
4163
+ case "paste":
4164
+ return t("paste", "Paste");
4165
+ case "open":
4166
+ return t("ctxOpen", "Open");
4167
+ case "delete":
4168
+ return t("ctxDelete", "Delete");
4169
+ case "export":
4170
+ return t("ctxExportAs", "Export as");
4171
+ case "exportDocument":
4172
+ return t("ctxExportDocument", "Scheme");
4173
+ case "exportPng":
4174
+ return t("ctxExportPng", "PNG image");
4175
+ case "exportSvg":
4176
+ return t("ctxExportSvg", "SVG image");
4177
+ case "overview":
4178
+ return t("ctxOverview", "Overview");
4179
+ case "properties":
4180
+ return t("properties", "Properties");
4181
+ case "help":
4182
+ return t("ctxHelp", "Help");
4183
+ }
4184
+ }
4185
+ var PANEL_STYLE = {
4186
+ position: "fixed",
4187
+ zIndex: "2147483000",
4188
+ minWidth: "176px",
4189
+ // Translations run longer than English -- German and Russian routinely half again. Without
4190
+ // a cap the panel just grows, and on a narrow viewport it leaves the screen entirely, which
4191
+ // no amount of repositioning can recover.
4192
+ maxWidth: "min(320px, calc(100vw - 16px))",
4193
+ padding: "4px",
4194
+ border: "1px solid var(--ssdiagram-menu-border, var(--ssdiagram-control-border, var(--t-border, #3a4250)))",
4195
+ borderRadius: "7px",
4196
+ background: "var(--ssdiagram-menu-background, var(--ssdiagram-control-background, var(--t-panel, #181a20)))",
4197
+ color: "var(--ssdiagram-menu-color, var(--ssdiagram-control-color, var(--t-text, #eaecef)))",
4198
+ boxShadow: "0 12px 32px rgba(0, 0, 0, 0.45)",
4199
+ font: 'var(--ssdiagram-menu-font, 12.5px/1.35 system-ui, -apple-system, "Segoe UI", sans-serif)',
4200
+ boxSizing: "border-box",
4201
+ userSelect: "none"
4202
+ };
4203
+ var ITEM_STYLE = {
4204
+ display: "flex",
4205
+ alignItems: "center",
4206
+ gap: "10px",
4207
+ width: "100%",
4208
+ padding: "6px 10px",
4209
+ border: "0",
4210
+ borderRadius: "4px",
4211
+ background: "transparent",
4212
+ color: "inherit",
4213
+ font: "inherit",
4214
+ // Logical, not physical: on an RTL page these follow the writing direction instead of
4215
+ // pinning the text left and truncating from the wrong end.
4216
+ textAlign: "start",
4217
+ overflowWrap: "anywhere",
4218
+ cursor: "pointer",
4219
+ boxSizing: "border-box"
4220
+ };
4221
+ var HOVER_BACKGROUND = "var(--ssdiagram-menu-hover, var(--t-hover, rgba(255, 255, 255, 0.08)))";
4222
+ var DISABLED_COLOR = "var(--ssdiagram-menu-disabled-color, var(--t-text-dim, #848e9c))";
4223
+ var ContextMenuView = class {
4224
+ constructor(options) {
4225
+ this.root = null;
4226
+ this.submenu = null;
4227
+ this.submenuTriggers = /* @__PURE__ */ new Map();
4228
+ this.detach = [];
4229
+ this.container = options.container;
4230
+ this.execute = options.execute;
4231
+ }
4232
+ get isOpen() {
4233
+ return this.root !== null;
4234
+ }
4235
+ show(x, y, items) {
4236
+ this.hide();
4237
+ const owner = this.container.ownerDocument ?? document;
4238
+ const root = this.panel(owner);
4239
+ root.className = ROOT_CLASS;
4240
+ root.setAttribute("role", "menu");
4241
+ const gutter = items.some((item) => !("group" in item) && item.checked !== void 0);
4242
+ items.forEach((item, index) => {
4243
+ if ("group" in item) root.appendChild(this.groupEntry(owner, item, gutter));
4244
+ else root.appendChild(this.commandEntry(owner, item.command, item.enabled, true, item.checked, gutter));
4245
+ const id = "group" in item ? item.group : item.command;
4246
+ if (SEPARATE_AFTER.has(id) && index < items.length - 1) root.appendChild(this.separator(owner));
4247
+ });
4248
+ this.container.appendChild(root);
4249
+ this.root = root;
4250
+ this.place(root, x, y);
4251
+ this.watchForDismissal(owner);
4252
+ }
4253
+ hide() {
4254
+ for (const dispose of this.detach.splice(0)) dispose();
4255
+ this.submenu = null;
4256
+ this.submenuTriggers.clear();
4257
+ this.root?.remove();
4258
+ this.root = null;
4259
+ }
4260
+ destroy() {
4261
+ this.hide();
4262
+ }
4263
+ panel(owner) {
4264
+ const panel = owner.createElement("div");
4265
+ Object.assign(panel.style, PANEL_STYLE);
4266
+ return panel;
4267
+ }
4268
+ separator(owner) {
4269
+ const rule = owner.createElement("div");
4270
+ rule.className = SEPARATOR_CLASS;
4271
+ Object.assign(rule.style, {
4272
+ height: "1px",
4273
+ margin: "4px 6px",
4274
+ background: "var(--ssdiagram-menu-border, var(--t-border, #3a4250))"
4275
+ });
4276
+ return rule;
4277
+ }
4278
+ item(owner, text, enabled, arrow, checked, gutter) {
4279
+ const button = owner.createElement("button");
4280
+ button.type = "button";
4281
+ button.className = ITEM_CLASS;
4282
+ button.setAttribute("role", checked === void 0 ? "menuitem" : "menuitemcheckbox");
4283
+ if (checked !== void 0) button.setAttribute("aria-checked", String(checked));
4284
+ Object.assign(button.style, ITEM_STYLE);
4285
+ if (!enabled) {
4286
+ button.disabled = true;
4287
+ button.style.color = DISABLED_COLOR;
4288
+ button.style.cursor = "default";
4289
+ }
4290
+ if (gutter) {
4291
+ const tick = owner.createElement("span");
4292
+ tick.className = CHECK_CLASS;
4293
+ tick.textContent = checked === true ? "\u2713" : "";
4294
+ tick.setAttribute("aria-hidden", "true");
4295
+ Object.assign(tick.style, { flex: "none", width: "12px", textAlign: "center" });
4296
+ button.appendChild(tick);
4297
+ }
4298
+ const caption = owner.createElement("span");
4299
+ caption.className = LABEL_CLASS;
4300
+ caption.textContent = text;
4301
+ caption.style.flex = "1";
4302
+ button.appendChild(caption);
4303
+ if (arrow) {
4304
+ const chevron = owner.createElement("span");
4305
+ chevron.className = ARROW_CLASS;
4306
+ chevron.textContent = this.rtl() ? "\u2039" : "\u203A";
4307
+ chevron.setAttribute("aria-hidden", "true");
4308
+ Object.assign(chevron.style, { flex: "none", opacity: "0.7", fontSize: "14px" });
4309
+ button.appendChild(chevron);
4310
+ }
4311
+ return button;
4312
+ }
4313
+ // `closesSubmenu` separates the two places an item can live. A top-level item shuts any
4314
+ // open submenu when the pointer arrives, the way menus everywhere behave. An item inside
4315
+ // the submenu must not: reaching it means the pointer has entered that very panel, and
4316
+ // closing it there would pull it out from under the pointer and drop the click on the
4317
+ // canvas behind.
4318
+ commandEntry(owner, command, enabled, closesSubmenu, checked, gutter) {
4319
+ const button = this.item(owner, label(command), enabled, false, checked, gutter);
4320
+ this.on(button, "pointerenter", () => {
4321
+ if (closesSubmenu) this.closeSubmenu();
4322
+ if (enabled) button.style.background = HOVER_BACKGROUND;
4323
+ });
4324
+ this.on(button, "pointerleave", () => {
4325
+ button.style.background = "transparent";
4326
+ });
4327
+ this.on(button, "click", () => {
4328
+ if (!enabled) return;
4329
+ this.hide();
4330
+ this.execute(command);
4331
+ });
4332
+ return button;
4333
+ }
4334
+ groupEntry(owner, group, gutter) {
4335
+ const host = owner.createElement("div");
4336
+ host.style.position = "relative";
4337
+ const button = this.item(owner, label(group.group), group.enabled, true, void 0, gutter);
4338
+ button.setAttribute("aria-haspopup", "menu");
4339
+ button.setAttribute("aria-expanded", "false");
4340
+ host.appendChild(button);
4341
+ const panel = this.panel(owner);
4342
+ panel.className = SUBMENU_CLASS;
4343
+ panel.setAttribute("role", "menu");
4344
+ panel.setAttribute("aria-label", label(group.group));
4345
+ Object.assign(panel.style, { position: "absolute", insetInlineStart: "100%", top: "-5px", display: "none" });
4346
+ const childGutter = group.commands.some((child) => child.checked !== void 0);
4347
+ for (const child of group.commands) {
4348
+ panel.appendChild(this.commandEntry(owner, child.command, child.enabled, false, child.checked, childGutter));
4349
+ }
4350
+ host.appendChild(panel);
4351
+ const open = () => {
4352
+ if (!group.enabled) return;
4353
+ button.style.background = HOVER_BACKGROUND;
4354
+ button.setAttribute("aria-expanded", "true");
4355
+ panel.style.display = "block";
4356
+ this.submenu = panel;
4357
+ this.keepOnScreen(panel);
4358
+ };
4359
+ this.on(button, "pointerenter", open);
4360
+ this.on(button, "click", open);
4361
+ this.on(host, "pointerleave", () => {
4362
+ if (this.submenu !== panel) return;
4363
+ button.style.background = "transparent";
4364
+ button.setAttribute("aria-expanded", "false");
4365
+ panel.style.display = "none";
4366
+ this.submenu = null;
4367
+ });
4368
+ this.submenuTriggers.set(panel, button);
4369
+ return host;
4370
+ }
4371
+ closeSubmenu() {
4372
+ if (this.submenu === null) return;
4373
+ this.submenu.style.display = "none";
4374
+ this.submenuTriggers.get(this.submenu)?.setAttribute("aria-expanded", "false");
4375
+ this.submenu = null;
4376
+ }
4377
+ // Reads the resolved writing direction so the submenu's side and its arrow agree with it.
4378
+ rtl() {
4379
+ if (typeof getComputedStyle !== "function") return false;
4380
+ try {
4381
+ return getComputedStyle(this.container).direction === "rtl";
4382
+ } catch {
4383
+ return false;
4384
+ }
4385
+ }
4386
+ /** Keeps the panel inside the viewport, flipping it left when it would run off the edge. */
4387
+ keepOnScreen(panel) {
4388
+ if (typeof panel.getBoundingClientRect !== "function" || typeof window === "undefined") return;
4389
+ const box = panel.getBoundingClientRect();
4390
+ if (box.right > window.innerWidth - 4 || box.left < 4) {
4391
+ panel.style.insetInlineStart = "auto";
4392
+ panel.style.insetInlineEnd = "100%";
4393
+ }
4394
+ if (box.bottom > window.innerHeight - 4) {
4395
+ panel.style.top = "auto";
4396
+ panel.style.bottom = "-5px";
4397
+ }
4398
+ }
4399
+ place(root, x, y) {
4400
+ root.style.left = `${x}px`;
4401
+ root.style.top = `${y}px`;
4402
+ if (typeof root.getBoundingClientRect !== "function" || typeof window === "undefined") return;
4403
+ 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`;
4406
+ }
4407
+ watchForDismissal(owner) {
4408
+ const dismiss = () => this.hide();
4409
+ const outside = (event) => {
4410
+ const root = this.root;
4411
+ const target = event.target;
4412
+ if (root !== null && target !== null && typeof root.contains === "function" && root.contains(target)) return;
4413
+ this.hide();
4414
+ };
4415
+ this.on(owner, "pointerdown", outside, true);
4416
+ this.on(owner, "keydown", (event) => {
4417
+ if (event.key === "Escape") this.hide();
4418
+ });
4419
+ if (typeof window !== "undefined") {
4420
+ this.on(window, "blur", dismiss);
4421
+ this.on(window, "resize", dismiss);
4422
+ this.on(window, "scroll", dismiss, true);
4423
+ }
4424
+ }
4425
+ on(target, type, listener, capture = false) {
4426
+ if (typeof target.addEventListener !== "function") return;
4427
+ target.addEventListener(type, listener, capture);
4428
+ this.detach.push(() => target.removeEventListener?.(type, listener, capture));
4429
+ }
4430
+ };
4431
+
3744
4432
  // src/diagram/event-emitter.ts
3745
4433
  var EventEmitter = class {
3746
4434
  constructor() {
@@ -3895,6 +4583,22 @@ ${runtime.error}`;
3895
4583
  };
3896
4584
 
3897
4585
  // src/diagram/stocksharp-diagram.ts
4586
+ var CONTEXT_MENU = [
4587
+ "undo",
4588
+ "redo",
4589
+ "cut",
4590
+ "copy",
4591
+ "paste",
4592
+ "open",
4593
+ "delete",
4594
+ { group: "export", commands: ["exportDocument", "exportPng", "exportSvg"] },
4595
+ "overview",
4596
+ "properties",
4597
+ "help"
4598
+ ];
4599
+ var CONTEXT_CHECKED = {
4600
+ overview: (diagram) => diagram.getViewState().overviewVisible
4601
+ };
3898
4602
  var StockSharpDiagram = class extends EventEmitter {
3899
4603
  constructor(options) {
3900
4604
  super();
@@ -3907,8 +4611,12 @@ ${runtime.error}`;
3907
4611
  this.destroyed = false;
3908
4612
  this.fullscreen = false;
3909
4613
  this.fullscreenButtonVisible = true;
3910
- this.fullscreenLabels = { enter: "Enter fullscreen", exit: "Exit fullscreen" };
4614
+ // null means the host never named the button, so its wording comes from the shared bundle.
4615
+ // Keeping the two apart is what lets an explicit label win while an untouched control still
4616
+ // follows the page's language instead of being stuck in English.
4617
+ this.fullscreenLabels = null;
3911
4618
  this.contextActions = new DiagramActionRegistry();
4619
+ this.contextMenu = null;
3912
4620
  this.div = options.div;
3913
4621
  this.catalog = options.catalog;
3914
4622
  this.overviewContainer = options.overviewContainer ?? null;
@@ -3921,10 +4629,11 @@ ${runtime.error}`;
3921
4629
  gridSize: options.gridSize
3922
4630
  });
3923
4631
  this.fullscreenButtonVisible = options.showFullscreenButton ?? true;
3924
- this.fullscreenLabels = options.fullscreenLabels ?? this.fullscreenLabels;
4632
+ this.fullscreenLabels = options.fullscreenLabels ?? null;
3925
4633
  this.prepareFullscreenButtonHost();
3926
4634
  this.fullscreenButton = this.createFullscreenButton();
3927
4635
  this.updateFullscreenButton();
4636
+ this.setContextMenuEnabled(options.showContextMenu ?? true);
3928
4637
  this.registerContextActions();
3929
4638
  this.bindCanvasEvents();
3930
4639
  this.disposables.push(this.catalog.on("portTypesChanged", () => this.applySocketTheme()));
@@ -4084,8 +4793,8 @@ ${runtime.error}`;
4084
4793
  return this.canvas.updateNode(nodeId, { name: value });
4085
4794
  }
4086
4795
  /** Groups host-driven document edits into one undo/redo operation. */
4087
- transaction(label, action) {
4088
- return this.canvas.withTransaction(label, action);
4796
+ transaction(label2, action) {
4797
+ return this.canvas.withTransaction(label2, action);
4089
4798
  }
4090
4799
  setShowNodeMessages(show) {
4091
4800
  this.canvas.setShowNodeMessages(show);
@@ -4134,6 +4843,10 @@ ${runtime.error}`;
4134
4843
  takeScreenshot(options = {}) {
4135
4844
  return this.canvas.takeScreenshot(options);
4136
4845
  }
4846
+ /** The same picture as takeScreenshot, as an SVG document. */
4847
+ takeSvg(options = {}) {
4848
+ return this.canvas.takeSvg(options);
4849
+ }
4137
4850
  getSelection() {
4138
4851
  return this.canvas.getSelection();
4139
4852
  }
@@ -4165,8 +4878,22 @@ ${runtime.error}`;
4165
4878
  this.fullscreenLabels = labels;
4166
4879
  this.updateFullscreenButton();
4167
4880
  }
4881
+ /**
4882
+ * Re-reads the shared bundle for text the control renders itself. Call it after changing
4883
+ * the page language: the context menu is rebuilt on every open and needs nothing, but the
4884
+ * fullscreen button is drawn once and would otherwise keep the old wording.
4885
+ */
4886
+ refreshLabels() {
4887
+ this.updateFullscreenButton();
4888
+ }
4168
4889
  getFullscreenLabels() {
4169
- return { ...this.fullscreenLabels };
4890
+ return { ...this.resolveFullscreenLabels() };
4891
+ }
4892
+ resolveFullscreenLabels() {
4893
+ return this.fullscreenLabels ?? {
4894
+ enter: t("fullscreenEnter", "Enter fullscreen"),
4895
+ exit: t("fullscreenExit", "Exit fullscreen")
4896
+ };
4170
4897
  }
4171
4898
  /** Updates only the control's state after the host changed its own layout. */
4172
4899
  setFullscreenState(fullscreen) {
@@ -4266,11 +4993,44 @@ ${runtime.error}`;
4266
4993
  const fallback = this.canvas.getClipboardDocument();
4267
4994
  return fallback !== null && this.canvas.pasteDocument(fallback).length > 0;
4268
4995
  }
4996
+ /**
4997
+ * Turns the built-in menu on or off. Off leaves `contextMenuRequested` untouched, so a
4998
+ * host that draws its own is only trading one menu for another, never losing the event.
4999
+ */
5000
+ setContextMenuEnabled(enabled) {
5001
+ if (enabled === (this.contextMenu !== null)) return;
5002
+ if (!enabled) {
5003
+ this.contextMenu?.destroy();
5004
+ this.contextMenu = null;
5005
+ return;
5006
+ }
5007
+ this.contextMenu = new ContextMenuView({
5008
+ container: this.div,
5009
+ // Through the public entry point, so the built-in menu is exactly as privileged
5010
+ // as a host's own -- same guards, same contextCommand event.
5011
+ execute: (command) => {
5012
+ this.executeContextCommand(command);
5013
+ }
5014
+ });
5015
+ }
5016
+ isContextMenuEnabled() {
5017
+ return this.contextMenu !== null;
5018
+ }
4269
5019
  getContextCommands() {
4270
- return this.contextActions.states(this.contextActionContext()).map(({ id, enabled }) => ({
4271
- command: id,
4272
- enabled
4273
- }));
5020
+ const context = this.contextActionContext();
5021
+ const state = (command) => {
5022
+ const checked = CONTEXT_CHECKED[command];
5023
+ return {
5024
+ command,
5025
+ enabled: this.contextActions.canExecute(command, context),
5026
+ ...checked === void 0 ? {} : { checked: checked(this) }
5027
+ };
5028
+ };
5029
+ return CONTEXT_MENU.map((entry) => {
5030
+ if (typeof entry === "string") return state(entry);
5031
+ const commands = entry.commands.map(state);
5032
+ return { group: entry.group, enabled: commands.some(({ enabled }) => enabled), commands };
5033
+ });
4274
5034
  }
4275
5035
  executeContextCommand(command) {
4276
5036
  const context = this.contextActionContext();
@@ -4338,6 +5098,8 @@ ${runtime.error}`;
4338
5098
  destroy() {
4339
5099
  if (this.destroyed) return;
4340
5100
  this.destroyed = true;
5101
+ this.contextMenu?.destroy();
5102
+ this.contextMenu = null;
4341
5103
  for (const dispose of this.disposables.splice(0).reverse()) dispose();
4342
5104
  this.canvas.destroy();
4343
5105
  }
@@ -4390,11 +5152,12 @@ ${runtime.error}`;
4390
5152
  }
4391
5153
  updateFullscreenButton() {
4392
5154
  const fullscreen = this.fullscreen;
4393
- const label = fullscreen ? this.fullscreenLabels.exit : this.fullscreenLabels.enter;
5155
+ const labels = this.resolveFullscreenLabels();
5156
+ const label2 = fullscreen ? labels.exit : labels.enter;
4394
5157
  const path = fullscreen ? "M4 9h5V4M20 9h-5V4M4 15h5v5M20 15h-5v5" : "M9 4H4v5M15 4h5v5M9 20H4v-5M15 20h5v-5";
4395
5158
  this.fullscreenButton.className = fullscreen ? "ssdiagram-fullscreen-button is-active" : "ssdiagram-fullscreen-button";
4396
- this.fullscreenButton.title = label;
4397
- this.fullscreenButton.setAttribute("aria-label", label);
5159
+ this.fullscreenButton.title = label2;
5160
+ this.fullscreenButton.setAttribute("aria-label", label2);
4398
5161
  this.fullscreenButton.setAttribute("aria-pressed", String(fullscreen));
4399
5162
  this.fullscreenButton.innerHTML = `<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="${path}"/></svg>`;
4400
5163
  }
@@ -4490,6 +5253,7 @@ ${runtime.error}`;
4490
5253
  }),
4491
5254
  this.canvas.on("undoStackChanged", (state) => this.emit("undoStackChanged", state)),
4492
5255
  this.canvas.on("contextMenu", ({ x, y, node, link, port }) => {
5256
+ const commands = this.getContextCommands();
4493
5257
  this.emit("contextMenuRequested", {
4494
5258
  x,
4495
5259
  y,
@@ -4497,8 +5261,9 @@ ${runtime.error}`;
4497
5261
  link: link === null ? null : this.fromCanvasLink(link),
4498
5262
  port: port === null ? null : this.fromCanvasPort(port.port),
4499
5263
  portDirection: port?.port.direction ?? null,
4500
- commands: this.getContextCommands()
5264
+ commands
4501
5265
  });
5266
+ this.contextMenu?.show(x, y, commands);
4502
5267
  })
4503
5268
  );
4504
5269
  }
@@ -4533,6 +5298,30 @@ ${runtime.error}`;
4533
5298
  canExecute: ({ selection }) => permissions().deleteSelection && (selection.nodeIds.length > 0 || selection.linkIds.length > 0),
4534
5299
  execute: () => this.canvas.deleteSelection()
4535
5300
  },
5301
+ // Export asks rather than does. Where the result goes -- a download, an upload,
5302
+ // a clipboard, a print preview -- is the host's decision, and so is which options
5303
+ // to render with, so the control reports the request and stops there.
5304
+ // Availability follows the diagram, not the selection: all three export the whole
5305
+ // picture, and an empty diagram has none to give. They stay on in read-only mode,
5306
+ // which forbids changing the diagram, not looking at it.
5307
+ exportDocument: {
5308
+ canExecute: ({ document: document2 }) => document2.nodes.length > 0,
5309
+ execute: () => this.emit("exportRequested", { format: "document" })
5310
+ },
5311
+ exportPng: {
5312
+ canExecute: ({ document: document2 }) => document2.nodes.length > 0,
5313
+ execute: () => this.emit("exportRequested", { format: "png" })
5314
+ },
5315
+ exportSvg: {
5316
+ canExecute: ({ document: document2 }) => document2.nodes.length > 0,
5317
+ execute: () => this.emit("exportRequested", { format: "svg" })
5318
+ },
5319
+ // Always available: it shows the overview as readily as it hides it, and an empty
5320
+ // diagram is exactly when someone may want the panel out of the way.
5321
+ overview: {
5322
+ canExecute: () => true,
5323
+ execute: () => this.setOverviewVisible(!this.getViewState().overviewVisible)
5324
+ },
4536
5325
  properties: {
4537
5326
  canExecute: ({ nodes }) => nodes.length > 0,
4538
5327
  execute: ({ nodes }) => this.emit("nodeProperties", { nodes })
@@ -4550,6 +5339,7 @@ ${runtime.error}`;
4550
5339
  const linkIds = new Set(selection.linkIds);
4551
5340
  return {
4552
5341
  selection,
5342
+ document: document2,
4553
5343
  nodes: document2.nodes.filter((node) => nodeIds.has(node.id)).map((node) => this.fromCanvasInit(node)),
4554
5344
  links: document2.links.filter((link) => linkIds.has(link.id)).map((link) => new Link({
4555
5345
  id: link.id,
@@ -4708,8 +5498,8 @@ ${runtime.error}`;
4708
5498
  // imported node ("Element type is missing from the palette"). Node.id
4709
5499
  // keeps its original casing so save/load round-trips unchanged.
4710
5500
  addNodeType(node) {
4711
- const n = (node instanceof Node ? node : new Node(node)).clone();
4712
- this.nodeTypes.set(n.id.toLowerCase(), n);
5501
+ const n2 = (node instanceof Node ? node : new Node(node)).clone();
5502
+ this.nodeTypes.set(n2.id.toLowerCase(), n2);
4713
5503
  this.emitNodeTypesChanged();
4714
5504
  }
4715
5505
  removeNodeType(id) {
@@ -4853,11 +5643,11 @@ ${runtime.error}`;
4853
5643
  const chev = document.createElement("span");
4854
5644
  chev.className = "d-palette-chev";
4855
5645
  chev.setAttribute("aria-hidden", "true");
4856
- const label = document.createElement("span");
4857
- label.className = "d-palette-group-name";
4858
- label.textContent = name;
5646
+ const label2 = document.createElement("span");
5647
+ label2.className = "d-palette-group-name";
5648
+ label2.textContent = name;
4859
5649
  head.appendChild(chev);
4860
- head.appendChild(label);
5650
+ head.appendChild(label2);
4861
5651
  head.addEventListener("click", () => {
4862
5652
  if (this.collapsed.has(name)) this.collapsed.delete(name);
4863
5653
  else this.collapsed.add(name);
@@ -4893,10 +5683,10 @@ ${runtime.error}`;
4893
5683
  placeholder.setAttribute("aria-hidden", "true");
4894
5684
  li.appendChild(placeholder);
4895
5685
  }
4896
- const label = document.createElement("span");
4897
- label.className = "d-palette-label";
4898
- label.textContent = item.name;
4899
- li.appendChild(label);
5686
+ const label2 = document.createElement("span");
5687
+ label2.className = "d-palette-label";
5688
+ label2.textContent = item.name;
5689
+ li.appendChild(label2);
4900
5690
  li.addEventListener("click", () => this.selectNodeType(item.id));
4901
5691
  li.addEventListener("dblclick", (event) => {
4902
5692
  event.preventDefault();
@@ -4976,8 +5766,8 @@ ${runtime.error}`;
4976
5766
  const m = hex.match(/^#?([0-9a-fA-F]{6})$/);
4977
5767
  if (m === null)
4978
5768
  return 0;
4979
- const n = parseInt(m[1], 16);
4980
- return 0.299 * (n >> 16 & 255) + 0.587 * (n >> 8 & 255) + 0.114 * (n & 255);
5769
+ const n2 = parseInt(m[1], 16);
5770
+ return 0.299 * (n2 >> 16 & 255) + 0.587 * (n2 >> 8 & 255) + 0.114 * (n2 & 255);
4981
5771
  }
4982
5772
  function makePort(p) {
4983
5773
  return new Port({
@@ -5018,23 +5808,23 @@ ${runtime.error}`;
5018
5808
  add(usedOut, l.from, l.fromPort);
5019
5809
  add(usedIn, l.to, l.toPort);
5020
5810
  }
5021
- const nodes = scheme.nodes.map((n) => {
5022
- const t = catalog.getNodeType(n.typeId);
5023
- const inPorts = t ? t.inPorts.map(portOf) : [];
5024
- const outPorts = t ? t.outPorts.map(portOf) : [];
5025
- const anchorType = t?.inPorts.find((p) => p.isDynamic)?.type ?? "Any";
5026
- for (const key of usedIn.get(n.id) ?? [])
5811
+ const nodes = scheme.nodes.map((n2) => {
5812
+ const t2 = catalog.getNodeType(n2.typeId);
5813
+ const inPorts = t2 ? t2.inPorts.map(portOf) : [];
5814
+ const outPorts = t2 ? t2.outPorts.map(portOf) : [];
5815
+ const anchorType = t2?.inPorts.find((p) => p.isDynamic)?.type ?? "Any";
5816
+ for (const key of usedIn.get(n2.id) ?? [])
5027
5817
  if (!inPorts.some((p) => p.id === key))
5028
5818
  inPorts.push({ id: key, name: key, type: anchorType, maxLinks: 0 });
5029
- for (const key of usedOut.get(n.id) ?? [])
5819
+ for (const key of usedOut.get(n2.id) ?? [])
5030
5820
  if (!outPorts.some((p) => p.id === key))
5031
5821
  outPorts.push({ id: key, name: key, type: "Any", maxLinks: 0 });
5032
- const data = new DiagramNode({ id: n.id, typeId: n.typeId, name: n.name, x: n.x, y: n.y, inPorts, outPorts });
5033
- if (t?.icon)
5034
- data.icon = t.icon;
5035
- if (!t) {
5822
+ const data = new DiagramNode({ id: n2.id, typeId: n2.typeId, name: n2.name, x: n2.x, y: n2.y, inPorts, outPorts });
5823
+ if (t2?.icon)
5824
+ data.icon = t2.icon;
5825
+ if (!t2) {
5036
5826
  data.isPlaceholder = true;
5037
- nodeErrors[n.id] = missingTypeMessage(n.typeId);
5827
+ nodeErrors[n2.id] = missingTypeMessage(n2.typeId);
5038
5828
  }
5039
5829
  return data;
5040
5830
  });
@@ -5242,7 +6032,7 @@ ${runtime.error}`;
5242
6032
  diagram.setReadOnly(true);
5243
6033
  diagram.setLinkValidator(() => true);
5244
6034
  diagram.setOverviewVisible(false);
5245
- const missingTypeTemplate = div.dataset.diagramMissingElement ?? 'Element type "{typeId}" is missing from the palette.';
6035
+ const missingTypeTemplate = div.dataset.diagramMissingElement ?? t("embedMissingElement", 'Element type "{typeId}" is missing from the palette.');
5246
6036
  const converted = toDiagramNodes(
5247
6037
  scheme,
5248
6038
  catalog,
@@ -5327,9 +6117,9 @@ ${runtime.error}`;
5327
6117
  function errorTexts(div) {
5328
6118
  const parts = (div.dataset.diagramErrors ?? "").split("|");
5329
6119
  return {
5330
- load: parts[0] || "Diagram source could not be loaded.",
5331
- empty: parts[1] || "Diagram is empty or malformed.",
5332
- draw: parts[2] || "Diagram could not be rendered."
6120
+ load: parts[0] || t("embedErrorLoad", "Diagram source could not be loaded."),
6121
+ empty: parts[1] || t("embedErrorEmpty", "Diagram is empty or malformed."),
6122
+ draw: parts[2] || t("embedErrorDraw", "Diagram could not be rendered.")
5333
6123
  };
5334
6124
  }
5335
6125
  function note(div, message, revision) {