@luxalgo/vela 0.6.16 → 0.6.18

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 (46) hide show
  1. package/README.md +4 -0
  2. package/dist/{DataProvider-XLdJB86x.d.ts → DataProvider-Cb-2lC5O.d.ts} +1 -1
  3. package/dist/{DataProvider-xH2VBcFT.d.cts → DataProvider-UtWw2CgJ.d.cts} +1 -1
  4. package/dist/{chunk-GIJYO4LO.js → chunk-22QJKGXX.js} +666 -189
  5. package/dist/{chunk-SEOO6YG7.js → chunk-BXF7VNNE.js} +181 -111
  6. package/dist/{chunk-6DPAYEGO.js → chunk-CAFCLMPF.js} +1 -0
  7. package/dist/{chunk-I77F2SVE.js → chunk-IFJJPXSV.js} +1 -1
  8. package/dist/{chunk-MGU3USTP.js → chunk-T5Z5YUCF.js} +1 -1
  9. package/dist/{chunk-Q3TFJNJW.js → chunk-XCBJU674.js} +2 -2
  10. package/dist/{contributions-BEkxwA5F.d.cts → contributions-5nz3PLRe.d.ts} +9 -4
  11. package/dist/{contributions-CoAX7_SF.d.ts → contributions-DEPRtkmG.d.cts} +9 -4
  12. package/dist/index.cjs +662 -185
  13. package/dist/index.d.cts +15 -6
  14. package/dist/index.d.ts +15 -6
  15. package/dist/index.js +4 -4
  16. package/dist/{options-bdSAtp7y.d.ts → options-BCRmYALw.d.cts} +24 -5
  17. package/dist/{options-bdSAtp7y.d.cts → options-BCRmYALw.d.ts} +24 -5
  18. package/dist/{plugin-CNUgbRNq.d.ts → plugin-CUtfLeqz.d.ts} +3 -3
  19. package/dist/{plugin-Dxyq35Zf.d.cts → plugin-DVUaROPo.d.cts} +3 -3
  20. package/dist/plugin.cjs +1 -0
  21. package/dist/plugin.d.cts +4 -4
  22. package/dist/plugin.d.ts +4 -4
  23. package/dist/plugin.js +2 -2
  24. package/dist/providers/binance.d.cts +2 -2
  25. package/dist/providers/binance.d.ts +2 -2
  26. package/dist/providers/coinbase.d.cts +2 -2
  27. package/dist/providers/coinbase.d.ts +2 -2
  28. package/dist/providers/hyperliquid.d.cts +2 -2
  29. package/dist/providers/hyperliquid.d.ts +2 -2
  30. package/dist/{statusline-model-CbPza-Wc.d.cts → statusline-model-7ECSDtgt.d.cts} +3 -3
  31. package/dist/{statusline-model-D7cWkw8A.d.ts → statusline-model-D1Fhs8Dk.d.ts} +3 -3
  32. package/dist/ui.cjs +1 -0
  33. package/dist/ui.d.cts +1 -1
  34. package/dist/ui.d.ts +1 -1
  35. package/dist/ui.js +3 -3
  36. package/dist/vela.global.js +662 -185
  37. package/dist/vela.global.min.js +52 -53
  38. package/dist/widget.cjs +839 -291
  39. package/dist/widget.d.cts +39 -12
  40. package/dist/widget.d.ts +39 -12
  41. package/dist/widget.js +7 -7
  42. package/dist/workspace.cjs +839 -291
  43. package/dist/workspace.d.cts +5 -5
  44. package/dist/workspace.d.ts +5 -5
  45. package/dist/workspace.js +6 -6
  46. package/package.json +1 -1
@@ -1,6 +1,23 @@
1
1
  var Vela = (function (exports) {
2
2
  'use strict';
3
3
 
4
+ // src/core/options.ts
5
+ var LIVE_BAR_EASE_DEFAULT_MS = 90;
6
+ var LIVE_BAR_EASE_MAX_MS = 1e3;
7
+ function resolveLiveBarEaseMs(value) {
8
+ if (value === true) return LIVE_BAR_EASE_DEFAULT_MS;
9
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return 0;
10
+ return Math.min(value, LIVE_BAR_EASE_MAX_MS);
11
+ }
12
+ function resolveAnimations(animations) {
13
+ if (typeof animations === "boolean") return { animZoom: animations, animPan: animations, animLiveBar: 0 };
14
+ return {
15
+ animZoom: animations?.zoom ?? true,
16
+ animPan: animations?.pan ?? true,
17
+ animLiveBar: resolveLiveBarEaseMs(animations?.liveBar)
18
+ };
19
+ }
20
+
4
21
  // src/core/model/series.ts
5
22
  function isLineLikeSeries(spec) {
6
23
  return spec.kind !== "candle" && spec.kind !== "bar" && spec.kind !== "markers";
@@ -551,6 +568,7 @@ var Vela = (function (exports) {
551
568
  registerIcon("grip", S('<circle cx="6" cy="3.5" r="1"/><circle cx="10" cy="3.5" r="1"/><circle cx="6" cy="8" r="1"/><circle cx="10" cy="8" r="1"/><circle cx="6" cy="12.5" r="1"/><circle cx="10" cy="12.5" r="1"/>', 'fill="currentColor" stroke="none"'));
552
569
  registerIcon("kebab", S('<circle cx="8" cy="3.2" r="1.2"/><circle cx="8" cy="8" r="1.2"/><circle cx="8" cy="12.8" r="1.2"/>', 'fill="currentColor" stroke="none"'));
553
570
  registerIcon("burger", S('<path d="M2.5 4.5h11M2.5 8h11M2.5 11.5h11"/>'));
571
+ registerIcon("info", S('<path d="M8 7.4v4.4"/><circle cx="8" cy="4.6" r="0.9" fill="currentColor" stroke="none"/>'));
554
572
  registerIcon("reset", S('<path d="M2 8a6 6 0 1 0 6-6 6.5 6.5 0 0 0-4.5 1.83L2 5.33"/><path d="M2 2v3.33h3.33"/>'));
555
573
  registerIcon("pane-collapse", S('<path d="M2.6 9.4h4v4M13.4 6.6h-4v-4"/><path d="m9.4 6.6 4.2-4.2M2.4 13.6l4.2-4.2"/>'));
556
574
  registerIcon("pane-expand", S('<path d="M9.8 2.4h3.8v3.8M6.2 13.6H2.4V9.8"/><path d="m13.6 2.4-4.4 4.4M2.4 13.6l4.4-4.4"/>'));
@@ -6882,15 +6900,18 @@ var Vela = (function (exports) {
6882
6900
  next2 = [...ids];
6883
6901
  }
6884
6902
  this.selectedIds = next2.filter((id) => this.store.has(id));
6903
+ this.announceSelection();
6904
+ }
6905
+ /** Push the selection to the renderer and announce it (primary + the full list). */
6906
+ announceSelection() {
6885
6907
  this.port?.setSelection(this.selectedIds);
6886
- this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null });
6908
+ this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null, ids: [...this.selectedIds] });
6887
6909
  }
6888
6910
  /** Restore a history snapshot, reconciling selection against what survived. */
6889
6911
  restoreSnapshot(doc) {
6890
6912
  this.store.load(doc);
6891
6913
  this.selectedIds = this.selectedIds.filter((id) => this.store.has(id));
6892
- this.port?.setSelection(this.selectedIds);
6893
- this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null });
6914
+ this.announceSelection();
6894
6915
  }
6895
6916
  /** Delete drawings as one undo step; prune them from the selection. */
