@stocksharp/diagram 1.2.0 → 1.2.2

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.
@@ -348,6 +348,69 @@ var SSDiagramHeadless = (() => {
348
348
  }
349
349
  };
350
350
 
351
+ // src/color.ts
352
+ var CSS_COLOR_KEYWORDS = {
353
+ white: [255, 255, 255],
354
+ black: [0, 0, 0],
355
+ transparent: [255, 255, 255]
356
+ };
357
+ function parseRgb(color) {
358
+ const value = color.trim().toLowerCase();
359
+ if (value === "")
360
+ return null;
361
+ const keyword = CSS_COLOR_KEYWORDS[value];
362
+ if (keyword !== void 0)
363
+ return [...keyword];
364
+ const long = value.match(/^#?([0-9a-f]{6})$/);
365
+ if (long !== null) {
366
+ const n2 = parseInt(long[1], 16);
367
+ return [n2 >> 16 & 255, n2 >> 8 & 255, n2 & 255];
368
+ }
369
+ const short = value.match(/^#?([0-9a-f]{3})([0-9a-f])?$/);
370
+ if (short !== null) {
371
+ const [r, g, b] = [...short[1]].map((digit) => parseInt(digit + digit, 16));
372
+ return [r, g, b];
373
+ }
374
+ const long8 = value.match(/^#?([0-9a-f]{6})[0-9a-f]{2}$/);
375
+ if (long8 !== null) {
376
+ const n2 = parseInt(long8[1], 16);
377
+ return [n2 >> 16 & 255, n2 >> 8 & 255, n2 & 255];
378
+ }
379
+ const rgb = value.match(/^rgba?\(([^)]+)\)$/);
380
+ if (rgb !== null) {
381
+ const parts = rgb[1].split(/[\s,/]+/).filter((part) => part !== "");
382
+ if (parts.length >= 3) {
383
+ const [r, g, b] = parts.slice(0, 3).map((part) => part.endsWith("%") ? parseFloat(part) / 100 * 255 : parseFloat(part));
384
+ if ([r, g, b].every((channel) => Number.isFinite(channel)))
385
+ return [r, g, b];
386
+ }
387
+ }
388
+ return null;
389
+ }
390
+ var DARK_TEXT = "#1b1b1b";
391
+ var LIGHT_TEXT = "#f5f5f5";
392
+ function toLinear(channel) {
393
+ const v = channel / 255;
394
+ return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
395
+ }
396
+ function relativeLuminance(color) {
397
+ const rgb = parseRgb(color);
398
+ if (rgb === null)
399
+ return -1;
400
+ const [r, g, b] = rgb;
401
+ return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
402
+ }
403
+ function contrast(a, b) {
404
+ const [lighter, darker] = a > b ? [a, b] : [b, a];
405
+ return (lighter + 0.05) / (darker + 0.05);
406
+ }
407
+ function readableTextOn(fill) {
408
+ const background = relativeLuminance(fill);
409
+ if (background < 0)
410
+ return DARK_TEXT;
411
+ return contrast(background, relativeLuminance(LIGHT_TEXT)) > contrast(background, relativeLuminance(DARK_TEXT)) ? LIGHT_TEXT : DARK_TEXT;
412
+ }
413
+
351
414
  // src/core/json.ts
352
415
  function setJsonKey(target, key, value) {
353
416
  if (key === "__proto__") {
@@ -980,6 +1043,9 @@ var SSDiagramHeadless = (() => {
980
1043
  const normalized = normalizePortType(type);
981
1044
  return normalized === "" || normalized === "*" || normalized === "any" || normalized === "anydata" || normalized === "any data" || normalized === "object" || normalized === "system.object" || normalized.startsWith("system.object,");
982
1045
  }
1046
+ function isGrowAnchor(port) {
1047
+ return port.isDynamic && !port.isSibling;
1048
+ }
983
1049
  function arePortTypesCompatible(fromType, toType, availableTypes) {
984
1050
  if (isWildcardPortType(fromType) || isWildcardPortType(toType)) return true;
985
1051
  const normalizedFrom = normalizePortType(fromType);
@@ -1187,6 +1253,8 @@ var SSDiagramHeadless = (() => {
1187
1253
  this.hoverPort = null;
1188
1254
  this.hoverNode = null;
1189
1255
  this.hoveredLink = null;
1256
+ // Type feeding each wired input, keyed by node and port, rebuilt for every frame.
1257
+ this.portFeed = /* @__PURE__ */ new Map();
1190
1258
  this.tipTimer = null;
1191
1259
  this.tipShow = false;
1192
1260
  this.tipTarget = null;
@@ -1229,8 +1297,20 @@ var SSDiagramHeadless = (() => {
1229
1297
  if (ctx === null) throw new Error("ssdiagram: 2d context unavailable");
1230
1298
  this.ctx = ctx;
1231
1299
  this.resize(this.host.clientWidth || 800, this.host.clientHeight || 480);
1300
+ this.observeHostSize();
1232
1301
  this.bind();
1233
1302
  }
1303
+ /// The control creates the canvas, so it keeps it the size of the element it was mounted in.
1304
+ /// A host that lays its panels out after construction - a docking shell, a tab that opens
1305
+ /// hidden - measures nothing at that point, and the fallback size above would then outlive
1306
+ /// the layout: a strip of the panel that draws nothing and answers no clicks. resize()
1307
+ /// ignores anything smaller than two pixels, so a panel switched away from keeps its view.
1308
+ observeHostSize() {
1309
+ if (typeof ResizeObserver === "undefined") return;
1310
+ const observer = new ResizeObserver(() => this.resize(this.host.clientWidth, this.host.clientHeight));
1311
+ observer.observe(this.host);
1312
+ this.domDisposables.push(() => observer.disconnect());
1313
+ }
1234
1314
  // ---- events -----------------------------------------------------
1235
1315
  on(ev, h) {
1236
1316
  let set = this.handlers.get(ev);
@@ -2397,8 +2477,9 @@ var SSDiagramHeadless = (() => {
2397
2477
  n2.h = Math.max(48, rows * PORT_ROW_H + 16);
2398
2478
  const off = PORT_SQ / 2;
2399
2479
  const place = (ports, x) => {
2400
- const startY = n2.y + (n2.h - ports.length * PORT_ROW_H) / 2 + PORT_ROW_H / 2;
2401
- ports.forEach((p, i) => {
2480
+ const stack = ports.some(isGrowAnchor) ? [...ports.filter((p) => !isGrowAnchor(p)), ...ports.filter(isGrowAnchor)] : ports;
2481
+ const startY = n2.y + (n2.h - stack.length * PORT_ROW_H) / 2 + PORT_ROW_H / 2;
2482
+ stack.forEach((p, i) => {
2402
2483
  p.cx = x;
2403
2484
  p.cy = startY + i * PORT_ROW_H;
2404
2485
  });
@@ -2415,6 +2496,26 @@ var SSDiagramHeadless = (() => {
2415
2496
  toWorld(sx, sy) {
2416
2497
  return [(sx - this.offX) / this.scale, (sy - this.offY) / this.scale];
2417
2498
  }
2499
+ /// Type of the output wired into each connected input. First link wins, which is the one
2500
+ /// drawn first, so a socket accepting several links agrees with the wire on top.
2501
+ feedTypes() {
2502
+ const feed = /* @__PURE__ */ new Map();
2503
+ for (const l of this.links) {
2504
+ const from = this.findNode(l.from)?.outPorts.find((p) => p.id === l.fromPort);
2505
+ if (from === void 0 || isWildcardPortType(from.type)) continue;
2506
+ const to = this.findNode(l.to)?.inPorts.find((p) => p.id === l.toPort);
2507
+ if (to === void 0 || feed.has(to)) continue;
2508
+ feed.set(to, from.type);
2509
+ }
2510
+ return feed;
2511
+ }
2512
+ /// Colour type of a socket: its own, unless it is a wildcard input with something wired in.
2513
+ /// A wildcard promises to take whatever arrives, so once something has arrived the socket
2514
+ /// shows what that is. An untyped socket ('') promises nothing and keeps its neutral fill.
2515
+ portFillType(p) {
2516
+ if (p.direction !== "in" || p.type === "" || !isWildcardPortType(p.type)) return p.type;
2517
+ return this.portFeed.get(p) ?? p.type;
2518
+ }
2418
2519
  portColor(type) {
2419
2520
  const maxL = this.opts.linkMaxLightness;
2420
2521
  const light = maxL !== void 0 && maxL < 0.5;
@@ -2755,10 +2856,36 @@ var SSDiagramHeadless = (() => {
2755
2856
  }
2756
2857
  this.lpStart = null;
2757
2858
  }
2758
- // Hit-test at screen coords and emit a contextMenu event. Cancels
2759
- // any partial drag/rubber/link gesture that may have started once
2760
- // the menu opens we don't want a half-drag racing it.
2859
+ /// Ends a node drag the way releasing the pointer does: the whole multi-node move becomes
2860
+ /// one undo step, and every node that actually moved is announced. Nodes are moved in place
2861
+ /// as the pointer travels, so a caller that clears the drag without coming through here
2862
+ /// leaves them where the drag put them with nothing in the history and the host unaware.
2863
+ commitNodeDrag() {
2864
+ if (this.dragNode === null) return;
2865
+ 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);
2866
+ const moved = new Set(moves.map((m) => m.id));
2867
+ for (const it of this.dragStart) {
2868
+ if (moved.has(it.n.id)) this.emit("nodeMoved", { node: it.n });
2869
+ }
2870
+ this.dragNode = null;
2871
+ this.dragStart = [];
2872
+ if (moves.length > 0) {
2873
+ this.record({
2874
+ do: () => {
2875
+ for (const m of moves) this.doMoveNode(m.id, m.toX, m.toY);
2876
+ },
2877
+ undo: () => {
2878
+ for (const m of moves) this.doMoveNode(m.id, m.fromX, m.fromY);
2879
+ },
2880
+ label: "drag"
2881
+ });
2882
+ }
2883
+ }
2884
+ // Hit-test at screen coords and emit a contextMenu event. Ends whatever gesture was in
2885
+ // flight — once the menu opens we don't want a half-drag racing it — and a node drag ends
2886
+ // properly rather than being dropped, because its nodes have already moved.
2761
2887
  fireContextMenu(sx, sy, pageX, pageY) {
2888
+ this.commitNodeDrag();
2762
2889
  const [wx, wy] = this.toWorld(sx, sy);
2763
2890
  const port = this.portAt(wx, wy);
2764
2891
  const node = port?.node ?? this.nodeAt(wx, wy);
@@ -2775,11 +2902,14 @@ var SSDiagramHeadless = (() => {
2775
2902
  this.dragNode = null;
2776
2903
  this.dragStart = [];
2777
2904
  this.rubber = null;
2778
- this.panning = false;
2779
2905
  this.linking = null;
2780
2906
  this.relinking = null;
2781
2907
  this.relinkCandidate = null;
2782
2908
  this.linkSnap = null;
2909
+ const viewportMoved = this.panning || this.ovDragging;
2910
+ this.panning = false;
2911
+ this.ovDragging = false;
2912
+ if (viewportMoved) this.emitViewChanged(false);
2783
2913
  this.clearHover();
2784
2914
  this.emit("contextMenu", { x: pageX, y: pageY, link, node, port });
2785
2915
  }
@@ -3021,26 +3151,7 @@ var SSDiagramHeadless = (() => {
3021
3151
  const finish = (e) => {
3022
3152
  this.cancelLongPress();
3023
3153
  this.relinkCandidate = null;
3024
- if (this.dragNode !== null) {
3025
- 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);
3026
- const moved = new Set(moves.map((m) => m.id));
3027
- for (const it of this.dragStart) {
3028
- if (moved.has(it.n.id)) this.emit("nodeMoved", { node: it.n });
3029
- }
3030
- this.dragNode = null;
3031
- this.dragStart = [];
3032
- if (moves.length > 0) {
3033
- this.record({
3034
- do: () => {
3035
- for (const m of moves) this.doMoveNode(m.id, m.toX, m.toY);
3036
- },
3037
- undo: () => {
3038
- for (const m of moves) this.doMoveNode(m.id, m.fromX, m.fromY);
3039
- },
3040
- label: "drag"
3041
- });
3042
- }
3043
- }
3154
+ this.commitNodeDrag();
3044
3155
  if (this.rubber !== null) {
3045
3156
  const rx0 = Math.min(this.rubber.x0, this.rubber.x);
3046
3157
  const ry0 = Math.min(this.rubber.y0, this.rubber.y);
@@ -3124,12 +3235,16 @@ var SSDiagramHeadless = (() => {
3124
3235
  }
3125
3236
  this.zoomToFit();
3126
3237
  });
3127
- this.listen(this.canvas, "contextmenu", (e) => {
3238
+ const openContextMenu = (e) => {
3128
3239
  e.preventDefault();
3129
3240
  if (!this.permissions.inspect) return;
3130
3241
  const [sx, sy] = localXY(e);
3131
3242
  this.cancelLongPress();
3132
3243
  this.fireContextMenu(sx, sy, e.clientX, e.clientY);
3244
+ };
3245
+ this.listen(this.canvas, "contextmenu", openContextMenu);
3246
+ this.listen(this.host, "contextmenu", (e) => {
3247
+ if (e.target !== this.canvas) openContextMenu(e);
3133
3248
  });
3134
3249
  let pinchDist = 0;
3135
3250
  let pinchScale = 1;
@@ -3285,6 +3400,7 @@ var SSDiagramHeadless = (() => {
3285
3400
  }
3286
3401
  }
3287
3402
  if (options.transient && (this.linking !== null || this.relinking !== null)) this.drawPendingLink();
3403
+ this.portFeed = this.feedTypes();
3288
3404
  for (const n2 of this.nodes) this.drawNode(n2, options.selection && this.selectedNodes.has(n2), options);
3289
3405
  if (options.selection && this.permissions.createLinks && this.relinking === null)
3290
3406
  this.drawSelectedLinkEndpoints();
@@ -3539,7 +3655,7 @@ ${runtime.error}`;
3539
3655
  }
3540
3656
  }
3541
3657
  const titleShift = n2.icon ? iconW + 4 : 0;
3542
- ctx.fillStyle = hasLoadError ? "#ffffff" : "#1b1b1b";
3658
+ ctx.fillStyle = hasLoadError ? "#ffffff" : readableTextOn(active ? "#ffd1dc" : n2.color);
3543
3659
  ctx.font = "600 12px Segoe UI, Tahoma, sans-serif";
3544
3660
  ctx.textAlign = "center";
3545
3661
  ctx.textBaseline = "middle";
@@ -3562,7 +3678,7 @@ ${runtime.error}`;
3562
3678
  ctx.fill();
3563
3679
  }
3564
3680
  roundRect(ctx, x, y, s, s, r);
3565
- ctx.fillStyle = this.portColor(p.type);
3681
+ ctx.fillStyle = this.portColor(this.portFillType(p));
3566
3682
  ctx.fill();
3567
3683
  ctx.lineWidth = magnet ? 1.5 : 1;
3568
3684
  ctx.strokeStyle = magnet ? "#ffffff" : "rgba(12,12,16,0.55)";