6896
6917
  deleteMany(ids) {
@@ -7023,6 +7044,9 @@ var Vela = (function (exports) {
7023
7044
  case "duplicate":
7024
7045
  this.duplicate(i.ids);
7025
7046
  break;
7047
+ case "clone":
7048
+ if (this.enabled && i.docs.length) this.insertClones(i.docs);
7049
+ break;
7026
7050
  case "copy":
7027
7051
  this.copy(i.ids);
7028
7052
  break;
@@ -8859,7 +8883,12 @@ var Vela = (function (exports) {
8859
8883
  if (!model) continue;
8860
8884
  const paneId = model.paneId ?? "price";
8861
8885
  if (!byPane.has(paneId)) byPane.set(paneId, []);
8862
- byPane.get(paneId).push({ id: r.id, title: model.title || r.title, ownScale: model.ownScale === true });
8886
+ byPane.get(paneId).push({
8887
+ id: r.id,
8888
+ title: model.title || r.title,
8889
+ ...model.shorttitle ? { shorttitle: model.shorttitle } : {},
8890
+ ownScale: model.ownScale === true
8891
+ });
8863
8892
  }
8864
8893
  const panes = [];
8865
8894
  for (const paneId of this.paneOrder) {
@@ -10501,6 +10530,23 @@ var Vela = (function (exports) {
10501
10530
  clear();
10502
10531
  };
10503
10532
  }
10533
+ function chromeHint(text, opts) {
10534
+ const px = opts.size ?? 16;
10535
+ const el = document.createElement("span");
10536
+ el.className = "vela-hint";
10537
+ el.setAttribute("role", "img");
10538
+ el.setAttribute("aria-label", text);
10539
+ el.innerHTML = iconAt("info", px);
10540
+ el.style.cssText = `flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:${px}px;height:${px}px;border-radius:50%;cursor:help;user-select:none;background:var(--vela-hover);color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;`;
10541
+ const hover = (on) => {
10542
+ el.style.background = on ? "var(--vela-active)" : "var(--vela-hover)";
10543
+ el.style.color = on ? "var(--vela-fg-bright)" : "var(--vela-fg-muted)";
10544
+ };
10545
+ el.addEventListener("pointerenter", () => hover(true));
10546
+ el.addEventListener("pointerleave", () => hover(false));
10547
+ const dispose = attachChromeTooltip(el, { host: opts.host, theme: opts.theme, text: () => text, wrap: true, delayMs: 250 });
10548
+ return { el, dispose };
10549
+ }
10504
10550
 
10505
10551
  // node_modules/@zag-js/vanilla/dist/chunk-QZ7TP4HQ.mjs
10506
10552
  var __defProp = Object.defineProperty;
@@ -21082,14 +21128,11 @@ ${overlayScrollbarCss(".vela-dialog *")}
21082
21128
  buildTextField(id, current, emit) {
21083
21129
  return buildFieldControl({ kind: "text", id, value: current, onChange: emit }).el;
21084
21130
  }
21085
- /** A hoverable ⓘ affordance carrying an input's `tooltip` (themed chrome tip; wraps). */
21131
+ /** The shared ⓘ hint carrying an input's `tooltip` (see {@link chromeHint}). */
21086
21132
  infoButton(tooltip) {
21087
- const b = document.createElement("span");
21088
- b.textContent = "i";
21089
- this.dialogTips.push(attachChromeTooltip(b, { host: this.host.container, theme: () => this.host.theme(), text: () => tooltip, wrap: true, delayMs: 250 }));
21090
- b.className = "vela-ind-hint";
21091
- b.style.cssText = "flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;font-weight:600;font-size:11px;font-family:inherit;line-height:1;cursor:help;user-select:none;";
21092
- return b;
21133
+ const { el, dispose } = chromeHint(tooltip, { host: this.host.container, theme: () => this.host.theme(), size: 18 });
21134
+ this.dialogTips.push(dispose);
21135
+ return el;
21093
21136
  }
21094
21137
  /** A dropdown over plain string options (value === label) — a thin case of {@link selectPairs}. */
21095
21138
  select(id, options, current, emit) {
@@ -21526,8 +21569,6 @@ ${overlayScrollbarCss(".vela-dialog *")}
21526
21569
  ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21527
21570
  .vela-ind-tab{font-weight:600;font-size:13px;line-height:20px;transition:color var(--vela-dur-fast) ease,border-color var(--vela-dur-fast) ease;}
21528
21571
  .vela-ind-tab:not(.vela-ind-tab-active):hover{color:var(--vela-fg-bright);}
21529
- .vela-ind-hint{background:var(--vela-hover);color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
21530
- .vela-ind-hint:hover{background:var(--vela-active);color:var(--vela-fg-bright);}
21531
21572
  .vela-ind-ctl,.vela-ind-close{background-color:transparent;color:inherit;transition:color var(--vela-dur-fast) ease,background-color var(--vela-dur-fast) ease;}
21532
21573
  .vela-ind-ctl svg,.vela-ind-close svg{width:${LEGEND_ICON_PX}px;height:${LEGEND_ICON_PX}px;display:block;flex:none;stroke-width:1;}
21533
21574
  .vela-ind-ctl:hover{background-color:var(--vela-hover-strong);}
@@ -22918,15 +22959,15 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
22918
22959
  if (!hasOwnCandlePaint(style)) return null;
22919
22960
  const bag = bags[style] ?? {};
22920
22961
  const color = (v) => isColor(v) ? v : null;
22921
- const bool = (v) => isBool(v) ? v : null;
22962
+ const bool2 = (v) => isBool(v) ? v : null;
22922
22963
  return {
22923
22964
  upColor: color(bag.candleUpColor),
22924
22965
  downColor: color(bag.candleDownColor),
22925
- bodyVisible: bool(bag.candleBodyVisible),
22926
- borderVisible: bool(bag.candleBorderVisible),
22966
+ bodyVisible: bool2(bag.candleBodyVisible),
22967
+ borderVisible: bool2(bag.candleBorderVisible),
22927
22968
  borderUpColor: color(bag.candleBorderUpColor),
22928
22969
  borderDownColor: color(bag.candleBorderDownColor),
22929
- wickVisible: bool(bag.candleWickVisible),
22970
+ wickVisible: bool2(bag.candleWickVisible),
22930
22971
  wickUpColor: color(bag.candleWickUpColor),
22931
22972
  wickDownColor: color(bag.candleWickDownColor)
22932
22973
  };
@@ -23071,7 +23112,8 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
23071
23112
  labelsVisible: isBool(ps.labelsVisible) ? ps.labelsVisible : base.priceScale.labelsVisible,
23072
23113
  currentPriceLine: isBool(ps.currentPriceLine) ? ps.currentPriceLine : base.priceScale.currentPriceLine,
23073
23114
  priceLabel: isBool(ps.priceLabel) ? ps.priceLabel : base.priceScale.priceLabel,
23074
- countdown: isBool(ps.countdown) ? ps.countdown : base.priceScale.countdown
23115
+ countdown: isBool(ps.countdown) ? ps.countdown : base.priceScale.countdown,
23116
+ animateLastPrice: isBool(ps.animateLastPrice) ? ps.animateLastPrice : base.priceScale.animateLastPrice
23075
23117
  },
23076
23118
  panes: {
23077
23119
  separatorColor: isColor(panes.separatorColor) ? panes.separatorColor : base.panes.separatorColor
@@ -24716,7 +24758,7 @@ void main() {
24716
24758
  if (e.repeat || e.key !== "Shift" && e.key !== "Control" && e.key !== "Meta") return;
24717
24759
  if (Number.isNaN(this.cursorX)) return;
24718
24760
  if (this.dragging && this.region !== "drawing") return;
24719
- this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey);
24761
+ this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
24720
24762
  };
24721
24763
  this.onDown = (e) => {
24722
24764
  if (e.button === 1) {
@@ -24748,7 +24790,7 @@ void main() {
24748
24790
  this.startY = y;
24749
24791
  if (this.deps.drawingsClaim?.(x, y)) {
24750
24792
  this.region = "drawing";
24751
- this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey);
24793
+ this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
24752
24794
  this.capture(e.pointerId);
24753
24795
  return;
24754
24796
  }
@@ -24757,6 +24799,11 @@ void main() {
24757
24799
  this.capture(e.pointerId);
24758
24800
  return;
24759
24801
  }
24802
+ if ((e.ctrlKey || e.metaKey) && this.regionAt(x, y) === "data" && this.deps.drawingsMarqueeStart?.(x, y)) {
24803
+ this.region = "drawing";
24804
+ this.capture(e.pointerId);
24805
+ return;
24806
+ }
24760
24807
  const vp = this.deps.getCoords().getViewport();
24761
24808
  this.deps.apply(vp);
24762
24809
  this.startRightOffset = vp.rightOffset;
@@ -24805,7 +24852,7 @@ void main() {
24805
24852
  this.deps.apply({ barSpacing, rightOffset: this.startRightOffset });
24806
24853
  } else if (this.region === "drawing") {
24807
24854
  if (Math.abs(x - this.startX) > slop || Math.abs(y - this.startY) > slop) this.moved = true;
24808
- this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
24855
+ this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
24809
24856
  } else {
24810
24857
  const coords = this.deps.getCoords();
24811
24858
  const vp = coords.getViewport();
@@ -24828,7 +24875,7 @@ void main() {
24828
24875
  const drawCursor = this.deps.drawingsCursor?.(x, y);
24829
24876
  const r = this.regionAt(x, y);
24830
24877
  this.el.style.cursor = drawCursor ?? (r === "price" ? "ns-resize" : r === "time" ? "ew-resize" : r === "separator" ? "row-resize" : "");
24831
- this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
24878
+ this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
24832
24879
  }
24833
24880
  if (e.pointerType !== "touch") this.deps.onPointerMove(x, y);
24834
24881
  };
@@ -25169,12 +25216,12 @@ void main() {
25169
25216
  function mergeTradeMarkersState(base, patch) {
25170
25217
  const p = patch && typeof patch === "object" ? patch : {};
25171
25218
  const c = p.colors && typeof p.colors === "object" ? p.colors : {};
25172
- const bool = (v, fb) => typeof v === "boolean" ? v : fb;
25219
+ const bool2 = (v, fb) => typeof v === "boolean" ? v : fb;
25173
25220
  const color = (v, fb) => typeof v === "string" && v.trim().length > 0 ? v : fb;
25174
25221
  return {
25175
- visible: bool(p.visible, base.visible),
25176
- labels: bool(p.labels, base.labels),
25177
- qty: bool(p.qty, base.qty),
25222
+ visible: bool2(p.visible, base.visible),
25223
+ labels: bool2(p.labels, base.labels),
25224
+ qty: bool2(p.qty, base.qty),
25178
25225
  colors: {
25179
25226
  long: color(c.long, base.colors.long),
25180
25227
  short: color(c.short, base.colors.short),
@@ -27935,6 +27982,8 @@ void main() {
27935
27982
  "symbol.style.baseline.fill-bottom",
27936
27983
  "symbol.style.baseline.base-level",
27937
27984
  "symbol.style.baseline.width",
27985
+ "symbol.animation",
27986
+ "symbol.animation.price-changes",
27938
27987
  "symbol.timezone",
27939
27988
  "scales",
27940
27989
  "scales.price-scale",
@@ -28077,8 +28126,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
28077
28126
  .vela-sd-mobile .vela-select-trigger,.vela-sd-mobile .vela-num input,.vela-sd-mobile .vela-width-field{height:34px;}
28078
28127
  .vela-sd-mobile .vela-sd-close{width:40px;height:40px;}
28079
28128
  .vela-sd-mobile .vela-sd-btn{height:38px;}
28080
- .vela-sd-mobile .vela-sd-row span,.vela-sd-mobile .vela-sd-bool span,.vela-sd-mobile .vela-field-label{white-space:normal !important;}
28081
- `;
28129
+ .vela-sd-mobile .vela-sd-row span,.vela-sd-mobile .vela-sd-bool span,.vela-sd-mobile .vela-field-label{white-space:normal !important;}`;
28082
28130
  if (!existing) document.head.appendChild(st);
28083
28131
  }
28084
28132
  var SettingsDialog = class {
@@ -28099,6 +28147,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
28099
28147
  this.themeControl = null;
28100
28148
  /** The built tabs, by title — how `showSection` reaches a pane while the dialog is open. */
28101
28149
  this.tabs = [];
28150
+ this.hintTips = [];
28151
+ // ⓘ tooltip disposers of the open build (see hint)
28102
28152
  /** Active instance-strip tab per chart type — remembered across dialog rebuilds. */
28103
28153
  this.typeActiveInstance = /* @__PURE__ */ new Map();
28104
28154
  /** Active TOC group per structured pane (`<typeId>/<pane>` → group label). */
@@ -28287,6 +28337,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
28287
28337
  body.append(g);
28288
28338
  }
28289
28339
  showActive(config.series.style);
28340
+ body.append(sid(this.sectionTitle("Animation"), "symbol.animation"));
28341
+ body.append(sid(this.boolRow(
28342
+ "Animate price changes",
28343
+ config.priceScale.animateLastPrice,
28344
+ (v) => this.emit({ priceScale: { animateLastPrice: v } }),
28345
+ this.hint("Glide the live bar to each new price instead of snapping.")
28346
+ ), "symbol.animation.price-changes"));
28290
28347
  body.append(sid(this.sectionTitle("Time zone"), "symbol.timezone"));
28291
28348
  body.append(sid(this.selectRowLabeled("Time zone", normalizeTimezone(config.timeScale.timezone), timezoneOptions(config.timeScale.timezone), (v) => this.emit({ timeScale: { timezone: v } })), "symbol.timezone"));
28292
28349
  const renderHostSections = (placement) => {
@@ -28494,6 +28551,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
28494
28551
  this.ui = null;
28495
28552
  this.root = null;
28496
28553
  this.tabs = [];
28554
+ for (const dispose of this.hintTips) dispose();
28555
+ this.hintTips = [];
28497
28556
  ui?.destroy();
28498
28557
  }
28499
28558
  destroy() {
@@ -28907,23 +28966,31 @@ ${overlayScrollbarCss(".vela-sd-pane")}
28907
28966
  }).el
28908
28967
  });
28909
28968
  }
28910
- /** A toggle row: the checkbox sits to the LEFT of its label (never in the control area). */
28911
- boolRow(label, value, onChange) {
28912
- return this.toggleRow(label, value, onChange, []);
28969
+ /** A toggle row: the checkbox sits to the LEFT of its label (never in the control area).
28970
+ * `info` (see {@link hint}) rides after the label. */
28971
+ boolRow(label, value, onChange, info) {
28972
+ return this.toggleRow(label, value, onChange, [], void 0, info);
28913
28973
  }
28914
28974
  /** An enable row: checkbox + label in the label column, dependent controls in the
28915
28975
  * shared control column; the control group dims and ignores input while the toggle
28916
28976
  * is off. With `get`, the row re-reads its state on a 'vela-sync' event. */
28917
- toggleRow(label, value, onToggle, controls, get) {
28977
+ toggleRow(label, value, onToggle, controls, get, info) {
28918
28978
  return fieldRow({
28919
28979
  label,
28920
28980
  labelSize: "sm",
28921
28981
  bool: controls.length === 0,
28922
28982
  toggle: { checked: value, onChange: onToggle, get },
28923
28983
  control: controls,
28984
+ info,
28924
28985
  className: controls.length === 0 ? "vela-sd-bool" : "vela-sd-row"
28925
28986
  });
28926
28987
  }
28988
+ /** The shared ⓘ hint after a row label (see {@link chromeHint}). Disposed with the dialog. */
28989
+ hint(text) {
28990
+ const { el, dispose } = chromeHint(text, { host: this.container, theme: () => this.theme });
28991
+ this.hintTips.push(dispose);
28992
+ return el;
28993
+ }
28927
28994
  numberRow(label, value, min2, max2, step, onChange) {
28928
28995
  return fieldRow({
28929
28996
  label,
@@ -29104,6 +29171,18 @@ ${overlayScrollbarCss(".vela-sd-pane")}
29104
29171
  }
29105
29172
  this.paintHandles(ctx, pts);
29106
29173
  }
29174
+ /** The selection box a Ctrl/Cmd-drag on the empty plot sweeps: a faint fill in the handle
29175
+ * accent under a thin dashed outline. */
29176
+ paintMarquee(ctx, rect) {
29177
+ ctx.save();
29178
+ ctx.fillStyle = withAlpha(HANDLE_BORDER, 0.08);
29179
+ ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
29180
+ ctx.strokeStyle = HANDLE_BORDER;
29181
+ ctx.lineWidth = 1;
29182
+ ctx.setLineDash([4, 3]);
29183
+ ctx.strokeRect(rect.x + 0.5, rect.y + 0.5, rect.w, rect.h);
29184
+ ctx.restore();
29185
+ }
29107
29186
  /** A hollow ring marking the candle point the magnet (Ctrl) will snap the next anchor to. */
29108
29187
  paintSnapRing(ctx, x, y, theme) {
29109
29188
  ctx.setLineDash([]);
@@ -30572,6 +30651,15 @@ ${overlayScrollbarCss(".vela-sd-pane")}
30572
30651
  }
30573
30652
  return null;
30574
30653
  }
30654
+ function deletableSelection(selected, drawings) {
30655
+ const ids = [...selected];
30656
+ if (ids.length < 2) return ids;
30657
+ return ids.filter((id) => !drawings.find((d) => d.id === id)?.locked);
30658
+ }
30659
+ function deleteTargets(hit, selected, drawings, withSelection) {
30660
+ if (withSelection && selected.size >= 2 && selected.has(hit.id)) return deletableSelection(selected, drawings);
30661
+ return hit.locked ? [] : [hit.id];
30662
+ }
30575
30663
 
30576
30664
  // src/renderers/native/drawings/DrawingInteraction.ts
30577
30665
  var DRAG_SLOP2 = 3;
@@ -30675,10 +30763,32 @@ ${overlayScrollbarCss(".vela-sd-pane")}
30675
30763
  activeDragId() {
30676
30764
  return this.state.kind === "pressed" && this.state.moved ? this.state.id : null;
30677
30765
  }
30766
+ /** The drawing under a press that has not been released yet (drag or click undecided), or null. */
30767
+ pressedId() {
30768
+ return this.state.kind === "pressed" ? this.state.id : null;
30769
+ }
30770
+ /** The copies a Ctrl-drag is moving (transient — not yet in the store), or null. */
30771
+ dragClones() {
30772
+ return this.state.kind === "pressed" && this.state.moved && this.state.clones ? this.state.clones.map((c) => c.d) : null;
30773
+ }
30774
+ /** The marquee box being swept, normalized (or null when none is in progress). */
30775
+ marqueeRect() {
30776
+ if (this.state.kind !== "marquee") return null;
30777
+ const { x0, y0, x1, y1 } = this.state;
30778
+ return { x: Math.min(x0, x1), y: Math.min(y0, y1), w: Math.abs(x1 - x0), h: Math.abs(y1 - y0) };
30779
+ }
30780
+ /** Ctrl/Cmd-press on the empty plot: start sweeping a selection box. False when a tool is
30781
+ * armed or a gesture is in flight (that path owns the press). */
30782
+ beginMarquee(x, y) {
30783
+ if (this.state.kind !== "idle" || this.deps.activeTool() != null) return false;
30784
+ this.state = { kind: "marquee", x0: x, y0: y, x1: x, y1: y };
30785
+ return true;
30786
+ }
30678
30787
  /** A press: place the next anchor, or grab a drawing/handle (resolved as click or drag on release).
30679
30788
  * `shift` over a drawing toggles it in/out of the selection (multi-select) instead of dragging;
30680
- * while placing a line tool it locks the segment angle to 45° steps. */
30681
- down(x, y, mode = "off", shift4 = false) {
30789
+ * while placing a line tool it locks the segment angle to 45° steps. `mod` (Ctrl/Cmd) over a
30790
+ * drawing also toggles on a click, but a body drag then moves a COPY. */
30791
+ down(x, y, mode = "off", shift4 = false, mod = false) {
30682
30792
  const proj = this.deps.projector();
30683
30793
  if (this.state.kind === "placing") {
30684
30794
  if (this.state.draft.placementMode() !== "click") return;
@@ -30706,7 +30816,50 @@ ${overlayScrollbarCss(".vela-sd-pane")}
30706
30816
  return;
30707
30817
  }
30708
30818
  const handle = hit.hitHandle(x, y, proj, HIT_TOLERANCE);
30709
- this.state = { kind: "pressed", id: hit.id, handle, grab: proj.pxToPoint(x, y, hit.paneId), orig: cloneAnchors(hit), px: x, py: y, moved: false };
30819
+ const riders = [];
30820
+ if (handle < 0 && this.deps.selectedIds().has(hit.id)) {
30821
+ for (const id of this.deps.selectedIds()) {
30822
+ const d = id === hit.id ? null : this.byId(id);
30823
+ if (d && !d.locked && d.visible) riders.push({ id, orig: cloneAnchors(d) });
30824
+ }
30825
+ }
30826
+ this.state = { kind: "pressed", id: hit.id, handle, orig: cloneAnchors(hit), px: x, py: y, moved: false, riders, mod, clones: null };
30827
+ }
30828
+ }
30829
+ /** The data-space delta a body drag from the press to (x, y) means on `paneId` — resolved per
30830
+ * pane so drawings riding along on another pane move by the same PIXELS, not the same price. */
30831
+ bodyDelta(paneId, x, y) {
30832
+ if (this.state.kind !== "pressed") return { dt: 0, dp: 0 };
30833
+ const proj = this.deps.projector();
30834
+ const from = proj.pxToPoint(this.state.px, this.state.py, paneId);
30835
+ const to = proj.pxToPoint(x, y, paneId);
30836
+ return { dt: to.time - from.time, dp: to.price - from.price };
30837
+ }
30838
+ /** The pressed drawing and its riders, each with its anchors at the press. */
30839
+ dragSources(d) {
30840
+ if (this.state.kind !== "pressed") return [];
30841
+ const out = [{ d, orig: this.state.orig }];
30842
+ for (const r of this.state.riders) {
30843
+ const rd = this.byId(r.id);
30844
+ if (rd) out.push({ d: rd, orig: r.orig });
30845
+ }
30846
+ return out;
30847
+ }
30848
+ /** Move the pressed drawing's body (and its riders) — or, on a Ctrl-drag, their copies — to the
30849
+ * cursor. The copies are built on the first move (so a Ctrl-click never creates anything), from
30850
+ * sources that have not moved yet, so their own anchors are the origin to translate from. */
30851
+ dragBody(d, x, y) {
30852
+ if (this.state.kind !== "pressed") return;
30853
+ this.snapAt = null;
30854
+ if (this.state.mod && !this.state.clones) {
30855
+ this.state.clones = this.dragSources(d).flatMap((s) => {
30856
+ const c = deserializeDrawing(s.d.serialize());
30857
+ return c ? [{ d: c, orig: cloneAnchors(c) }] : [];
30858
+ });
30859
+ }
30860
+ for (const t of this.state.clones ?? this.dragSources(d)) {
30861
+ const { dt, dp } = this.bodyDelta(t.d.paneId, x, y);
30862
+ t.d.anchors = t.d.translateBody(dt, dp, t.orig);
30710
30863
  }
30711
30864
  }
30712
30865
  /** Pointer move: advance the placing ghost, or live-preview a drag once past the slop.
@@ -30730,12 +30883,20 @@ ${overlayScrollbarCss(".vela-sd-pane")}
30730
30883
  this.deps.changed();
30731
30884
  return;
30732
30885
  }
30886
+ if (this.state.kind === "marquee") {
30887
+ this.state.x1 = x;
30888
+ this.state.y1 = y;
30889
+ this.deps.changed();
30890
+ return;
30891
+ }
30733
30892
  if (this.state.kind !== "pressed") return;
30734
30893
  const d = this.byId(this.state.id);
30735
30894
  if (!d || d.locked) return;
30736
30895
  if (!this.state.moved && Math.abs(x - this.state.px) <= DRAG_SLOP2 && Math.abs(y - this.state.py) <= DRAG_SLOP2) return;
30737
30896
  this.state.moved = true;
30738
- if (this.state.handle >= 0) {
30897
+ if (this.state.handle < 0) {
30898
+ this.dragBody(d, x, y);
30899
+ } else {
30739
30900
  const a = d.anchors[this.state.handle];
30740
30901
  if (a) {
30741
30902
  const pivot = shift4 && ANGLE_SNAP_TYPES.has(d.type) && d.anchors.length === 2 ? d.anchors[1 - this.state.handle] : void 0;
@@ -30743,12 +30904,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
30743
30904
  applyFree(a, point, d.anchorSchema().slots[this.state.handle]?.free ?? "both");
30744
30905
  }
30745
30906
  d.constrainHandleDrag(this.state.handle);
30746
- } else {
30747
- const pt2 = this.deps.projector().pxToPoint(x, y, d.paneId);
30748
- this.snapAt = null;
30749
- const dt = pt2.time - this.state.grab.time;
30750
- const dp = pt2.price - this.state.grab.price;
30751
- d.anchors = d.translateBody(dt, dp, this.state.orig);
30752
30907
  }
30753
30908
  this.deps.changed();
30754
30909
  }
@@ -30768,16 +30923,46 @@ ${overlayScrollbarCss(".vela-sd-pane")}
30768
30923
  }
30769
30924
  return;
30770
30925
  }
30926
+ if (this.state.kind === "marquee") {
30927
+ const rect = this.marqueeRect();
30928
+ this.state = { kind: "idle" };
30929
+ if (rect.w > DRAG_SLOP2 || rect.h > DRAG_SLOP2) this.selectWithin(rect);
30930
+ this.deps.changed();
30931
+ return;
30932
+ }
30771
30933
  if (this.state.kind !== "pressed") return;
30772
- const { id, moved } = this.state;
30934
+ const { id, moved, riders, mod, clones } = this.state;
30773
30935
  this.state = { kind: "idle" };
30774
30936
  const d = this.byId(id);
30775
- if (moved) {
30776
- if (d) this.deps.emit({ kind: "edit", doc: d.serialize() });
30777
- this.deps.changed();
30778
- } else {
30779
- this.deps.openSettings(id, x, y);
30937
+ if (!moved) {
30938
+ if (mod) this.deps.emit({ kind: "select", ids: [id], additive: true });
30939
+ else this.deps.openSettings(id, x, y);
30940
+ return;
30941
+ }
30942
+ if (clones) {
30943
+ this.deps.emit({ kind: "clone", docs: clones.map((c) => c.d.serialize()) });
30944
+ } else if (d) {
30945
+ const docs = [d.serialize()];
30946
+ for (const r of riders) {
30947
+ const rd = this.byId(r.id);
30948
+ if (rd) docs.push(rd.serialize());
30949
+ }
30950
+ if (docs.length > 1) this.deps.emit({ kind: "edit-many", docs });
30951
+ else this.deps.emit({ kind: "edit", doc: docs[0] });
30780
30952
  }
30953
+ this.deps.changed();
30954
+ }
30955
+ /** Add every visible drawing whose on-screen bounds touch `rect` to the selection (a union,
30956
+ * so successive sweeps accumulate). No change when the box touches nothing new. */
30957
+ selectWithin(rect) {
30958
+ const proj = this.deps.projector();
30959
+ const ids = [...this.deps.selectedIds()];
30960
+ for (const d of this.deps.drawings()) {
30961
+ if (!d.visible || ids.includes(d.id)) continue;
30962
+ const b = d.bounds(proj);
30963
+ if (b && rectsIntersect(rect, b)) ids.push(d.id);
30964
+ }
30965
+ if (ids.length > this.deps.selectedIds().size) this.deps.emit({ kind: "select", ids });
30781
30966
  }
30782
30967
  /** Cancel an in-progress placement or drag (Escape). Returns whether anything was cancelled. */
30783
30968
  cancel() {
@@ -30790,8 +30975,15 @@ ${overlayScrollbarCss(".vela-sd-pane")}
30790
30975
  return true;
30791
30976
  }
30792
30977
  if (this.state.kind === "pressed") {
30793
- const d = this.byId(this.state.id);
30794
- if (d && this.state.moved) d.anchors = this.state.orig.map((o) => ({ time: o.time, price: o.price }));
30978
+ if (this.state.moved && !this.state.clones) {
30979
+ const d = this.byId(this.state.id);
30980
+ if (d) for (const s of this.dragSources(d)) s.d.anchors = s.orig.map((o) => ({ time: o.time, price: o.price }));
30981
+ }
30982
+ this.state = { kind: "idle" };
30983
+ this.deps.changed();
30984
+ return true;
30985
+ }
30986
+ if (this.state.kind === "marquee") {
30795
30987
  this.state = { kind: "idle" };
30796
30988
  this.deps.changed();
30797
30989
  return true;
@@ -30906,6 +31098,9 @@ ${overlayScrollbarCss(".vela-sd-pane")}
30906
31098
  function cloneAnchors(d) {
30907
31099
  return d.anchors.map((a) => ({ time: a.time, price: a.price }));
30908
31100
  }
31101
+ function rectsIntersect(a, b) {
31102
+ return a.x <= b.x + b.w && b.x <= a.x + a.w && a.y <= b.y + b.h && b.y <= a.y + a.h;
31103
+ }
30909
31104
  function applyFree(anchor, pt2, free) {
30910
31105
  if (free === "both" || free === "x") anchor.time = pt2.time;
30911
31106
  if (free === "both" || free === "y") anchor.price = pt2.price;
@@ -31291,10 +31486,27 @@ ${overlayScrollbarCss(".vela-sd-pane")}
31291
31486
  }
31292
31487
 
31293
31488
  // src/renderers/native/drawings/DrawingSettingsPopup.ts
31489
+ var MIXED = /* @__PURE__ */ Symbol("mixed");
31490
+ function sharedPaths(drawings) {
31491
+ const [first2, ...rest] = drawings;
31492
+ const paths = new Set(first2?.schema().fields.map((f) => f.path) ?? []);
31493
+ for (const d of rest) {
31494
+ const own = new Set(d.schema().fields.map((f) => f.path));
31495
+ for (const p of paths) if (!own.has(p)) paths.delete(p);
31496
+ }
31497
+ return paths;
31498
+ }
31499
+ function commonValue(drawings, read) {
31500
+ const first2 = read(drawings[0]);
31501
+ return drawings.every((d) => read(d) === first2) ? first2 : MIXED;
31502
+ }
31503
+ function distinctValues(drawings, read) {
31504
+ return [...new Set(drawings.map(read))];
31505
+ }
31294
31506
  var BTN = 30;
31295
31507
  var ICON = 21;
31296
31508
  var STYLE_ID3 = "vela-drawing-popup-styles";
31297
- var STYLE_REV = "3";
31509
+ var STYLE_REV = "4";
31298
31510
  function ensureStyles3() {
31299
31511
  if (typeof document === "undefined") return;
31300
31512
  const existing = document.getElementById(STYLE_ID3);
@@ -31306,6 +31518,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
31306
31518
  .vela-dpop-btn{background:transparent;color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
31307
31519
  .vela-dpop-btn:hover{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
31308
31520
  .vela-dpop-btn[data-active='1']{background:var(--vela-active);color:var(--vela-fg-bright);}
31521
+ /* Mixed (a multi-selection disagrees): the active fill at half strength behind a dashed ring. */
31522
+ .vela-dpop-btn[data-active='mixed']{background:var(--vela-hover-strong);color:var(--vela-fg-bright);outline:1px dashed var(--vela-fg-muted);outline-offset:-2px;}
31309
31523
  .vela-dpop-item{background:transparent;transition:background var(--vela-dur-fast) ease;}
31310
31524
  .vela-dpop-item:hover{background:var(--vela-hover-strong);}
31311
31525
  .vela-dpop-item[data-active='1']{background:var(--vela-active);}
@@ -31337,7 +31551,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
31337
31551
  if (this.isOwnChrome(node)) return;
31338
31552
  const cb = this.onClose;
31339
31553
  this.close();
31340
- cb?.();
31554
+ cb?.(e);
31341
31555
  };
31342
31556
  this.theme = theme;
31343
31557
  this.settingsDialog = new DrawingSettingsDialog(host, theme);
@@ -31362,16 +31576,27 @@ ${overlayScrollbarCss(".vela-sd-pane")}
31362
31576
  contains(node) {
31363
31577
  return node != null && (this.isOwnChrome(node) || this.settingsDialog.contains(node));
31364
31578
  }
31365
- /** Open the quick toolbar for `drawing`, floating clear of its `anchor` box.
31366
- * `onClose` fires on an outside-click dismissal (not a programmatic close). */
31367
- open(drawing, anchor, actions, onClose) {
31579
+ /** Open the quick toolbar for `drawings` (one, or a whole multi-selection), floating clear of
31580
+ * their `anchor` box. With several drawings the bar shows only the controls they ALL support,
31581
+ * a control whose values differ reads as mixed, and every edit applies to all of them —
31582
+ * per-drawing panels (levels, position sizing, the text field) stay single-drawing only.
31583
+ * `onClose` fires on a dismissal (an outside press, or Escape in the text field — not a
31584
+ * programmatic close), with the dismissing press when there is one, so the caller can tell a
31585
+ * modifier press (multi-select) from a plain one. */
31586
+ open(drawings, anchor, actions, onClose) {
31368
31587
  this.close();
31588
+ const drawing = drawings[0];
31589
+ if (!drawing) return;
31369
31590
  ensureStyles3();
31370
31591
  this.onClose = onClose ?? null;
31371
31592
  const t = this.theme;
31593
+ const multi = drawings.length > 1;
31372
31594
  const schema = drawing.schema();
31373
- const paths = new Set(schema.fields.map((f) => f.path));
31374
- const textOnBar = schema.textIsContent === true || !paths.has("text.value");
31595
+ const paths = sharedPaths(drawings);
31596
+ const common = (read) => commonValue(drawings, read);
31597
+ const every = (pred) => drawings.every(pred);
31598
+ const swatch = (tip, glyph, read, path) => this.colorButton(tip, glyph, common(read), (v) => actions.patch({ [path]: v }), void 0, distinctValues(drawings, read));
31599
+ const textOnBar = multi || schema.textIsContent === true || !paths.has("text.value");
31375
31600
  const el = document.createElement("div");
31376
31601
  el.className = "vela-dpop";
31377
31602
  el.style.cssText = `position:absolute;z-index:22;background:${t.background};border:1px solid var(--vela-border);border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow);color:${t.textColor};font:var(--vela-font-size-md) ${t.fontFamily};display:flex;flex-direction:column;pointer-events:auto;overflow:hidden;`;
@@ -31385,75 +31610,84 @@ ${overlayScrollbarCss(".vela-sd-pane")}
31385
31610
  bar.style.cssText = "display:flex;align-items:center;gap:2px;padding:4px;";
31386
31611
  bar.appendChild(this.dragHandle());
31387
31612
  if (paths.has("glyph")) {
31388
- const cur = drawing.glyph ?? GLYPH_OPTIONS[0];
31613
+ const cur = common((d) => d.glyph ?? GLYPH_OPTIONS[0]);
31389
31614
  bar.appendChild(this.dropdown("Icon", GLYPH_OPTIONS, cur, (g) => glyphIcon(g), (v) => actions.patch({ glyph: v })));
31390
31615
  }
31391
31616
  if (paths.has("size")) {
31392
- const sz = drawing.size ?? "normal";
31617
+ const sz = common((d) => d.size ?? "normal");
31393
31618
  bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
31394
31619
  }
31395
- if (paths.has("magnifier.timeframe") && drawing instanceof Magnifier) {
31620
+ if (paths.has("magnifier.timeframe") && every((d) => d instanceof Magnifier)) {
31621
+ const mag = (d) => d;
31396
31622
  const options = this.lowerTimeframeOptions();
31397
31623
  if (options.length > 0) {
31398
31624
  bar.appendChild(
31399
- this.dropdown("Lower timeframe", options.map((o) => o.value), drawing.magnifier.timeframe, () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
31625
+ this.dropdown("Lower timeframe", options.map((o) => o.value), common((d) => mag(d).magnifier.timeframe), () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
31400
31626
  label: (v) => magnifierTimeframeLabel(String(v)),
31401
31627
  labelInTrigger: true
31402
31628
  })
31403
31629
  );
31404
31630
  }
31405
- bar.appendChild(this.colorButton("Up candles", BUCKET_ICON, drawing.magnifier.upColor || t.upColor, (v) => actions.patch({ "magnifier.upColor": v })));
31406
- bar.appendChild(this.colorButton("Down candles", BUCKET_ICON, drawing.magnifier.downColor || t.downColor, (v) => actions.patch({ "magnifier.downColor": v })));
31631
+ bar.appendChild(swatch("Up candles", BUCKET_ICON, (d) => mag(d).magnifier.upColor || t.upColor, "magnifier.upColor"));
31632
+ bar.appendChild(swatch("Down candles", BUCKET_ICON, (d) => mag(d).magnifier.downColor || t.downColor, "magnifier.downColor"));
31407
31633
  }
31408
- if (paths.has("style.lineColor")) bar.appendChild(this.colorButton("Line color", BRUSH_ICON, drawing.style.lineColor || (drawing instanceof Magnifier ? contrastColor(this.theme.background) : DEFAULT_DRAWING_COLOR), (v) => actions.patch({ "style.lineColor": v })));
31634
+ if (paths.has("style.lineColor")) bar.appendChild(swatch("Line color", BRUSH_ICON, (d) => d.style.lineColor || (d instanceof Magnifier ? contrastColor(this.theme.background) : DEFAULT_DRAWING_COLOR), "style.lineColor"));
31409
31635
  if (paths.has("style.lineWidth")) {
31410
- const wf = schema.fields.find((f) => f.path === "style.lineWidth");
31411
- if (wf?.kind === "number" && (wf.min ?? 1) > 1) {
31412
- bar.appendChild(this.widthInput("Line width", drawing.style.lineWidth, wf.min ?? 1, wf.max ?? 60, wf.step ?? 1, (v) => actions.patch({ "style.lineWidth": v })));
31636
+ const widthField2 = (d) => d.schema().fields.find((f) => f.path === "style.lineWidth");
31637
+ const wf = widthField2(drawing);
31638
+ const width = common((d) => d.style.lineWidth);
31639
+ const markers = every((d) => {
31640
+ const f = widthField2(d);
31641
+ return f?.kind === "number" && (f.min ?? 1) > 1;
31642
+ });
31643
+ if (markers && wf?.kind === "number") {
31644
+ bar.appendChild(this.widthInput("Line width", width, wf.min ?? 1, wf.max ?? 60, wf.step ?? 1, (v) => actions.patch({ "style.lineWidth": v })));
31413
31645
  } else {
31414
- bar.appendChild(this.dropdown("Line width", [1, 2, 3, 4], drawing.style.lineWidth, (w) => lineIcon(w, "solid"), (v) => actions.patch({ "style.lineWidth": v }), { label: (v) => `${v}px`, labelInTrigger: true }));
31646
+ bar.appendChild(this.dropdown("Line width", [1, 2, 3, 4], width, (w) => lineIcon(w, "solid"), (v) => actions.patch({ "style.lineWidth": v }), { label: (v) => `${v}px`, labelInTrigger: true }));
31415
31647
  }
31416
31648
  }
31417
- if (paths.has("style.lineStyle")) bar.appendChild(this.dropdown("Line style", LINE_STYLE_OPTIONS.map((o) => o.value), drawing.style.lineStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "style.lineStyle": v }), { label: styleLabel2 }));
31418
- if (paths.has("style.fillColor")) bar.appendChild(this.colorButton("Fill", BUCKET_ICON, effectiveFillColor(drawing, this.theme) ?? drawing.style.fillColor ?? DEFAULT_DRAWING_COLOR, (v) => actions.patch({ "style.fillColor": v })));
31649
+ if (paths.has("style.lineStyle")) bar.appendChild(this.dropdown("Line style", LINE_STYLE_OPTIONS.map((o) => o.value), common((d) => d.style.lineStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "style.lineStyle": v }), { label: styleLabel2 }));
31650
+ if (paths.has("style.fillColor")) bar.appendChild(swatch("Fill", BUCKET_ICON, (d) => effectiveFillColor(d, this.theme) ?? d.style.fillColor ?? DEFAULT_DRAWING_COLOR, "style.fillColor"));
31419
31651
  const isFrvp = paths.has("frvp.rows") && drawing instanceof FixedRangeVolumeProfile;
31420
- const isPosition = paths.has("riskPercent") && drawing instanceof PositionTool;
31652
+ const isPosition = paths.has("riskPercent") && every((d) => d instanceof PositionTool);
31421
31653
  if (isPosition) {
31422
- const pos = drawing;
31423
- bar.appendChild(this.colorButton("Profit zone", BUCKET_ICON, pos.profitColor, (v) => actions.patch({ profitColor: v })));
31424
- bar.appendChild(this.colorButton("Loss zone", BUCKET_ICON, pos.lossColor, (v) => actions.patch({ lossColor: v })));
31654
+ const pos = (d) => d;
31655
+ bar.appendChild(swatch("Profit zone", BUCKET_ICON, (d) => pos(d).profitColor, "profitColor"));
31656
+ bar.appendChild(swatch("Loss zone", BUCKET_ICON, (d) => pos(d).lossColor, "lossColor"));
31425
31657
  }
31426
- const reg = drawing.reg;
31427
- if (paths.has("reg.midColor") && reg) {
31658
+ const regOf = (d) => d.reg;
31659
+ if (paths.has("reg.midColor") && every((d) => regOf(d) != null)) {
31660
+ const reg = (d) => regOf(d);
31428
31661
  const styles = LINE_STYLE_OPTIONS.map((o) => o.value);
31429
- bar.appendChild(this.colorButton("Midline color", BRUSH_ICON, reg.midColor, (v) => actions.patch({ "reg.midColor": v })));
31430
- bar.appendChild(this.dropdown("Midline style", styles, reg.midStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.midStyle": v }), { label: styleLabel2 }));
31431
- bar.appendChild(this.colorButton("Upper line color", BRUSH_ICON, reg.upperColor, (v) => actions.patch({ "reg.upperColor": v })));
31432
- bar.appendChild(this.dropdown("Upper line style", styles, reg.upperStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.upperStyle": v }), { label: styleLabel2 }));
31433
- bar.appendChild(this.colorButton("Lower line color", BRUSH_ICON, reg.lowerColor, (v) => actions.patch({ "reg.lowerColor": v })));
31434
- bar.appendChild(this.dropdown("Lower line style", styles, reg.lowerStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.lowerStyle": v }), { label: styleLabel2 }));
31435
- bar.appendChild(this.colorButton("Upper fill", BUCKET_ICON, reg.upperFill, (v) => actions.patch({ "reg.upperFill": v })));
31436
- bar.appendChild(this.colorButton("Lower fill", BUCKET_ICON, reg.lowerFill, (v) => actions.patch({ "reg.lowerFill": v })));
31437
- bar.appendChild(this.toggle("Show R\xB2", R2_ICON, reg.showR2, (v) => actions.patch({ "reg.showR2": v })));
31438
- }
31439
- const vwap2 = drawing.vwap;
31440
- if (paths.has("vwap.midColor") && vwap2) {
31662
+ bar.appendChild(swatch("Midline color", BRUSH_ICON, (d) => reg(d).midColor, "reg.midColor"));
31663
+ bar.appendChild(this.dropdown("Midline style", styles, common((d) => reg(d).midStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "reg.midStyle": v }), { label: styleLabel2 }));
31664
+ bar.appendChild(swatch("Upper line color", BRUSH_ICON, (d) => reg(d).upperColor, "reg.upperColor"));
31665
+ bar.appendChild(this.dropdown("Upper line style", styles, common((d) => reg(d).upperStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "reg.upperStyle": v }), { label: styleLabel2 }));
31666
+ bar.appendChild(swatch("Lower line color", BRUSH_ICON, (d) => reg(d).lowerColor, "reg.lowerColor"));
31667
+ bar.appendChild(this.dropdown("Lower line style", styles, common((d) => reg(d).lowerStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "reg.lowerStyle": v }), { label: styleLabel2 }));
31668
+ bar.appendChild(swatch("Upper fill", BUCKET_ICON, (d) => reg(d).upperFill, "reg.upperFill"));
31669
+ bar.appendChild(swatch("Lower fill", BUCKET_ICON, (d) => reg(d).lowerFill, "reg.lowerFill"));
31670
+ bar.appendChild(this.toggle("Show R\xB2", R2_ICON, common((d) => reg(d).showR2), (v) => actions.patch({ "reg.showR2": v })));
31671
+ }
31672
+ const vwapOf = (d) => d.vwap;
31673
+ if (paths.has("vwap.midColor") && every((d) => vwapOf(d) != null)) {
31674
+ const vwap2 = (d) => vwapOf(d);
31441
31675
  const styles = LINE_STYLE_OPTIONS.map((o) => o.value);
31442
31676
  const MULTS = [0.5, 1, 1.5, 2, 2.5, 3, 4, 5];
31443
- bar.appendChild(this.colorButton("VWAP color", BRUSH_ICON, vwap2.midColor, (v) => actions.patch({ "vwap.midColor": v })));
31444
- bar.appendChild(this.dropdown("VWAP style", styles, vwap2.midStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "vwap.midStyle": v }), { label: styleLabel2 }));
31677
+ bar.appendChild(swatch("VWAP color", BRUSH_ICON, (d) => vwap2(d).midColor, "vwap.midColor"));
31678
+ bar.appendChild(this.dropdown("VWAP style", styles, common((d) => vwap2(d).midStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "vwap.midStyle": v }), { label: styleLabel2 }));
31445
31679
  bar.appendChild(
31446
- this.dropdown("Band multiplier", MULTS, vwap2.multiplier, () => BANDS_ICON, (v) => actions.patch({ "vwap.multiplier": v }), {
31680
+ this.dropdown("Band multiplier", MULTS, common((d) => vwap2(d).multiplier), () => BANDS_ICON, (v) => actions.patch({ "vwap.multiplier": v }), {
31447
31681
  label: (v) => `${v}\u03C3`,
31448
31682
  labelInTrigger: true
31449
31683
  })
31450
31684
  );
31451
- bar.appendChild(this.colorButton("Upper band color", BRUSH_ICON, vwap2.upperColor, (v) => actions.patch({ "vwap.upperColor": v })));
31452
- bar.appendChild(this.colorButton("Lower band color", BRUSH_ICON, vwap2.lowerColor, (v) => actions.patch({ "vwap.lowerColor": v })));
31453
- bar.appendChild(this.colorButton("Band fill", BUCKET_ICON, vwap2.bandFill, (v) => actions.patch({ "vwap.bandFill": v })));
31685
+ bar.appendChild(swatch("Upper band color", BRUSH_ICON, (d) => vwap2(d).upperColor, "vwap.upperColor"));
31686
+ bar.appendChild(swatch("Lower band color", BRUSH_ICON, (d) => vwap2(d).lowerColor, "vwap.lowerColor"));
31687
+ bar.appendChild(swatch("Band fill", BUCKET_ICON, (d) => vwap2(d).bandFill, "vwap.bandFill"));
31454
31688
  }
31455
31689
  if (paths.has("maxCurvature")) {
31456
- const cur = drawing.maxCurvature ?? 24;
31690
+ const cur = common((d) => d.maxCurvature ?? 24);
31457
31691
  bar.appendChild(
31458
31692
  this.dropdown("Max curvature", DEDEKIND_CURVATURE_OPTIONS, cur, () => DEDEKIND_ICON2, (v) => actions.patch({ maxCurvature: v }), {
31459
31693
  label: (v) => `n=${v}`,
@@ -31462,7 +31696,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
31462
31696
  );
31463
31697
  }
31464
31698
  if (paths.has("mach")) {
31465
- const cur = drawing.mach ?? 2;
31699
+ const cur = common((d) => d.mach ?? 2);
31466
31700
  bar.appendChild(
31467
31701
  this.dropdown("Mach number", MACH_NUMBER_OPTIONS, cur, () => SUPERSONIC_ICON2, (v) => actions.patch({ mach: v }), {
31468
31702
  label: (v) => `M=${v}`,
@@ -31471,7 +31705,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
31471
31705
  );
31472
31706
  }
31473
31707
  if (paths.has("waveCount")) {
31474
- const cur = drawing.waveCount ?? 6;
31708
+ const cur = common((d) => d.waveCount ?? 6);
31475
31709
  bar.appendChild(
31476
31710
  this.dropdown("Waves", MACH_WAVE_COUNT_OPTIONS, cur, () => SONIC_ICON2, (v) => actions.patch({ waveCount: v }), {
31477
31711
  label: (v) => `${v}`,
@@ -31479,26 +31713,27 @@ ${overlayScrollbarCss(".vela-sd-pane")}
31479
31713
  })
31480
31714
  );
31481
31715
  }
31482
- const toggles = drawing;
31483
- if (paths.has("showPrice")) bar.appendChild(this.toggle("Show price", PRICE_DELTA_ICON, toggles.showPrice !== false, (v) => actions.patch({ showPrice: v })));
31484
- if (paths.has("showDate")) bar.appendChild(this.toggle("Show date", DATE_DELTA_ICON, toggles.showDate !== false, (v) => actions.patch({ showDate: v })));
31485
- if (paths.has("text.color") && textOnBar) bar.appendChild(this.colorButton("Text color", TYPE_ICON, drawing.text?.color || t.textColor, (v) => actions.patch({ "text.color": v })));
31486
- if (paths.has("text.size") && textOnBar) bar.appendChild(this.dropdown("Text size", TEXT_SIZE_OPTIONS.map((o) => o.value), drawing.text?.size ?? "normal", (s) => labelSizeIcon(s), (v) => actions.patch({ "text.size": v }), { label: sizeLabel }));
31487
- if (paths.has("text.bold") && !paths.has("text.value")) bar.appendChild(this.toggle("Bold", BOLD_ICON, !!drawing.text?.bold, (v) => actions.patch({ "text.bold": v })));
31488
- if (paths.has("text.italic") && !paths.has("text.value")) bar.appendChild(this.toggle("Italic", ITALIC_ICON, !!drawing.text?.italic, (v) => actions.patch({ "text.italic": v })));
31489
- if (paths.has("text.value")) bar.appendChild(this.iconBtn("Text", TYPE_ICON, () => this.toggleTextPanel(drawing, actions, !textOnBar)));
31716
+ const flags = (d) => d;
31717
+ if (paths.has("showPrice")) bar.appendChild(this.toggle("Show price", PRICE_DELTA_ICON, common((d) => flags(d).showPrice !== false), (v) => actions.patch({ showPrice: v })));
31718
+ if (paths.has("showDate")) bar.appendChild(this.toggle("Show date", DATE_DELTA_ICON, common((d) => flags(d).showDate !== false), (v) => actions.patch({ showDate: v })));
31719
+ if (paths.has("text.color") && textOnBar) bar.appendChild(swatch("Text color", TYPE_ICON, (d) => d.text?.color || t.textColor, "text.color"));
31720
+ if (paths.has("text.size") && textOnBar) bar.appendChild(this.dropdown("Text size", TEXT_SIZE_OPTIONS.map((o) => o.value), common((d) => d.text?.size ?? "normal"), (s) => labelSizeIcon(s), (v) => actions.patch({ "text.size": v }), { label: sizeLabel }));
31721
+ const textField = !multi && paths.has("text.value");
31722
+ if (paths.has("text.bold") && !textField) bar.appendChild(this.toggle("Bold", BOLD_ICON, common((d) => !!d.text?.bold), (v) => actions.patch({ "text.bold": v })));
31723
+ if (paths.has("text.italic") && !textField) bar.appendChild(this.toggle("Italic", ITALIC_ICON, common((d) => !!d.text?.italic), (v) => actions.patch({ "text.italic": v })));
31724
+ if (textField) bar.appendChild(this.iconBtn("Text", TYPE_ICON, () => this.toggleTextPanel(drawing, actions, !textOnBar)));
31490
31725
  const editableLevels = drawing.editableLevels();
31491
- if (editableLevels && !(drawing instanceof MachFigure)) {
31492
- const fib = drawing;
31726
+ if (editableLevels && every((d) => d.editableLevels() != null && !(d instanceof MachFigure))) {
31727
+ const fib = (d) => d;
31493
31728
  const sizes = TEXT_SIZE_OPTIONS.map((o) => o.value);
31494
- bar.appendChild(this.dropdown("Numbers size", sizes, fib.numbersSize ?? "small", (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel }));
31495
- bar.appendChild(this.dropdown("Label size", sizes, fib.labelsSize ?? "normal", (s) => labelSizeIcon(s), (v) => actions.patch({ labelsSize: v }), { label: sizeLabel }));
31729
+ bar.appendChild(this.dropdown("Numbers size", sizes, common((d) => fib(d).numbersSize ?? "small"), (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel }));
31730
+ bar.appendChild(this.dropdown("Label size", sizes, common((d) => fib(d).labelsSize ?? "normal"), (s) => labelSizeIcon(s), (v) => actions.patch({ labelsSize: v }), { label: sizeLabel }));
31496
31731
  }
31497
31732
  bar.appendChild(this.divider());
31498
- if (isFrvp) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "frvp")));
31499
- if (isPosition) bar.appendChild(this.iconBtn("Position size", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "position")));
31500
- if (editableLevels) bar.appendChild(this.iconBtn("Levels", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "levels")));
31501
- bar.appendChild(this.toggle("Lock", LOCK_ICON, drawing.locked, (v) => actions.setLocked(v)));
31733
+ if (!multi && isFrvp) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "frvp")));
31734
+ if (!multi && isPosition) bar.appendChild(this.iconBtn("Position size", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "position")));
31735
+ if (!multi && editableLevels) bar.appendChild(this.iconBtn("Levels", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "levels")));
31736
+ bar.appendChild(this.toggle("Lock", LOCK_ICON, common((d) => d.locked), (v) => actions.setLocked(v), UNLOCK_ICON));
31502
31737
  const del = this.iconBtn("Delete", TRASH_ICON, () => actions.remove());
31503
31738
  del.style.color = "var(--vela-danger)";
31504
31739
  bar.appendChild(del);
@@ -31684,7 +31919,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
31684
31919
  });
31685
31920
  return b;
31686
31921
  }
31687
- /** The trailing kebab overflow — z-order actions + reset settings. */
31922
+ /** The trailing kebab overflow — duplicate, z-order actions, reset settings. */
31688
31923
  kebabButton(actions) {
31689
31924
  const b = this.base("More");
31690
31925
  b.innerHTML = sized(KEBAB_ICON);
@@ -31696,6 +31931,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
31696
31931
  return;
31697
31932
  }
31698
31933
  this.openActionMenu(b, [
31934
+ { icon: CLONE_ICON, label: "Duplicate", onClick: () => actions.duplicate() },
31699
31935
  { icon: FRONT_ICON, label: "Bring to front", onClick: () => actions.reorder("front") },
31700
31936
  { icon: BACK_ICON, label: "Send to back", onClick: () => actions.reorder("back") },
31701
31937
  { icon: RESET_ICON, label: "Reset settings", onClick: () => actions.resetSettings() }
@@ -31852,26 +32088,30 @@ ${overlayScrollbarCss(".vela-sd-pane")}
31852
32088
  this.tipEl?.remove();
31853
32089
  this.tipEl = null;
31854
32090
  }
31855
- toggle(tip, icon2, active, onChange) {
32091
+ /** An on/off button. A mixed state (the selected drawings disagree) reads as half-lit and
32092
+ * the first click turns it ON for all of them. `off` swaps in a distinct glyph while the
32093
+ * toggle is off (a lock reads as an open padlock until it is engaged); mixed shows `icon`. */
32094
+ toggle(tip, icon2, active, onChange, off) {
31856
32095
  const b = this.base(tip);
31857
- b.innerHTML = sized(icon2);
31858
32096
  const set = (on2) => {
31859
- b.dataset.active = on2 ? "1" : "0";
32097
+ b.dataset.active = on2 === MIXED ? "mixed" : on2 ? "1" : "0";
32098
+ b.innerHTML = sized(off && on2 === false ? off : icon2);
31860
32099
  };
31861
32100
  set(active);
31862
32101
  let on = active;
31863
32102
  b.addEventListener("click", () => {
31864
- on = !on;
32103
+ on = on === MIXED ? true : !on;
31865
32104
  set(on);
31866
32105
  onChange(on);
31867
32106
  });
31868
32107
  return b;
31869
32108
  }
31870
32109
  /** An inline numeric width field for tools whose stroke range outgrows the 1–4px
31871
- * ladder — the value is clamped to the schema's declared min/max on commit. */
32110
+ * ladder — the value is clamped to the schema's declared min/max on commit. A mixed
32111
+ * value shows an empty field with a dash until a number is typed for all. */
31872
32112
  widthInput(tip, value, min2, max2, step, onChange) {
31873
32113
  const ni = new NumberInput({
31874
- value,
32114
+ value: value === MIXED ? min2 : value,
31875
32115
  min: min2,
31876
32116
  max: max2,
31877
32117
  step,
@@ -31883,13 +32123,18 @@ ${overlayScrollbarCss(".vela-sd-pane")}
31883
32123
  title: tip,
31884
32124
  onChange
31885
32125
  });
32126
+ if (value === MIXED) {
32127
+ ni.input.value = "";
32128
+ ni.input.placeholder = "\u2014";
32129
+ }
31886
32130
  ni.el.dataset.tip = tip;
31887
32131
  trapChartKeys(ni.el);
31888
32132
  return ni.el;
31889
32133
  }
31890
32134
  /** A pick-one dropdown: the trigger shows the current value's glyph (plus an optional inline
31891
32135
  * text label — e.g. `2px` — for controls that would otherwise look alike), and clicking it
31892
- * opens a floating list of every option so a value is one click away (no cycling through). */
32136
+ * opens a floating list of every option so a value is one click away (no cycling through).
32137
+ * A mixed value shows a dash in the trigger and highlights no row. */
31893
32138
  dropdown(tip, values, current, render, onChange, opts = {}) {
31894
32139
  const b = this.base(tip);
31895
32140
  b.style.width = "auto";
@@ -31899,14 +32144,20 @@ ${overlayScrollbarCss(".vela-sd-pane")}
31899
32144
  let cur = current;
31900
32145
  const paint = (v) => {
31901
32146
  b.replaceChildren();
31902
- const glyph = render(v);
32147
+ if (v === MIXED) {
32148
+ const dash = document.createElement("span");
32149
+ dash.textContent = "\u2014";
32150
+ dash.style.cssText = "min-width:18px;text-align:center;opacity:0.85;";
32151
+ b.appendChild(dash);
32152
+ }
32153
+ const glyph = v === MIXED ? "" : render(v);
31903
32154
  if (glyph) {
31904
32155
  const ic = document.createElement("span");
31905
32156
  ic.style.cssText = "display:flex;";
31906
32157
  ic.innerHTML = sized(glyph);
31907
32158
  b.appendChild(ic);
31908
32159
  }
31909
- if (opts.label && opts.labelInTrigger) {
32160
+ if (opts.label && opts.labelInTrigger && v !== MIXED) {
31910
32161
  const tx = document.createElement("span");
31911
32162
  tx.textContent = opts.label(v);
31912
32163
  tx.style.cssText = "font-size:11px;opacity:0.85;font-variant-numeric:tabular-nums;";
@@ -31976,8 +32227,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
31976
32227
  }
31977
32228
  /** A color control: an `icon` over a thin **colored underline** showing the current
31978
32229
  * value (the common idiom), with an invisible native picker overlaid to edit it.
31979
- * `iconSize` shrinks it for the floating text controls. */
31980
- colorButton(tip, icon2, color, onChange, iconSize = 17) {
32230
+ * `iconSize` shrinks it for the floating text controls. A mixed color paints the
32231
+ * underline striped with the colors `used` across the selection, and the picker then
32232
+ * leads with those colors so unifying onto one of them is a single click. */
32233
+ colorButton(tip, icon2, color, onChange, iconSize = 17, used = []) {
31981
32234
  const b = document.createElement("button");
31982
32235
  b.type = "button";
31983
32236
  b.dataset.tip = tip;
@@ -31987,13 +32240,14 @@ ${overlayScrollbarCss(".vela-sd-pane")}
31987
32240
  ic.style.cssText = "display:flex;";
31988
32241
  ic.innerHTML = sized(icon2, iconSize);
31989
32242
  const bar = document.createElement("span");
31990
- bar.style.cssText = `display:block;height:3px;width:${Math.round(iconSize * 0.85)}px;border-radius:2px;background:${color};`;
32243
+ bar.style.cssText = `display:block;height:3px;width:${Math.round(iconSize * 0.85)}px;border-radius:2px;background:${color === MIXED ? stripes(used) : color};`;
31991
32244
  b.append(ic, bar);
31992
32245
  let cur = color;
31993
32246
  b.addEventListener("pointerdown", (e) => e.stopPropagation());
31994
32247
  b.addEventListener("click", (e) => {
31995
32248
  e.stopPropagation();
31996
- this.toggleColorPopover(b, cur, (v) => {
32249
+ const seed = cur === MIXED ? used[0] ?? DEFAULT_DRAWING_COLOR : cur;
32250
+ this.toggleColorPopover(b, seed, cur === MIXED ? used : [], (v) => {
31997
32251
  cur = v;
31998
32252
  bar.style.background = v;
31999
32253
  onChange(v);
@@ -32002,20 +32256,42 @@ ${overlayScrollbarCss(".vela-sd-pane")}
32002
32256
  return b;
32003
32257
  }
32004
32258
  /** Open the color popover for `anchor`, or close it if it's already this anchor's (toggle). */
32005
- toggleColorPopover(anchor, color, onChange) {
32259
+ toggleColorPopover(anchor, color, used, onChange) {
32006
32260
  if (this.colorOwner === anchor) {
32007
32261
  this.closeColorPopover();
32008
32262
  return;
32009
32263
  }
32010
- this.openColorPopover(anchor, color, onChange);
32264
+ this.openColorPopover(anchor, color, used, onChange);
32011
32265
  }
32012
- /** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`. */
32013
- openColorPopover(anchor, color, onChange) {
32266
+ /** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`.
32267
+ * `used` (a mixed swatch's colors) leads the picker as one-click unify shortcuts. */
32268
+ openColorPopover(anchor, color, used, onChange) {
32014
32269
  const t = this.theme;
32015
32270
  this.colorPop = this.hostFloat(anchor, {
32016
32271
  zIndex: 25,
32017
32272
  padding: "10px",
32018
- fill: (el) => {
32273
+ fill: (el, pop) => {
32274
+ if (used.length > 1) {
32275
+ const row = document.createElement("div");
32276
+ row.style.cssText = "display:flex;align-items:center;gap:6px;padding-bottom:9px;border-bottom:1px solid var(--vela-border);";
32277
+ const lbl = document.createElement("span");
32278
+ lbl.textContent = "In use";
32279
+ lbl.style.cssText = "font:var(--vela-font-size-sm) inherit;opacity:0.7;margin-right:2px;";
32280
+ row.appendChild(lbl);
32281
+ for (const c of used) {
32282
+ const sw = document.createElement("button");
32283
+ sw.type = "button";
32284
+ sw.dataset.tip = c;
32285
+ sw.style.cssText = `width:18px;height:18px;border-radius:4px;border:1px solid var(--vela-border-strong);cursor:pointer;background:${c};padding:0;`;
32286
+ sw.addEventListener("click", (e) => {
32287
+ e.stopPropagation();
32288
+ pop.hide();
32289
+ onChange(c);
32290
+ });
32291
+ row.appendChild(sw);
32292
+ }
32293
+ el.appendChild(row);
32294
+ }
32019
32295
  el.appendChild(buildColorPicker(color, t, onChange));
32020
32296
  }
32021
32297
  });
@@ -32036,6 +32312,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
32036
32312
  var PRICE_DELTA_ICON = icon("price-delta");
32037
32313
  var DATE_DELTA_ICON = icon("date-delta");
32038
32314
  var LOCK_ICON = icon("lock");
32315
+ var UNLOCK_ICON = icon("unlock");
32039
32316
  var FRONT_ICON = icon("bring-front");
32040
32317
  var BACK_ICON = icon("send-back");
32041
32318
  var TRASH_ICON = icon("trash");
@@ -32043,6 +32320,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
32043
32320
  var ITALIC_ICON = icon("italic");
32044
32321
  var GRIP_ICON = icon("grip");
32045
32322
  var KEBAB_ICON = icon("kebab");
32323
+ var CLONE_ICON = icon("clone");
32046
32324
  var RESET_ICON = icon("reset");
32047
32325
  var GEAR_ICON = icon("gear");
32048
32326
  var CHEVRON_ICON = icon("chevron-down");
@@ -32051,6 +32329,12 @@ ${overlayScrollbarCss(".vela-sd-pane")}
32051
32329
  var DEDEKIND_ICON2 = icon("dedekind");
32052
32330
  var SONIC_ICON2 = icon("sonic");
32053
32331
  var SUPERSONIC_ICON2 = icon("supersonic");
32332
+ function stripes(colors) {
32333
+ const bands = colors.length > 1 ? colors : ["var(--vela-fg-faint)", "var(--vela-fg-muted)"];
32334
+ const step = 100 / bands.length;
32335
+ const stops = bands.map((c, i) => `${c} ${i * step}% ${(i + 1) * step}%`).join(",");
32336
+ return `linear-gradient(90deg,${stops})`;
32337
+ }
32054
32338
  function lineIcon(width, style) {
32055
32339
  const dash = style === "dashed" ? "5,3" : style === "dotted" ? "1.5,3" : "";
32056
32340
  return svg24(`<line x1="3.5" y1="12" x2="20.5" y2="12" stroke-width="${Number(width) || 2}" stroke-dasharray="${dash}"/>`);
@@ -32966,6 +33250,20 @@ ${overlayScrollbarCss(".vela-sd-pane")}
32966
33250
  // reported exactly when it actually changes).
32967
33251
  this.textEditor = null;
32968
33252
  this.painter = new DrawingPainter();
33253
+ /** The open popup is the multi-selection bar (vs one drawing's own) — decides what a
33254
+ * shrinking selection does with it. */
33255
+ this.popupMulti = false;
33256
+ /** Dismiss-on-outside-press also clears the selection — except when the press is ABOUT the
33257
+ * selection: it carries a multi-select modifier (Shift / Ctrl / Cmd — it will add to the
33258
+ * selection or sweep a marquee onto it), or it landed on a drawing that is already selected
33259
+ * (the canvas handler ran first and is holding it for a drag or a click), so the drawings
33260
+ * whose bar just closed must stay selected. */
33261
+ this.onPopupDismissed = (e) => {
33262
+ if (e && (e.shiftKey || e.ctrlKey || e.metaKey)) return;
33263
+ const pressed = this.interaction.pressedId();
33264
+ if (pressed && this.selectedIds.has(pressed)) return;
33265
+ this.clearSelection();
33266
+ };
32969
33267
  this.ctx = canvas.getContext("2d");
32970
33268
  this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), () => deps.chartBarMs());
32971
33269
  this.toolbar = new DrawingToolbar(
@@ -33142,6 +33440,12 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33142
33440
  }
33143
33441
  setSelection(ids) {
33144
33442
  this.selectedIds = new Set(ids);
33443
+ if (ids.length >= 2) this.openSettingsForSelection(ids);
33444
+ else if (this.popupMulti) {
33445
+ this.popupMulti = false;
33446
+ this.popup.close();
33447
+ if (ids.length === 1) this.openSettingsById(ids[0], 0, 0);
33448
+ }
33145
33449
  this.render();
33146
33450
  }
33147
33451
  /** Programmatic twin of clicking the drawing: highlight it + float its settings popup. */
@@ -33187,12 +33491,16 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33187
33491
  });
33188
33492
  }
33189
33493
  /** Delete the (unlocked) drawing under the cursor. True when one was removed.
33190
- * Shared by the eraser (click + drag) and the middle-click shortcut. */
33191
- deleteAt(x, y) {
33494
+ * Shared by the eraser (click + drag) and the middle-click shortcut; the latter passes
33495
+ * `withSelection` so a hit on a member of a multi-selection removes the selection's
33496
+ * unlocked members — whichever member was hit, locked or not. */
33497
+ deleteAt(x, y, withSelection = false) {
33192
33498
  const hit = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE);
33193
- if (!hit || hit.locked) return false;
33499
+ if (!hit) return false;
33500
+ const ids = deleteTargets(hit, this.selectedIds, this.drawings, withSelection);
33501
+ if (ids.length === 0) return false;
33194
33502
  this.popup.close();
33195
- this.emit({ kind: "delete", ids: [hit.id] });
33503
+ this.emit({ kind: "delete", ids });
33196
33504
  return true;
33197
33505
  }
33198
33506
  /** Shift+press on the empty plot: arm the measure ruler AND start it at (x, y) in one
@@ -33218,7 +33526,14 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33218
33526
  this.render();
33219
33527
  }
33220
33528
  }
33221
- pointerDown(x, y, snap = "off", shift4 = false) {
33529
+ /** Ctrl/Cmd+press on the empty plot: sweep a selection box (adds the drawings it touches to
33530
+ * the selection). Returns false when a mode/tool is active (the normal press path owns it). */
33531
+ beginMarqueeAt(x, y) {
33532
+ if (this.measureMode || this.eraserMode || !this.interaction.beginMarquee(x, y)) return false;
33533
+ this.render();
33534
+ return true;
33535
+ }
33536
+ pointerDown(x, y, snap = "off", shift4 = false, mod = false) {
33222
33537
  if (this.eraserMode) {
33223
33538
  this.erasing = true;
33224
33539
  this.deleteAt(x, y);
@@ -33238,9 +33553,9 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33238
33553
  return;
33239
33554
  }
33240
33555
  }
33241
- this.interaction.down(x, y, snap, shift4);
33556
+ this.interaction.down(x, y, snap, shift4, mod);
33242
33557
  }
33243
- pointerMove(x, y, snap = "off", shift4 = false) {
33558
+ pointerMove(x, y, snap = "off", shift4 = false, mod = false) {
33244
33559
  if (this.eraserMode) {
33245
33560
  if (this.erasing) this.deleteAt(x, y);
33246
33561
  return;
@@ -33252,12 +33567,14 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33252
33567
  return;
33253
33568
  }
33254
33569
  this.interaction.move(x, y, snap, shift4);
33255
- this.updateHover(x, y);
33570
+ this.updateHover(x, y, mod);
33256
33571
  }
33257
- /** Track which drawing is hovered so its handles appear on hover (and only then). */
33258
- updateHover(x, y) {
33572
+ /** Track which drawing is hovered so its handles appear on hover (and only then). While
33573
+ * Ctrl/Cmd is held the cursor is picking a selection — handles then mark only what IS
33574
+ * selected, so a hovered candidate doesn't already read as selected. */
33575
+ updateHover(x, y, mod = false) {
33259
33576
  let id = null;
33260
- if (this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
33577
+ if (!mod && this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
33261
33578
  id = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE)?.id ?? null;
33262
33579
  }
33263
33580
  if (id !== this.hoveredId) {
@@ -33277,7 +33594,12 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33277
33594
  this.render();
33278
33595
  return;
33279
33596
  }
33597
+ const dragged = this.interaction.isDragging() ? this.interaction.pressedId() : null;
33280
33598
  this.interaction.up(x, y);
33599
+ if (dragged && this.selectedIds.has(dragged) && !this.popup.isOpen()) {
33600
+ if (this.selectedIds.size >= 2) this.openSettingsForSelection(this.selectionIds());
33601
+ else this.openSettingsById(dragged, x, y);
33602
+ }
33281
33603
  }
33282
33604
  /** Toggle the transient ruler. Arming it clears any drawing tool + selection. */
33283
33605
  toggleMeasure() {
@@ -33542,7 +33864,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33542
33864
  this.emit({ kind: "duplicate", ids: this.selectionIds() });
33543
33865
  break;
33544
33866
  case "delete": {
33545
- const ids = this.selectedIds.size ? this.selectionIds() : this.hoveredId ? [this.hoveredId] : [];
33867
+ const ids = this.selectedIds.size ? this.deletableSelection() : this.hoveredId ? [this.hoveredId] : [];
33546
33868
  if (ids.length) {
33547
33869
  this.popup.close();
33548
33870
  this.emit({ kind: "delete", ids });
@@ -33558,6 +33880,9 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33558
33880
  selectionIds() {
33559
33881
  return [...this.selectedIds];
33560
33882
  }
33883
+ deletableSelection() {
33884
+ return deletableSelection(this.selectedIds, this.drawings);
33885
+ }
33561
33886
  /** Move every selected (unlocked) drawing by a pixel delta — one edit/edit-many → one undo step. */
33562
33887
  nudgeSelection(dx, dy) {
33563
33888
  const proj = this.deps.projector();
@@ -33666,6 +33991,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33666
33991
  this.painter.seriesLook = this.deps.seriesLook();
33667
33992
  this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
33668
33993
  this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
33994
+ const clones = this.interaction.dragClones();
33995
+ if (clones) this.painter.paintAll(ctx, clones, proj, this.deps.theme(), { selected: new Set(clones.map((c) => c.id)) });
33669
33996
  this.layoutTextEditor();
33670
33997
  const ghost = this.interaction.ghost();
33671
33998
  if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
@@ -33681,6 +34008,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33681
34008
  const my = m ? proj.yOf(m.point.price, m.paneId) : null;
33682
34009
  if (m && my != null) this.painter.paintSnapRing(ctx, proj.xOf(m.point.time), my, this.deps.theme());
33683
34010
  if (this.measure.isActive()) this.measure.paint(ctx, proj, this.deps.theme());
34011
+ const marquee = this.interaction.marqueeRect();
34012
+ if (marquee) this.painter.paintMarquee(ctx, marquee);
33684
34013
  }
33685
34014
  destroy() {
33686
34015
  this.closeTextEditor();
@@ -33698,15 +34027,80 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33698
34027
  if (this.selectedIds.size) this.emit({ kind: "select", ids: [] });
33699
34028
  this.render();
33700
34029
  }
34030
+ /** Report the live drawings' current state as one edit (one undo step, however many). */
34031
+ emitEdits(drawings) {
34032
+ const docs = drawings.map((d) => d.serialize());
34033
+ if (docs.length === 1) this.emit({ kind: "edit", doc: docs[0] });
34034
+ else if (docs.length > 1) this.emit({ kind: "edit-many", docs });
34035
+ }
34036
+ /** The one bar for a multi-selection: it shows the controls all of its drawings share and
34037
+ * every action applies to all of them. Floats clear of their combined bounds. */
34038
+ openSettingsForSelection(ids) {
34039
+ const live = () => ids.map((id) => this.drawings.find((d) => d.id === id)).filter((d) => d != null);
34040
+ const drawings = live();
34041
+ if (drawings.length < 2) return;
34042
+ const proj = this.deps.projector();
34043
+ let anchor = null;
34044
+ for (const d of drawings) {
34045
+ const b = d.bounds(proj);
34046
+ if (!b) continue;
34047
+ if (!anchor) anchor = { ...b };
34048
+ else {
34049
+ const right = Math.max(anchor.x + anchor.w, b.x + b.w);
34050
+ const bottom = Math.max(anchor.y + anchor.h, b.y + b.h);
34051
+ anchor.x = Math.min(anchor.x, b.x);
34052
+ anchor.y = Math.min(anchor.y, b.y);
34053
+ anchor.w = right - anchor.x;
34054
+ anchor.h = bottom - anchor.y;
34055
+ }
34056
+ }
34057
+ this.popupMulti = true;
34058
+ this.emit({ kind: "settings", id: drawings[0].id });
34059
+ this.popup.open(drawings, anchor, {
34060
+ resolve: () => live()[0] ?? null,
34061
+ patch: (p) => {
34062
+ const ds = live();
34063
+ for (const d of ds) d.applySettings(p);
34064
+ this.render();
34065
+ this.emitEdits(ds);
34066
+ },
34067
+ setLocked: (v) => {
34068
+ const ds = live();
34069
+ for (const d of ds) d.locked = v;
34070
+ this.emitEdits(ds);
34071
+ },
34072
+ reorder: (to) => {
34073
+ for (const d of live()) this.emit({ kind: "reorder", id: d.id, to });
34074
+ },
34075
+ duplicate: () => {
34076
+ this.popup.close();
34077
+ this.emit({ kind: "duplicate", ids: live().map((d) => d.id) });
34078
+ },
34079
+ resetSettings: () => {
34080
+ const ds = live();
34081
+ for (const d of ds) resetDrawingSettings(d);
34082
+ this.render();
34083
+ this.emitEdits(ds);
34084
+ this.openSettingsForSelection(ids);
34085
+ },
34086
+ remove: () => {
34087
+ const ids2 = live().filter((d) => !d.locked).map((d) => d.id);
34088
+ if (ids2.length === 0) return;
34089
+ this.popup.close();
34090
+ this.emit({ kind: "delete", ids: ids2 });
34091
+ }
34092
+ }, this.onPopupDismissed);
34093
+ }
33701
34094
  /** A click on a drawing opens its settings toolbar (text labels edit text here too). */
33702
34095
  openSettingsById(id, _x, _y) {
33703
34096
  const drawing = this.drawings.find((d) => d.id === id);
33704
34097
  if (!drawing) return;
33705
34098
  const live = () => this.drawings.find((d) => d.id === id);
34099
+ this.popupMulti = false;
33706
34100
  this.emit({ kind: "select", ids: [id] });
33707
34101
  this.emit({ kind: "settings", id });
33708
34102
  const anchor = drawing.bounds(this.deps.projector());
33709
- this.popup.open(drawing, anchor, {
34103
+ this.popup.open([drawing], anchor, {
33710
34104
  // Sync rebuilds instances, so a panel that reads values back after a patch (e.g. the
33711
34105
  // position tool's price fields, where one edit can flip another level) resolves fresh.
33712
34106
  resolve: () => live() ?? null,
@@ -33724,6 +34118,11 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33724
34118
  this.emit({ kind: "edit", doc: d.serialize() });
33725
34119
  },
33726
34120
  reorder: (to) => this.emit({ kind: "reorder", id, to }),
34121
+ duplicate: () => {
34122
+ this.finishTextEditor(true);
34123
+ this.popup.close();
34124
+ this.emit({ kind: "duplicate", ids: [id] });
34125
+ },
33727
34126
  resetSettings: () => {
33728
34127
  const d = live();
33729
34128
  if (!d) return;
@@ -33746,7 +34145,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33746
34145
  this.popup.close();
33747
34146
  this.emit({ kind: "delete", ids: [id] });
33748
34147
  }
33749
- }, () => this.clearSelection());
34148
+ }, this.onPopupDismissed);
34149
+ }
34150
+ /** A plain click on the empty plot: drop the selection. The popup's dismiss-on-outside-press
34151
+ * does the same, but a selection can exist with no bar open (a lone Ctrl-click pick, a
34152
+ * selection whose bar was dismissed by a modifier press) — this covers those. */
34153
+ deselect() {
34154
+ this.clearSelection();
33750
34155
  }
33751
34156
  /** Report placement progress upstream (`draft` intent) so the drawings sync can
33752
34157
  * mirror the ghost live on linked charts. Called from every render; only actual
@@ -33772,6 +34177,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33772
34177
  }, 0);
33773
34178
  return;
33774
34179
  }
34180
+ if (i.kind === "clone") {
34181
+ const before = new Set(this.drawings.map((d) => d.id));
34182
+ this.intentCb?.(i);
34183
+ const fresh = this.drawings.filter((d) => !before.has(d.id));
34184
+ if (fresh.length === 1) this.openSettingsById(fresh[0].id, 0, 0);
34185
+ return;
34186
+ }
33775
34187
  this.intentCb?.(i);
33776
34188
  }
33777
34189
  };
@@ -34936,7 +35348,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34936
35348
  var ZOOM_TAU_MS = 70;
34937
35349
  var SCALE_TAU_MS = 80;
34938
35350
  var FLING_TAU_MS = 110;
34939
- var LIVE_BAR_TAU_MS = 90;
34940
35351
  var SCROLL_TO_TAU_MS = 130;
34941
35352
  var FLING_STOP_PX = 0.02;
34942
35353
  var PRICE_SCALE_K = 4e-3;
@@ -35007,9 +35418,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35007
35418
  /** Drawings layer self-serves Ctrl+Z/Y (see the `historyChords` feature). */
35008
35419
  this.historyChordsEnabled = true;
35009
35420
  this.liveRegion = null;
35010
- // ── animation state (eased zoom + inertial pan) ──
35421
+ // ── animation state (eased zoom + inertial pan + live-bar glide) ──
35011
35422
  this.animZoom = true;
35012
35423
  this.animPan = true;
35424
+ this.animLiveBarMs = 0;
35425
+ // forming-bar OHLC glide time-constant; 0 = each tick snaps
35426
+ this.animLiveBarOnMs = LIVE_BAR_EASE_DEFAULT_MS;
35427
+ // the duration the settings-dialog on/off toggle restores (last non-zero value configured)
35013
35428
  // Brand default candles.
35014
35429
  this.candleUp = BULLISH;
35015
35430
  this.candleDown = BEARISH;
@@ -35134,7 +35549,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35134
35549
  this.moveIndicatorCbs = /* @__PURE__ */ new Set();
35135
35550
  this.priceStyleCbs = /* @__PURE__ */ new Set();
35136
35551
  this.name = "native";
35137
- this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "intro", "zoomAnchor", "axisDrag", "paneResize", "candleZOrder", "candleVisible", "seriesOrder", "highlights", "sessionZones", "gridlines", "axisLabels", "scaleMode", "invertScale", "paneScales", "autoScale", "timezone", "keyboard", "historyChords", "priceStyle", "priceBaseline", "baselinePrice", "settings", "attribution", "dialogHost", "tradeMarkers", "indicatorTitles", "indicatorValues"];
35552
+ this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "animLiveBar", "intro", "zoomAnchor", "axisDrag", "paneResize", "candleZOrder", "candleVisible", "seriesOrder", "highlights", "sessionZones", "gridlines", "axisLabels", "scaleMode", "invertScale", "paneScales", "autoScale", "timezone", "keyboard", "historyChords", "priceStyle", "priceBaseline", "baselinePrice", "settings", "attribution", "dialogHost", "tradeMarkers", "indicatorTitles", "indicatorValues"];
35138
35553
  /** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
35139
35554
  * so moving onto the button doesn't count as leaving). */
35140
35555
  this.onScrollProximityMove = (e) => {
@@ -35168,6 +35583,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35168
35583
  this.backendMode = opts.nativeBackend;
35169
35584
  this.animZoom = opts.animZoom;
35170
35585
  this.animPan = opts.animPan;
35586
+ this.setLiveBarEase(resolveLiveBarEaseMs(opts.animLiveBar));
35171
35587
  this.glowAmount = opts.glow;
35172
35588
  this.candleUp = opts.upColor;
35173
35589
  this.candleDown = opts.downColor;
@@ -35217,6 +35633,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35217
35633
  case "animPan":
35218
35634
  this.animPan = Boolean(value);
35219
35635
  return;
35636
+ case "animLiveBar":
35637
+ this.setLiveBarEase(resolveLiveBarEaseMs(value));
35638
+ return;
35639
+ // affects the next tick only; a glide in flight finishes at the new rate (or snaps at 0)
35220
35640
  case "intro": {
35221
35641
  const s = value === false || value === "none" || value === "off" || value == null ? "" : String(value);
35222
35642
  this.introStyle = s;
@@ -35353,6 +35773,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35353
35773
  return this.animZoom;
35354
35774
  case "animPan":
35355
35775
  return this.animPan;
35776
+ case "animLiveBar":
35777
+ return this.animLiveBarMs;
35356
35778
  case "intro":
35357
35779
  return this.introStyle;
35358
35780
  case "zoomAnchor":
@@ -35521,7 +35943,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35521
35943
  labelsVisible: this.scene.showAxisLabels,
35522
35944
  currentPriceLine: this.scene.showPriceLine,
35523
35945
  priceLabel: this.scene.showPriceLabel,
35524
- countdown: this.scene.showCountdown
35946
+ countdown: this.scene.showCountdown,
35947
+ animateLastPrice: this.animLiveBarMs > 0
35525
35948
  },
35526
35949
  panes: { separatorColor: s.separatorColor ?? t.borderColor },
35527
35950
  trades: {
@@ -35628,6 +36051,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35628
36051
  this.scene.showPriceLabel = next2.priceScale.priceLabel;
35629
36052
  this.scene.showCountdown = next2.priceScale.countdown;
35630
36053
  this.syncCountdownTimer();
36054
+ this.animLiveBarMs = next2.priceScale.animateLastPrice ? this.animLiveBarOnMs : 0;
35631
36055
  s.separatorColor = keepInherit(s.separatorColor, next2.panes.separatorColor, prevTheme.borderColor);
35632
36056
  this.scene.tradeMarkers = {
35633
36057
  visible: next2.trades.visible,
@@ -36071,7 +36495,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36071
36495
  zoomTo: (target, anchorLogical, anchorX) => this.zoomTo(target, anchorLogical, anchorX),
36072
36496
  fling: (v) => this.fling(v),
36073
36497
  onPointerMove: (x, y) => this.handlePointerMove(x, y),
36074
- onClick: (x) => this.handleClick(x),
36498
+ onClick: (x) => {
36499
+ this.userDrawings?.deselect();
36500
+ this.handleClick(x);
36501
+ },
36075
36502
  onAxisLongPress: (axis, x, y) => {
36076
36503
  for (const cb of this.axisLongPressCbs) cb({ axis, x, y });
36077
36504
  },
@@ -36089,11 +36516,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36089
36516
  // User drawings claim a gesture before pan when armed / over a drawing.
36090
36517
  drawingsClaim: (x, y) => this.userDrawings?.claim(x, y) ?? false,
36091
36518
  drawingsMeasureStart: (x, y, snap) => this.userDrawings?.beginMeasureAt(x, y, snap) ?? false,
36092
- drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y) ?? false,
36519
+ drawingsMarqueeStart: (x, y) => this.userDrawings?.beginMarqueeAt(x, y) ?? false,
36520
+ drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y, true) ?? false,
36521
+ // middle-click: a selected hit takes the whole selection
36093
36522
  drawingsCancelPlacement: () => this.userDrawings?.cancelPlacement() ?? false,
36094
36523
  drawingsSnapMode: () => this.snapMode,
36095
- drawingsPointerDown: (x, y, snap, shift4) => this.userDrawings?.pointerDown(x, y, snap, shift4),
36096
- drawingsPointerMove: (x, y, snap, shift4) => this.userDrawings?.pointerMove(x, y, snap, shift4),
36524
+ drawingsPointerDown: (x, y, snap, shift4, mod) => this.userDrawings?.pointerDown(x, y, snap, shift4, mod),
36525
+ drawingsPointerMove: (x, y, snap, shift4, mod) => this.userDrawings?.pointerMove(x, y, snap, shift4, mod),
36097
36526
  drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
36098
36527
  drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
36099
36528
  drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
@@ -36444,8 +36873,11 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36444
36873
  const last2 = this.bars[n - 1];
36445
36874
  if (last2 && bar.time === last2.time) {
36446
36875
  this.bars[n - 1] = bar;
36447
- if (this.liveEaseTime !== bar.time) this.syncLiveEase(bar);
36448
- this.animator.start();
36876
+ if (this.animLiveBarMs <= 0 || this.liveEaseTime !== bar.time) {
36877
+ this.syncLiveEase(bar);
36878
+ } else {
36879
+ this.animator.start();
36880
+ }
36449
36881
  } else if (!last2 || bar.time > last2.time) {
36450
36882
  this.bars.push(bar);
36451
36883
  this.syncLiveEase(bar);
@@ -36457,6 +36889,12 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36457
36889
  this.scene.bars = this.bars;
36458
36890
  this.scheduler.invalidate(4 /* Full */);
36459
36891
  }
36892
+ /** Set the live-bar glide duration (0 = off). A non-zero value is also remembered as
36893
+ * what the config's on/off toggle (`priceScale.animateLastPrice`) switches back on to. */
36894
+ setLiveBarEase(ms) {
36895
+ this.animLiveBarMs = ms;
36896
+ if (ms > 0) this.animLiveBarOnMs = ms;
36897
+ }
36460
36898
  /** Snap the eased forming-bar state to `bar` — no glide (a fresh bar or the first tick of one). */
36461
36899
  syncLiveEase(bar) {
36462
36900
  this.liveEaseTime = bar.time;
@@ -36469,9 +36907,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36469
36907
  const target = this.bars[this.bars.length - 1];
36470
36908
  if (!target || this.liveEaseTime !== target.time) return false;
36471
36909
  const eps = Math.max(1e-9, Math.abs(target.close) * 1e-6);
36472
- const nh = easeToward(this.liveEaseHigh, target.high, dtMs, LIVE_BAR_TAU_MS);
36473
- const nl = easeToward(this.liveEaseLow, target.low, dtMs, LIVE_BAR_TAU_MS);
36474
- const nc = easeToward(this.liveEaseClose, target.close, dtMs, LIVE_BAR_TAU_MS);
36910
+ const tau = this.animLiveBarMs;
36911
+ const nh = easeToward(this.liveEaseHigh, target.high, dtMs, tau);
36912
+ const nl = easeToward(this.liveEaseLow, target.low, dtMs, tau);
36913
+ const nc = easeToward(this.liveEaseClose, target.close, dtMs, tau);
36475
36914
  const active = Math.abs(nh - target.high) > eps || Math.abs(nl - target.low) > eps || Math.abs(nc - target.close) > eps;
36476
36915
  this.liveEaseHigh = active ? nh : target.high;
36477
36916
  this.liveEaseLow = active ? nl : target.low;
@@ -38446,6 +38885,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
38446
38885
  const v = inputs[key];
38447
38886
  return typeof v === "string" && v !== "" ? v : fallback;
38448
38887
  }
38888
+ function bool(inputs, key, fallback) {
38889
+ const v = inputs[key];
38890
+ return typeof v === "boolean" ? v : fallback;
38891
+ }
38449
38892
  function toPoints(bars, values, colors) {
38450
38893
  const pts = new Array(bars.length);
38451
38894
  for (let i = 0; i < bars.length; i++) {
@@ -39977,10 +40420,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
39977
40420
  plots: [
39978
40421
  { key: "up", title: "Aroon up", values: up, color: BULLISH, width: 2 },
39979
40422
  { key: "down", title: "Aroon down", values: down, color: BEARISH, width: 2 }
39980
- ],
39981
- levels: [
39982
- { key: "upper", price: 100, color: NEUTRAL, lineStyle: "dotted" },
39983
- { key: "lower", price: 0, color: NEUTRAL, lineStyle: "dotted" }
39984
40423
  ]
39985
40424
  };
39986
40425
  }
@@ -40478,8 +40917,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
40478
40917
  var DAY_MS2 = 864e5;
40479
40918
  function periodKey(anchor, time) {
40480
40919
  if (anchor === "Week") return Math.floor((Math.floor(time / DAY_MS2) + 3) / 7);
40481
- if (anchor === "Month") {
40920
+ if (anchor === "Month" || anchor === "Quarter" || anchor === "Year") {
40482
40921
  const d = new Date(time);
40922
+ if (anchor === "Year") return d.getUTCFullYear();
40923
+ if (anchor === "Quarter") return d.getUTCFullYear() * 4 + Math.floor(d.getUTCMonth() / 3);
40483
40924
  return d.getUTCFullYear() * 12 + d.getUTCMonth();
40484
40925
  }
40485
40926
  return Math.floor(time / DAY_MS2);
@@ -40531,40 +40972,86 @@ ${overlayScrollbarCss(".vela-sd-pane")}
40531
40972
  return { plots: [{ key: "sar", title: "PSAR", values, kind: "circles", color: SERIES_LINE, colors }] };
40532
40973
  }
40533
40974
  };
40975
+ var VWAP_BANDS_GROUP = "Bands";
40976
+ var VWAP_STYLE = "Style";
40977
+ var VWAP_BANDS = [
40978
+ { on: "band1", mult: "band1Mult", color: "band1Color", fill: "band1Fill", fillColor: "band1FillColor", defMult: 1, defOn: true, defColor: BULLISH },
40979
+ { on: "band2", mult: "band2Mult", color: "band2Color", fill: "band2Fill", fillColor: "band2FillColor", defMult: 2, defOn: false, defColor: WARNING },
40980
+ { on: "band3", mult: "band3Mult", color: "band3Color", fill: "band3Fill", fillColor: "band3FillColor", defMult: 3, defOn: false, defColor: CATEGORICAL[4] }
40981
+ ];
40534
40982
  var vwap = {
40535
40983
  type: "vwap",
40536
40984
  title: "Volume Weighted Average Price",
40537
40985
  shortTitle: "VWAP",
40538
40986
  overlay: true,
40539
40987
  inputs: [
40540
- { key: "anchor", title: "Anchor", type: "string", defval: "Day", options: ["Day", "Week", "Month"], tooltip: "Where the accumulation resets (UTC periods)" },
40541
- { key: "source", title: "Source", type: "string", defval: "HLC3", options: ["HLC3", "OHLC4", "Close"] },
40542
- colorInput(INFO)
40988
+ { key: "anchor", title: "Period", type: "string", defval: "Day", options: ["Day", "Week", "Month", "Quarter", "Year"], tooltip: "Where the accumulation resets (UTC periods)" },
40989
+ sourceInput("HLC3"),
40990
+ // Each band is one row: its toggle leads, the multiplier follows unlabeled.
40991
+ ...VWAP_BANDS.flatMap((b, i) => [
40992
+ { key: b.on, title: `Band ${i + 1} multiplier`, type: "bool", defval: b.defOn, inline: b.on, group: VWAP_BANDS_GROUP },
40993
+ { key: b.mult, title: "", type: "float", defval: b.defMult, min: 0.1, max: 10, step: 0.1, inline: b.on, group: VWAP_BANDS_GROUP }
40994
+ ]),
40995
+ { ...colorInput(INFO, "VWAP color"), group: VWAP_STYLE },
40996
+ // One row per band: its ink, then the fill toggle with the fill's own (translucent) color.
40997
+ ...VWAP_BANDS.flatMap((b, i) => [
40998
+ { ...colorInput(b.defColor, `Band ${i + 1} color`, b.color), inline: b.color, group: VWAP_STYLE },
40999
+ { key: b.fill, title: "Fill", type: "bool", defval: true, inline: b.color, group: VWAP_STYLE },
41000
+ { ...colorInput(withAlpha2(b.defColor), "", b.fillColor), inline: b.color, group: VWAP_STYLE }
41001
+ ])
40543
41002
  ],
40544
41003
  compute: (bars, inputs) => {
40545
41004
  const anchor = str3(inputs, "anchor", "Day");
40546
- const source = str3(inputs, "source", "HLC3");
40547
- const values = new Array(bars.length).fill(Number.NaN);
41005
+ const src = sourceValues(bars, str3(inputs, "source", "HLC3"));
41006
+ const n = bars.length;
41007
+ const values = new Array(n).fill(Number.NaN);
41008
+ const bands = VWAP_BANDS.filter((b) => bool(inputs, b.on, b.defOn)).map((b) => ({
41009
+ mult: Math.max(0, num3(inputs, b.mult, b.defMult)),
41010
+ color: str3(inputs, b.color, b.defColor),
41011
+ fill: bool(inputs, b.fill, true),
41012
+ fillColor: str3(inputs, b.fillColor, withAlpha2(b.defColor)),
41013
+ up: new Array(n).fill(Number.NaN),
41014
+ down: new Array(n).fill(Number.NaN)
41015
+ }));
40548
41016
  let period = Number.NaN;
40549
41017
  let cumPV = 0;
41018
+ let cumPV2 = 0;
40550
41019
  let cumV = 0;
40551
- for (let i = 0; i < bars.length; i++) {
41020
+ for (let i = 0; i < n; i++) {
40552
41021
  const b = bars[i];
40553
41022
  const key = periodKey(anchor, b.time);
40554
41023
  if (key !== period) {
40555
41024
  period = key;
40556
41025
  cumPV = 0;
41026
+ cumPV2 = 0;
40557
41027
  cumV = 0;
40558
41028
  }
40559
41029
  const v = b.volume;
40560
41030
  if (v != null && Number.isFinite(v) && v > 0) {
40561
- const tp = source === "Close" ? b.close : source === "OHLC4" ? (b.open + b.high + b.low + b.close) / 4 : (b.high + b.low + b.close) / 3;
41031
+ const tp = src[i];
40562
41032
  cumPV += tp * v;
41033
+ cumPV2 += tp * tp * v;
40563
41034
  cumV += v;
40564
41035
  }
40565
- if (cumV > 0) values[i] = cumPV / cumV;
40566
- }
40567
- return { plots: [{ key: "vwap", title: "VWAP", values, color: str3(inputs, "color", INFO), width: 2 }] };
41036
+ if (cumV <= 0) continue;
41037
+ const mean = cumPV / cumV;
41038
+ const sd = Math.sqrt(Math.max(0, cumPV2 / cumV - mean * mean));
41039
+ values[i] = mean;
41040
+ for (const band of bands) {
41041
+ band.up[i] = mean + band.mult * sd;
41042
+ band.down[i] = mean - band.mult * sd;
41043
+ }
41044
+ }
41045
+ const plots = [{ key: "vwap", title: "VWAP", values, color: str3(inputs, "color", INFO), width: 2 }];
41046
+ const fills = [];
41047
+ bands.forEach((band, i) => {
41048
+ plots.push(
41049
+ { key: `up${i}`, title: `Upper ${band.mult}\u03C3`, values: band.up, color: band.color },
41050
+ { key: `down${i}`, title: `Lower ${band.mult}\u03C3`, values: band.down, color: band.color }
41051
+ );
41052
+ if (band.fill) fills.push({ key: `band${i}`, from: `up${i}`, to: `down${i}`, color: band.fillColor });
41053
+ });
41054
+ return { plots, bands: fills };
40568
41055
  }
40569
41056
  };
40570
41057
  var pivotPoints = {
@@ -40755,21 +41242,11 @@ ${overlayScrollbarCss(".vela-sd-pane")}
40755
41242
  registerClassicIndicators();
40756
41243
  const element = resolveElement(container);
40757
41244
  const theme = resolveTheme(options.theme);
40758
- let animZoom = true;
40759
- let animPan = true;
40760
- if (typeof options.animations === "boolean") {
40761
- animZoom = options.animations;
40762
- animPan = options.animations;
40763
- } else if (options.animations) {
40764
- animZoom = options.animations.zoom ?? true;
40765
- animPan = options.animations.pan ?? true;
40766
- }
40767
41245
  const display = {
40768
41246
  currentPriceLine: options.currentPriceLine ?? true,
40769
41247
  logScale: options.logScale ?? false,
40770
41248
  nativeBackend: options.nativeBackend ?? "auto",
40771
- animZoom,
40772
- animPan,
41249
+ ...resolveAnimations(options.animations),
40773
41250
  glow: options.glow ?? 0,
40774
41251
  upColor: options.upColor ?? BULLISH,
40775
41252
  downColor: options.downColor ?? BEARISH,