@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
package/dist/index.cjs CHANGED
@@ -25,6 +25,23 @@ function _interopNamespace(e) {
25
25
  var menu__namespace = /*#__PURE__*/_interopNamespace(menu);
26
26
  var dialog__namespace = /*#__PURE__*/_interopNamespace(dialog);
27
27
 
28
+ // src/core/options.ts
29
+ var LIVE_BAR_EASE_DEFAULT_MS = 90;
30
+ var LIVE_BAR_EASE_MAX_MS = 1e3;
31
+ function resolveLiveBarEaseMs(value) {
32
+ if (value === true) return LIVE_BAR_EASE_DEFAULT_MS;
33
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return 0;
34
+ return Math.min(value, LIVE_BAR_EASE_MAX_MS);
35
+ }
36
+ function resolveAnimations(animations) {
37
+ if (typeof animations === "boolean") return { animZoom: animations, animPan: animations, animLiveBar: 0 };
38
+ return {
39
+ animZoom: animations?.zoom ?? true,
40
+ animPan: animations?.pan ?? true,
41
+ animLiveBar: resolveLiveBarEaseMs(animations?.liveBar)
42
+ };
43
+ }
44
+
28
45
  // src/core/model/series.ts
29
46
  function isLineLikeSeries(spec) {
30
47
  return spec.kind !== "candle" && spec.kind !== "bar" && spec.kind !== "markers";
@@ -575,6 +592,7 @@ registerIcon("star-filled", S('<path d="M8 2.2l1.75 3.55 3.9.55-2.8 2.75.65 3.9L
575
592
  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"'));
576
593
  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"'));
577
594
  registerIcon("burger", S('<path d="M2.5 4.5h11M2.5 8h11M2.5 11.5h11"/>'));
595
+ registerIcon("info", S('<path d="M8 7.4v4.4"/><circle cx="8" cy="4.6" r="0.9" fill="currentColor" stroke="none"/>'));
578
596
  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"/>'));
579
597
  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"/>'));
580
598
  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"/>'));
@@ -6906,15 +6924,18 @@ var DrawingController = class {
6906
6924
  next = [...ids];
6907
6925
  }
6908
6926
  this.selectedIds = next.filter((id) => this.store.has(id));
6927
+ this.announceSelection();
6928
+ }
6929
+ /** Push the selection to the renderer and announce it (primary + the full list). */
6930
+ announceSelection() {
6909
6931
  this.port?.setSelection(this.selectedIds);
6910
- this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null });
6932
+ this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null, ids: [...this.selectedIds] });
6911
6933
  }
6912
6934
  /** Restore a history snapshot, reconciling selection against what survived. */
6913
6935
  restoreSnapshot(doc) {
6914
6936
  this.store.load(doc);
6915
6937
  this.selectedIds = this.selectedIds.filter((id) => this.store.has(id));
6916
- this.port?.setSelection(this.selectedIds);
6917
- this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null });
6938
+ this.announceSelection();
6918
6939
  }
6919
6940
  /** Delete drawings as one undo step; prune them from the selection. */
6920
6941
  deleteMany(ids) {
@@ -7047,6 +7068,9 @@ var DrawingController = class {
7047
7068
  case "duplicate":
7048
7069
  this.duplicate(i.ids);
7049
7070
  break;
7071
+ case "clone":
7072
+ if (this.enabled && i.docs.length) this.insertClones(i.docs);
7073
+ break;
7050
7074
  case "copy":
7051
7075
  this.copy(i.ids);
7052
7076
  break;
@@ -8883,7 +8907,12 @@ var EngineOrchestrator = class _EngineOrchestrator {
8883
8907
  if (!model) continue;
8884
8908
  const paneId = model.paneId ?? "price";
8885
8909
  if (!byPane.has(paneId)) byPane.set(paneId, []);
8886
- byPane.get(paneId).push({ id: r.id, title: model.title || r.title, ownScale: model.ownScale === true });
8910
+ byPane.get(paneId).push({
8911
+ id: r.id,
8912
+ title: model.title || r.title,
8913
+ ...model.shorttitle ? { shorttitle: model.shorttitle } : {},
8914
+ ownScale: model.ownScale === true
8915
+ });
8887
8916
  }
8888
8917
  const panes = [];
8889
8918
  for (const paneId of this.paneOrder) {
@@ -10525,6 +10554,23 @@ function attachChromeTooltip(anchor, opts) {
10525
10554
  clear();
10526
10555
  };
10527
10556
  }
10557
+ function chromeHint(text, opts) {
10558
+ const px = opts.size ?? 16;
10559
+ const el = document.createElement("span");
10560
+ el.className = "vela-hint";
10561
+ el.setAttribute("role", "img");
10562
+ el.setAttribute("aria-label", text);
10563
+ el.innerHTML = iconAt("info", px);
10564
+ 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;`;
10565
+ const hover = (on) => {
10566
+ el.style.background = on ? "var(--vela-active)" : "var(--vela-hover)";
10567
+ el.style.color = on ? "var(--vela-fg-bright)" : "var(--vela-fg-muted)";
10568
+ };
10569
+ el.addEventListener("pointerenter", () => hover(true));
10570
+ el.addEventListener("pointerleave", () => hover(false));
10571
+ const dispose = attachChromeTooltip(el, { host: opts.host, theme: opts.theme, text: () => text, wrap: true, delayMs: 250 });
10572
+ return { el, dispose };
10573
+ }
10528
10574
  function runMachine(machine3, props, render) {
10529
10575
  const m = new vanilla.VanillaMachine(machine3, props);
10530
10576
  const unsub = m.subscribe(render);
@@ -13666,14 +13712,11 @@ var IndicatorInputsDialog = class {
13666
13712
  buildTextField(id, current, emit) {
13667
13713
  return buildFieldControl({ kind: "text", id, value: current, onChange: emit }).el;
13668
13714
  }
13669
- /** A hoverable ⓘ affordance carrying an input's `tooltip` (themed chrome tip; wraps). */
13715
+ /** The shared ⓘ hint carrying an input's `tooltip` (see {@link chromeHint}). */
13670
13716
  infoButton(tooltip) {
13671
- const b = document.createElement("span");
13672
- b.textContent = "i";
13673
- this.dialogTips.push(attachChromeTooltip(b, { host: this.host.container, theme: () => this.host.theme(), text: () => tooltip, wrap: true, delayMs: 250 }));
13674
- b.className = "vela-ind-hint";
13675
- 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;";
13676
- return b;
13717
+ const { el, dispose } = chromeHint(tooltip, { host: this.host.container, theme: () => this.host.theme(), size: 18 });
13718
+ this.dialogTips.push(dispose);
13719
+ return el;
13677
13720
  }
13678
13721
  /** A dropdown over plain string options (value === label) — a thin case of {@link selectPairs}. */
13679
13722
  select(id, options, current, emit) {
@@ -14110,8 +14153,6 @@ function ensureDialogStyles() {
14110
14153
  ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
14111
14154
  .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;}
14112
14155
  .vela-ind-tab:not(.vela-ind-tab-active):hover{color:var(--vela-fg-bright);}
14113
- .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;}
14114
- .vela-ind-hint:hover{background:var(--vela-active);color:var(--vela-fg-bright);}
14115
14156
  .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;}
14116
14157
  .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;}
14117
14158
  .vela-ind-ctl:hover{background-color:var(--vela-hover-strong);}
@@ -15502,15 +15543,15 @@ function candleOverrideFor(style, bags) {
15502
15543
  if (!hasOwnCandlePaint(style)) return null;
15503
15544
  const bag = bags[style] ?? {};
15504
15545
  const color = (v) => isColor(v) ? v : null;
15505
- const bool = (v) => isBool(v) ? v : null;
15546
+ const bool2 = (v) => isBool(v) ? v : null;
15506
15547
  return {
15507
15548
  upColor: color(bag.candleUpColor),
15508
15549
  downColor: color(bag.candleDownColor),
15509
- bodyVisible: bool(bag.candleBodyVisible),
15510
- borderVisible: bool(bag.candleBorderVisible),
15550
+ bodyVisible: bool2(bag.candleBodyVisible),
15551
+ borderVisible: bool2(bag.candleBorderVisible),
15511
15552
  borderUpColor: color(bag.candleBorderUpColor),
15512
15553
  borderDownColor: color(bag.candleBorderDownColor),
15513
- wickVisible: bool(bag.candleWickVisible),
15554
+ wickVisible: bool2(bag.candleWickVisible),
15514
15555
  wickUpColor: color(bag.candleWickUpColor),
15515
15556
  wickDownColor: color(bag.candleWickDownColor)
15516
15557
  };
@@ -15655,7 +15696,8 @@ function mergeConfig(base, patch) {
15655
15696
  labelsVisible: isBool(ps.labelsVisible) ? ps.labelsVisible : base.priceScale.labelsVisible,
15656
15697
  currentPriceLine: isBool(ps.currentPriceLine) ? ps.currentPriceLine : base.priceScale.currentPriceLine,
15657
15698
  priceLabel: isBool(ps.priceLabel) ? ps.priceLabel : base.priceScale.priceLabel,
15658
- countdown: isBool(ps.countdown) ? ps.countdown : base.priceScale.countdown
15699
+ countdown: isBool(ps.countdown) ? ps.countdown : base.priceScale.countdown,
15700
+ animateLastPrice: isBool(ps.animateLastPrice) ? ps.animateLastPrice : base.priceScale.animateLastPrice
15659
15701
  },
15660
15702
  panes: {
15661
15703
  separatorColor: isColor(panes.separatorColor) ? panes.separatorColor : base.panes.separatorColor
@@ -17300,7 +17342,7 @@ var InputController = class {
17300
17342
  if (e.repeat || e.key !== "Shift" && e.key !== "Control" && e.key !== "Meta") return;
17301
17343
  if (Number.isNaN(this.cursorX)) return;
17302
17344
  if (this.dragging && this.region !== "drawing") return;
17303
- this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey);
17345
+ this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
17304
17346
  };
17305
17347
  this.onDown = (e) => {
17306
17348
  if (e.button === 1) {
@@ -17332,7 +17374,7 @@ var InputController = class {
17332
17374
  this.startY = y;
17333
17375
  if (this.deps.drawingsClaim?.(x, y)) {
17334
17376
  this.region = "drawing";
17335
- this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey);
17377
+ this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
17336
17378
  this.capture(e.pointerId);
17337
17379
  return;
17338
17380
  }
@@ -17341,6 +17383,11 @@ var InputController = class {
17341
17383
  this.capture(e.pointerId);
17342
17384
  return;
17343
17385
  }
17386
+ if ((e.ctrlKey || e.metaKey) && this.regionAt(x, y) === "data" && this.deps.drawingsMarqueeStart?.(x, y)) {
17387
+ this.region = "drawing";
17388
+ this.capture(e.pointerId);
17389
+ return;
17390
+ }
17344
17391
  const vp = this.deps.getCoords().getViewport();
17345
17392
  this.deps.apply(vp);
17346
17393
  this.startRightOffset = vp.rightOffset;
@@ -17389,7 +17436,7 @@ var InputController = class {
17389
17436
  this.deps.apply({ barSpacing, rightOffset: this.startRightOffset });
17390
17437
  } else if (this.region === "drawing") {
17391
17438
  if (Math.abs(x - this.startX) > slop || Math.abs(y - this.startY) > slop) this.moved = true;
17392
- this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
17439
+ this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
17393
17440
  } else {
17394
17441
  const coords = this.deps.getCoords();
17395
17442
  const vp = coords.getViewport();
@@ -17412,7 +17459,7 @@ var InputController = class {
17412
17459
  const drawCursor = this.deps.drawingsCursor?.(x, y);
17413
17460
  const r = this.regionAt(x, y);
17414
17461
  this.el.style.cursor = drawCursor ?? (r === "price" ? "ns-resize" : r === "time" ? "ew-resize" : r === "separator" ? "row-resize" : "");
17415
- this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
17462
+ this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
17416
17463
  }
17417
17464
  if (e.pointerType !== "touch") this.deps.onPointerMove(x, y);
17418
17465
  };
@@ -17753,12 +17800,12 @@ function defaultTradeMarkersState() {
17753
17800
  function mergeTradeMarkersState(base, patch) {
17754
17801
  const p = patch && typeof patch === "object" ? patch : {};
17755
17802
  const c = p.colors && typeof p.colors === "object" ? p.colors : {};
17756
- const bool = (v, fb) => typeof v === "boolean" ? v : fb;
17803
+ const bool2 = (v, fb) => typeof v === "boolean" ? v : fb;
17757
17804
  const color = (v, fb) => typeof v === "string" && v.trim().length > 0 ? v : fb;
17758
17805
  return {
17759
- visible: bool(p.visible, base.visible),
17760
- labels: bool(p.labels, base.labels),
17761
- qty: bool(p.qty, base.qty),
17806
+ visible: bool2(p.visible, base.visible),
17807
+ labels: bool2(p.labels, base.labels),
17808
+ qty: bool2(p.qty, base.qty),
17762
17809
  colors: {
17763
17810
  long: color(c.long, base.colors.long),
17764
17811
  short: color(c.short, base.colors.short),
@@ -20519,6 +20566,8 @@ var BUILTIN_SETTINGS_IDS = [
20519
20566
  "symbol.style.baseline.fill-bottom",
20520
20567
  "symbol.style.baseline.base-level",
20521
20568
  "symbol.style.baseline.width",
20569
+ "symbol.animation",
20570
+ "symbol.animation.price-changes",
20522
20571
  "symbol.timezone",
20523
20572
  "scales",
20524
20573
  "scales.price-scale",
@@ -20661,8 +20710,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
20661
20710
  .vela-sd-mobile .vela-select-trigger,.vela-sd-mobile .vela-num input,.vela-sd-mobile .vela-width-field{height:34px;}
20662
20711
  .vela-sd-mobile .vela-sd-close{width:40px;height:40px;}
20663
20712
  .vela-sd-mobile .vela-sd-btn{height:38px;}
20664
- .vela-sd-mobile .vela-sd-row span,.vela-sd-mobile .vela-sd-bool span,.vela-sd-mobile .vela-field-label{white-space:normal !important;}
20665
- `;
20713
+ .vela-sd-mobile .vela-sd-row span,.vela-sd-mobile .vela-sd-bool span,.vela-sd-mobile .vela-field-label{white-space:normal !important;}`;
20666
20714
  if (!existing) document.head.appendChild(st);
20667
20715
  }
20668
20716
  var SettingsDialog = class {
@@ -20683,6 +20731,8 @@ var SettingsDialog = class {
20683
20731
  this.themeControl = null;
20684
20732
  /** The built tabs, by title — how `showSection` reaches a pane while the dialog is open. */
20685
20733
  this.tabs = [];
20734
+ this.hintTips = [];
20735
+ // ⓘ tooltip disposers of the open build (see hint)
20686
20736
  /** Active instance-strip tab per chart type — remembered across dialog rebuilds. */
20687
20737
  this.typeActiveInstance = /* @__PURE__ */ new Map();
20688
20738
  /** Active TOC group per structured pane (`<typeId>/<pane>` → group label). */
@@ -20871,6 +20921,13 @@ var SettingsDialog = class {
20871
20921
  body.append(g);
20872
20922
  }
20873
20923
  showActive(config.series.style);
20924
+ body.append(sid(this.sectionTitle("Animation"), "symbol.animation"));
20925
+ body.append(sid(this.boolRow(
20926
+ "Animate price changes",
20927
+ config.priceScale.animateLastPrice,
20928
+ (v) => this.emit({ priceScale: { animateLastPrice: v } }),
20929
+ this.hint("Glide the live bar to each new price instead of snapping.")
20930
+ ), "symbol.animation.price-changes"));
20874
20931
  body.append(sid(this.sectionTitle("Time zone"), "symbol.timezone"));
20875
20932
  body.append(sid(this.selectRowLabeled("Time zone", normalizeTimezone(config.timeScale.timezone), timezoneOptions(config.timeScale.timezone), (v) => this.emit({ timeScale: { timezone: v } })), "symbol.timezone"));
20876
20933
  const renderHostSections = (placement) => {
@@ -21078,6 +21135,8 @@ var SettingsDialog = class {
21078
21135
  this.ui = null;
21079
21136
  this.root = null;
21080
21137
  this.tabs = [];
21138
+ for (const dispose of this.hintTips) dispose();
21139
+ this.hintTips = [];
21081
21140
  ui?.destroy();
21082
21141
  }
21083
21142
  destroy() {
@@ -21491,23 +21550,31 @@ var SettingsDialog = class {
21491
21550
  }).el
21492
21551
  });
21493
21552
  }
21494
- /** A toggle row: the checkbox sits to the LEFT of its label (never in the control area). */
21495
- boolRow(label, value, onChange) {
21496
- return this.toggleRow(label, value, onChange, []);
21553
+ /** A toggle row: the checkbox sits to the LEFT of its label (never in the control area).
21554
+ * `info` (see {@link hint}) rides after the label. */
21555
+ boolRow(label, value, onChange, info) {
21556
+ return this.toggleRow(label, value, onChange, [], void 0, info);
21497
21557
  }
21498
21558
  /** An enable row: checkbox + label in the label column, dependent controls in the
21499
21559
  * shared control column; the control group dims and ignores input while the toggle
21500
21560
  * is off. With `get`, the row re-reads its state on a 'vela-sync' event. */
21501
- toggleRow(label, value, onToggle, controls, get) {
21561
+ toggleRow(label, value, onToggle, controls, get, info) {
21502
21562
  return fieldRow({
21503
21563
  label,
21504
21564
  labelSize: "sm",
21505
21565
  bool: controls.length === 0,
21506
21566
  toggle: { checked: value, onChange: onToggle, get },
21507
21567
  control: controls,
21568
+ info,
21508
21569
  className: controls.length === 0 ? "vela-sd-bool" : "vela-sd-row"
21509
21570
  });
21510
21571
  }
21572
+ /** The shared ⓘ hint after a row label (see {@link chromeHint}). Disposed with the dialog. */
21573
+ hint(text) {
21574
+ const { el, dispose } = chromeHint(text, { host: this.container, theme: () => this.theme });
21575
+ this.hintTips.push(dispose);
21576
+ return el;
21577
+ }
21511
21578
  numberRow(label, value, min, max, step, onChange) {
21512
21579
  return fieldRow({
21513
21580
  label,
@@ -21688,6 +21755,18 @@ var DrawingPainter = class {
21688
21755
  }
21689
21756
  this.paintHandles(ctx, pts);
21690
21757
  }
21758
+ /** The selection box a Ctrl/Cmd-drag on the empty plot sweeps: a faint fill in the handle
21759
+ * accent under a thin dashed outline. */
21760
+ paintMarquee(ctx, rect) {
21761
+ ctx.save();
21762
+ ctx.fillStyle = withAlpha(HANDLE_BORDER, 0.08);
21763
+ ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
21764
+ ctx.strokeStyle = HANDLE_BORDER;
21765
+ ctx.lineWidth = 1;
21766
+ ctx.setLineDash([4, 3]);
21767
+ ctx.strokeRect(rect.x + 0.5, rect.y + 0.5, rect.w, rect.h);
21768
+ ctx.restore();
21769
+ }
21691
21770
  /** A hollow ring marking the candle point the magnet (Ctrl) will snap the next anchor to. */
21692
21771
  paintSnapRing(ctx, x, y, theme) {
21693
21772
  ctx.setLineDash([]);
@@ -23156,6 +23235,15 @@ function topDrawingAt(drawings, x, y, proj, tol = HIT_TOLERANCE) {
23156
23235
  }
23157
23236
  return null;
23158
23237
  }
23238
+ function deletableSelection(selected, drawings) {
23239
+ const ids = [...selected];
23240
+ if (ids.length < 2) return ids;
23241
+ return ids.filter((id) => !drawings.find((d) => d.id === id)?.locked);
23242
+ }
23243
+ function deleteTargets(hit, selected, drawings, withSelection) {
23244
+ if (withSelection && selected.size >= 2 && selected.has(hit.id)) return deletableSelection(selected, drawings);
23245
+ return hit.locked ? [] : [hit.id];
23246
+ }
23159
23247
 
23160
23248
  // src/renderers/native/drawings/DrawingInteraction.ts
23161
23249
  var DRAG_SLOP2 = 3;
@@ -23259,10 +23347,32 @@ var DrawingInteraction = class {
23259
23347
  activeDragId() {
23260
23348
  return this.state.kind === "pressed" && this.state.moved ? this.state.id : null;
23261
23349
  }
23350
+ /** The drawing under a press that has not been released yet (drag or click undecided), or null. */
23351
+ pressedId() {
23352
+ return this.state.kind === "pressed" ? this.state.id : null;
23353
+ }
23354
+ /** The copies a Ctrl-drag is moving (transient — not yet in the store), or null. */
23355
+ dragClones() {
23356
+ return this.state.kind === "pressed" && this.state.moved && this.state.clones ? this.state.clones.map((c) => c.d) : null;
23357
+ }
23358
+ /** The marquee box being swept, normalized (or null when none is in progress). */
23359
+ marqueeRect() {
23360
+ if (this.state.kind !== "marquee") return null;
23361
+ const { x0, y0, x1, y1 } = this.state;
23362
+ return { x: Math.min(x0, x1), y: Math.min(y0, y1), w: Math.abs(x1 - x0), h: Math.abs(y1 - y0) };
23363
+ }
23364
+ /** Ctrl/Cmd-press on the empty plot: start sweeping a selection box. False when a tool is
23365
+ * armed or a gesture is in flight (that path owns the press). */
23366
+ beginMarquee(x, y) {
23367
+ if (this.state.kind !== "idle" || this.deps.activeTool() != null) return false;
23368
+ this.state = { kind: "marquee", x0: x, y0: y, x1: x, y1: y };
23369
+ return true;
23370
+ }
23262
23371
  /** A press: place the next anchor, or grab a drawing/handle (resolved as click or drag on release).
23263
23372
  * `shift` over a drawing toggles it in/out of the selection (multi-select) instead of dragging;
23264
- * while placing a line tool it locks the segment angle to 45° steps. */
23265
- down(x, y, mode = "off", shift2 = false) {
23373
+ * while placing a line tool it locks the segment angle to 45° steps. `mod` (Ctrl/Cmd) over a
23374
+ * drawing also toggles on a click, but a body drag then moves a COPY. */
23375
+ down(x, y, mode = "off", shift2 = false, mod = false) {
23266
23376
  const proj = this.deps.projector();
23267
23377
  if (this.state.kind === "placing") {
23268
23378
  if (this.state.draft.placementMode() !== "click") return;
@@ -23290,7 +23400,50 @@ var DrawingInteraction = class {
23290
23400
  return;
23291
23401
  }
23292
23402
  const handle = hit.hitHandle(x, y, proj, HIT_TOLERANCE);
23293
- this.state = { kind: "pressed", id: hit.id, handle, grab: proj.pxToPoint(x, y, hit.paneId), orig: cloneAnchors(hit), px: x, py: y, moved: false };
23403
+ const riders = [];
23404
+ if (handle < 0 && this.deps.selectedIds().has(hit.id)) {
23405
+ for (const id of this.deps.selectedIds()) {
23406
+ const d = id === hit.id ? null : this.byId(id);
23407
+ if (d && !d.locked && d.visible) riders.push({ id, orig: cloneAnchors(d) });
23408
+ }
23409
+ }
23410
+ this.state = { kind: "pressed", id: hit.id, handle, orig: cloneAnchors(hit), px: x, py: y, moved: false, riders, mod, clones: null };
23411
+ }
23412
+ }
23413
+ /** The data-space delta a body drag from the press to (x, y) means on `paneId` — resolved per
23414
+ * pane so drawings riding along on another pane move by the same PIXELS, not the same price. */
23415
+ bodyDelta(paneId, x, y) {
23416
+ if (this.state.kind !== "pressed") return { dt: 0, dp: 0 };
23417
+ const proj = this.deps.projector();
23418
+ const from = proj.pxToPoint(this.state.px, this.state.py, paneId);
23419
+ const to = proj.pxToPoint(x, y, paneId);
23420
+ return { dt: to.time - from.time, dp: to.price - from.price };
23421
+ }
23422
+ /** The pressed drawing and its riders, each with its anchors at the press. */
23423
+ dragSources(d) {
23424
+ if (this.state.kind !== "pressed") return [];
23425
+ const out = [{ d, orig: this.state.orig }];
23426
+ for (const r of this.state.riders) {
23427
+ const rd = this.byId(r.id);
23428
+ if (rd) out.push({ d: rd, orig: r.orig });
23429
+ }
23430
+ return out;
23431
+ }
23432
+ /** Move the pressed drawing's body (and its riders) — or, on a Ctrl-drag, their copies — to the
23433
+ * cursor. The copies are built on the first move (so a Ctrl-click never creates anything), from
23434
+ * sources that have not moved yet, so their own anchors are the origin to translate from. */
23435
+ dragBody(d, x, y) {
23436
+ if (this.state.kind !== "pressed") return;
23437
+ this.snapAt = null;
23438
+ if (this.state.mod && !this.state.clones) {
23439
+ this.state.clones = this.dragSources(d).flatMap((s) => {
23440
+ const c = deserializeDrawing(s.d.serialize());
23441
+ return c ? [{ d: c, orig: cloneAnchors(c) }] : [];
23442
+ });
23443
+ }
23444
+ for (const t of this.state.clones ?? this.dragSources(d)) {
23445
+ const { dt, dp } = this.bodyDelta(t.d.paneId, x, y);
23446
+ t.d.anchors = t.d.translateBody(dt, dp, t.orig);
23294
23447
  }
23295
23448
  }
23296
23449
  /** Pointer move: advance the placing ghost, or live-preview a drag once past the slop.
@@ -23314,12 +23467,20 @@ var DrawingInteraction = class {
23314
23467
  this.deps.changed();
23315
23468
  return;
23316
23469
  }
23470
+ if (this.state.kind === "marquee") {
23471
+ this.state.x1 = x;
23472
+ this.state.y1 = y;
23473
+ this.deps.changed();
23474
+ return;
23475
+ }
23317
23476
  if (this.state.kind !== "pressed") return;
23318
23477
  const d = this.byId(this.state.id);
23319
23478
  if (!d || d.locked) return;
23320
23479
  if (!this.state.moved && Math.abs(x - this.state.px) <= DRAG_SLOP2 && Math.abs(y - this.state.py) <= DRAG_SLOP2) return;
23321
23480
  this.state.moved = true;
23322
- if (this.state.handle >= 0) {
23481
+ if (this.state.handle < 0) {
23482
+ this.dragBody(d, x, y);
23483
+ } else {
23323
23484
  const a = d.anchors[this.state.handle];
23324
23485
  if (a) {
23325
23486
  const pivot = shift2 && ANGLE_SNAP_TYPES.has(d.type) && d.anchors.length === 2 ? d.anchors[1 - this.state.handle] : void 0;
@@ -23327,12 +23488,6 @@ var DrawingInteraction = class {
23327
23488
  applyFree(a, point, d.anchorSchema().slots[this.state.handle]?.free ?? "both");
23328
23489
  }
23329
23490
  d.constrainHandleDrag(this.state.handle);
23330
- } else {
23331
- const pt = this.deps.projector().pxToPoint(x, y, d.paneId);
23332
- this.snapAt = null;
23333
- const dt = pt.time - this.state.grab.time;
23334
- const dp = pt.price - this.state.grab.price;
23335
- d.anchors = d.translateBody(dt, dp, this.state.orig);
23336
23491
  }
23337
23492
  this.deps.changed();
23338
23493
  }
@@ -23352,16 +23507,46 @@ var DrawingInteraction = class {
23352
23507
  }
23353
23508
  return;
23354
23509
  }
23510
+ if (this.state.kind === "marquee") {
23511
+ const rect = this.marqueeRect();
23512
+ this.state = { kind: "idle" };
23513
+ if (rect.w > DRAG_SLOP2 || rect.h > DRAG_SLOP2) this.selectWithin(rect);
23514
+ this.deps.changed();
23515
+ return;
23516
+ }
23355
23517
  if (this.state.kind !== "pressed") return;
23356
- const { id, moved } = this.state;
23518
+ const { id, moved, riders, mod, clones } = this.state;
23357
23519
  this.state = { kind: "idle" };
23358
23520
  const d = this.byId(id);
23359
- if (moved) {
23360
- if (d) this.deps.emit({ kind: "edit", doc: d.serialize() });
23361
- this.deps.changed();
23362
- } else {
23363
- this.deps.openSettings(id, x, y);
23521
+ if (!moved) {
23522
+ if (mod) this.deps.emit({ kind: "select", ids: [id], additive: true });
23523
+ else this.deps.openSettings(id, x, y);
23524
+ return;
23525
+ }
23526
+ if (clones) {
23527
+ this.deps.emit({ kind: "clone", docs: clones.map((c) => c.d.serialize()) });
23528
+ } else if (d) {
23529
+ const docs = [d.serialize()];
23530
+ for (const r of riders) {
23531
+ const rd = this.byId(r.id);
23532
+ if (rd) docs.push(rd.serialize());
23533
+ }
23534
+ if (docs.length > 1) this.deps.emit({ kind: "edit-many", docs });
23535
+ else this.deps.emit({ kind: "edit", doc: docs[0] });
23364
23536
  }
23537
+ this.deps.changed();
23538
+ }
23539
+ /** Add every visible drawing whose on-screen bounds touch `rect` to the selection (a union,
23540
+ * so successive sweeps accumulate). No change when the box touches nothing new. */
23541
+ selectWithin(rect) {
23542
+ const proj = this.deps.projector();
23543
+ const ids = [...this.deps.selectedIds()];
23544
+ for (const d of this.deps.drawings()) {
23545
+ if (!d.visible || ids.includes(d.id)) continue;
23546
+ const b = d.bounds(proj);
23547
+ if (b && rectsIntersect(rect, b)) ids.push(d.id);
23548
+ }
23549
+ if (ids.length > this.deps.selectedIds().size) this.deps.emit({ kind: "select", ids });
23365
23550
  }
23366
23551
  /** Cancel an in-progress placement or drag (Escape). Returns whether anything was cancelled. */
23367
23552
  cancel() {
@@ -23374,8 +23559,15 @@ var DrawingInteraction = class {
23374
23559
  return true;
23375
23560
  }
23376
23561
  if (this.state.kind === "pressed") {
23377
- const d = this.byId(this.state.id);
23378
- if (d && this.state.moved) d.anchors = this.state.orig.map((o) => ({ time: o.time, price: o.price }));
23562
+ if (this.state.moved && !this.state.clones) {
23563
+ const d = this.byId(this.state.id);
23564
+ if (d) for (const s of this.dragSources(d)) s.d.anchors = s.orig.map((o) => ({ time: o.time, price: o.price }));
23565
+ }
23566
+ this.state = { kind: "idle" };
23567
+ this.deps.changed();
23568
+ return true;
23569
+ }
23570
+ if (this.state.kind === "marquee") {
23379
23571
  this.state = { kind: "idle" };
23380
23572
  this.deps.changed();
23381
23573
  return true;
@@ -23490,6 +23682,9 @@ var DrawingInteraction = class {
23490
23682
  function cloneAnchors(d) {
23491
23683
  return d.anchors.map((a) => ({ time: a.time, price: a.price }));
23492
23684
  }
23685
+ function rectsIntersect(a, b) {
23686
+ 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;
23687
+ }
23493
23688
  function applyFree(anchor, pt, free) {
23494
23689
  if (free === "both" || free === "x") anchor.time = pt.time;
23495
23690
  if (free === "both" || free === "y") anchor.price = pt.price;
@@ -23875,10 +24070,27 @@ function btn(label, primary, onClick) {
23875
24070
  }
23876
24071
 
23877
24072
  // src/renderers/native/drawings/DrawingSettingsPopup.ts
24073
+ var MIXED = /* @__PURE__ */ Symbol("mixed");
24074
+ function sharedPaths(drawings) {
24075
+ const [first, ...rest] = drawings;
24076
+ const paths = new Set(first?.schema().fields.map((f) => f.path) ?? []);
24077
+ for (const d of rest) {
24078
+ const own = new Set(d.schema().fields.map((f) => f.path));
24079
+ for (const p of paths) if (!own.has(p)) paths.delete(p);
24080
+ }
24081
+ return paths;
24082
+ }
24083
+ function commonValue(drawings, read) {
24084
+ const first = read(drawings[0]);
24085
+ return drawings.every((d) => read(d) === first) ? first : MIXED;
24086
+ }
24087
+ function distinctValues(drawings, read) {
24088
+ return [...new Set(drawings.map(read))];
24089
+ }
23878
24090
  var BTN = 30;
23879
24091
  var ICON = 21;
23880
24092
  var STYLE_ID3 = "vela-drawing-popup-styles";
23881
- var STYLE_REV = "3";
24093
+ var STYLE_REV = "4";
23882
24094
  function ensureStyles3() {
23883
24095
  if (typeof document === "undefined") return;
23884
24096
  const existing = document.getElementById(STYLE_ID3);
@@ -23890,6 +24102,8 @@ function ensureStyles3() {
23890
24102
  .vela-dpop-btn{background:transparent;color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
23891
24103
  .vela-dpop-btn:hover{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
23892
24104
  .vela-dpop-btn[data-active='1']{background:var(--vela-active);color:var(--vela-fg-bright);}
24105
+ /* Mixed (a multi-selection disagrees): the active fill at half strength behind a dashed ring. */
24106
+ .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;}
23893
24107
  .vela-dpop-item{background:transparent;transition:background var(--vela-dur-fast) ease;}
23894
24108
  .vela-dpop-item:hover{background:var(--vela-hover-strong);}
23895
24109
  .vela-dpop-item[data-active='1']{background:var(--vela-active);}
@@ -23921,7 +24135,7 @@ var DrawingSettingsPopup = class {
23921
24135
  if (this.isOwnChrome(node)) return;
23922
24136
  const cb = this.onClose;
23923
24137
  this.close();
23924
- cb?.();
24138
+ cb?.(e);
23925
24139
  };
23926
24140
  this.theme = theme;
23927
24141
  this.settingsDialog = new DrawingSettingsDialog(host, theme);
@@ -23946,16 +24160,27 @@ var DrawingSettingsPopup = class {
23946
24160
  contains(node) {
23947
24161
  return node != null && (this.isOwnChrome(node) || this.settingsDialog.contains(node));
23948
24162
  }
23949
- /** Open the quick toolbar for `drawing`, floating clear of its `anchor` box.
23950
- * `onClose` fires on an outside-click dismissal (not a programmatic close). */
23951
- open(drawing, anchor, actions, onClose) {
24163
+ /** Open the quick toolbar for `drawings` (one, or a whole multi-selection), floating clear of
24164
+ * their `anchor` box. With several drawings the bar shows only the controls they ALL support,
24165
+ * a control whose values differ reads as mixed, and every edit applies to all of them —
24166
+ * per-drawing panels (levels, position sizing, the text field) stay single-drawing only.
24167
+ * `onClose` fires on a dismissal (an outside press, or Escape in the text field — not a
24168
+ * programmatic close), with the dismissing press when there is one, so the caller can tell a
24169
+ * modifier press (multi-select) from a plain one. */
24170
+ open(drawings, anchor, actions, onClose) {
23952
24171
  this.close();
24172
+ const drawing = drawings[0];
24173
+ if (!drawing) return;
23953
24174
  ensureStyles3();
23954
24175
  this.onClose = onClose ?? null;
23955
24176
  const t = this.theme;
24177
+ const multi = drawings.length > 1;
23956
24178
  const schema = drawing.schema();
23957
- const paths = new Set(schema.fields.map((f) => f.path));
23958
- const textOnBar = schema.textIsContent === true || !paths.has("text.value");
24179
+ const paths = sharedPaths(drawings);
24180
+ const common = (read) => commonValue(drawings, read);
24181
+ const every = (pred) => drawings.every(pred);
24182
+ const swatch = (tip, glyph, read, path) => this.colorButton(tip, glyph, common(read), (v) => actions.patch({ [path]: v }), void 0, distinctValues(drawings, read));
24183
+ const textOnBar = multi || schema.textIsContent === true || !paths.has("text.value");
23959
24184
  const el = document.createElement("div");
23960
24185
  el.className = "vela-dpop";
23961
24186
  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;`;
@@ -23969,75 +24194,84 @@ var DrawingSettingsPopup = class {
23969
24194
  bar.style.cssText = "display:flex;align-items:center;gap:2px;padding:4px;";
23970
24195
  bar.appendChild(this.dragHandle());
23971
24196
  if (paths.has("glyph")) {
23972
- const cur = drawing.glyph ?? GLYPH_OPTIONS[0];
24197
+ const cur = common((d) => d.glyph ?? GLYPH_OPTIONS[0]);
23973
24198
  bar.appendChild(this.dropdown("Icon", GLYPH_OPTIONS, cur, (g) => glyphIcon(g), (v) => actions.patch({ glyph: v })));
23974
24199
  }
23975
24200
  if (paths.has("size")) {
23976
- const sz = drawing.size ?? "normal";
24201
+ const sz = common((d) => d.size ?? "normal");
23977
24202
  bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
23978
24203
  }
23979
- if (paths.has("magnifier.timeframe") && drawing instanceof Magnifier) {
24204
+ if (paths.has("magnifier.timeframe") && every((d) => d instanceof Magnifier)) {
24205
+ const mag = (d) => d;
23980
24206
  const options = this.lowerTimeframeOptions();
23981
24207
  if (options.length > 0) {
23982
24208
  bar.appendChild(
23983
- this.dropdown("Lower timeframe", options.map((o) => o.value), drawing.magnifier.timeframe, () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
24209
+ this.dropdown("Lower timeframe", options.map((o) => o.value), common((d) => mag(d).magnifier.timeframe), () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
23984
24210
  label: (v) => magnifierTimeframeLabel(String(v)),
23985
24211
  labelInTrigger: true
23986
24212
  })
23987
24213
  );
23988
24214
  }
23989
- bar.appendChild(this.colorButton("Up candles", BUCKET_ICON, drawing.magnifier.upColor || t.upColor, (v) => actions.patch({ "magnifier.upColor": v })));
23990
- bar.appendChild(this.colorButton("Down candles", BUCKET_ICON, drawing.magnifier.downColor || t.downColor, (v) => actions.patch({ "magnifier.downColor": v })));
24215
+ bar.appendChild(swatch("Up candles", BUCKET_ICON, (d) => mag(d).magnifier.upColor || t.upColor, "magnifier.upColor"));
24216
+ bar.appendChild(swatch("Down candles", BUCKET_ICON, (d) => mag(d).magnifier.downColor || t.downColor, "magnifier.downColor"));
23991
24217
  }
23992
- 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 })));
24218
+ 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"));
23993
24219
  if (paths.has("style.lineWidth")) {
23994
- const wf = schema.fields.find((f) => f.path === "style.lineWidth");
23995
- if (wf?.kind === "number" && (wf.min ?? 1) > 1) {
23996
- bar.appendChild(this.widthInput("Line width", drawing.style.lineWidth, wf.min ?? 1, wf.max ?? 60, wf.step ?? 1, (v) => actions.patch({ "style.lineWidth": v })));
24220
+ const widthField2 = (d) => d.schema().fields.find((f) => f.path === "style.lineWidth");
24221
+ const wf = widthField2(drawing);
24222
+ const width = common((d) => d.style.lineWidth);
24223
+ const markers = every((d) => {
24224
+ const f = widthField2(d);
24225
+ return f?.kind === "number" && (f.min ?? 1) > 1;
24226
+ });
24227
+ if (markers && wf?.kind === "number") {
24228
+ bar.appendChild(this.widthInput("Line width", width, wf.min ?? 1, wf.max ?? 60, wf.step ?? 1, (v) => actions.patch({ "style.lineWidth": v })));
23997
24229
  } else {
23998
- 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 }));
24230
+ 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 }));
23999
24231
  }
24000
24232
  }
24001
- 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 }));
24002
- 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 })));
24233
+ 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 }));
24234
+ if (paths.has("style.fillColor")) bar.appendChild(swatch("Fill", BUCKET_ICON, (d) => effectiveFillColor(d, this.theme) ?? d.style.fillColor ?? DEFAULT_DRAWING_COLOR, "style.fillColor"));
24003
24235
  const isFrvp = paths.has("frvp.rows") && drawing instanceof FixedRangeVolumeProfile;
24004
- const isPosition = paths.has("riskPercent") && drawing instanceof PositionTool;
24236
+ const isPosition = paths.has("riskPercent") && every((d) => d instanceof PositionTool);
24005
24237
  if (isPosition) {
24006
- const pos = drawing;
24007
- bar.appendChild(this.colorButton("Profit zone", BUCKET_ICON, pos.profitColor, (v) => actions.patch({ profitColor: v })));
24008
- bar.appendChild(this.colorButton("Loss zone", BUCKET_ICON, pos.lossColor, (v) => actions.patch({ lossColor: v })));
24238
+ const pos = (d) => d;
24239
+ bar.appendChild(swatch("Profit zone", BUCKET_ICON, (d) => pos(d).profitColor, "profitColor"));
24240
+ bar.appendChild(swatch("Loss zone", BUCKET_ICON, (d) => pos(d).lossColor, "lossColor"));
24009
24241
  }
24010
- const reg = drawing.reg;
24011
- if (paths.has("reg.midColor") && reg) {
24242
+ const regOf = (d) => d.reg;
24243
+ if (paths.has("reg.midColor") && every((d) => regOf(d) != null)) {
24244
+ const reg = (d) => regOf(d);
24012
24245
  const styles = LINE_STYLE_OPTIONS.map((o) => o.value);
24013
- bar.appendChild(this.colorButton("Midline color", BRUSH_ICON, reg.midColor, (v) => actions.patch({ "reg.midColor": v })));
24014
- bar.appendChild(this.dropdown("Midline style", styles, reg.midStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.midStyle": v }), { label: styleLabel2 }));
24015
- bar.appendChild(this.colorButton("Upper line color", BRUSH_ICON, reg.upperColor, (v) => actions.patch({ "reg.upperColor": v })));
24016
- bar.appendChild(this.dropdown("Upper line style", styles, reg.upperStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.upperStyle": v }), { label: styleLabel2 }));
24017
- bar.appendChild(this.colorButton("Lower line color", BRUSH_ICON, reg.lowerColor, (v) => actions.patch({ "reg.lowerColor": v })));
24018
- bar.appendChild(this.dropdown("Lower line style", styles, reg.lowerStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.lowerStyle": v }), { label: styleLabel2 }));
24019
- bar.appendChild(this.colorButton("Upper fill", BUCKET_ICON, reg.upperFill, (v) => actions.patch({ "reg.upperFill": v })));
24020
- bar.appendChild(this.colorButton("Lower fill", BUCKET_ICON, reg.lowerFill, (v) => actions.patch({ "reg.lowerFill": v })));
24021
- bar.appendChild(this.toggle("Show R\xB2", R2_ICON, reg.showR2, (v) => actions.patch({ "reg.showR2": v })));
24022
- }
24023
- const vwap2 = drawing.vwap;
24024
- if (paths.has("vwap.midColor") && vwap2) {
24246
+ bar.appendChild(swatch("Midline color", BRUSH_ICON, (d) => reg(d).midColor, "reg.midColor"));
24247
+ bar.appendChild(this.dropdown("Midline style", styles, common((d) => reg(d).midStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "reg.midStyle": v }), { label: styleLabel2 }));
24248
+ bar.appendChild(swatch("Upper line color", BRUSH_ICON, (d) => reg(d).upperColor, "reg.upperColor"));
24249
+ 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 }));
24250
+ bar.appendChild(swatch("Lower line color", BRUSH_ICON, (d) => reg(d).lowerColor, "reg.lowerColor"));
24251
+ 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 }));
24252
+ bar.appendChild(swatch("Upper fill", BUCKET_ICON, (d) => reg(d).upperFill, "reg.upperFill"));
24253
+ bar.appendChild(swatch("Lower fill", BUCKET_ICON, (d) => reg(d).lowerFill, "reg.lowerFill"));
24254
+ bar.appendChild(this.toggle("Show R\xB2", R2_ICON, common((d) => reg(d).showR2), (v) => actions.patch({ "reg.showR2": v })));
24255
+ }
24256
+ const vwapOf = (d) => d.vwap;
24257
+ if (paths.has("vwap.midColor") && every((d) => vwapOf(d) != null)) {
24258
+ const vwap2 = (d) => vwapOf(d);
24025
24259
  const styles = LINE_STYLE_OPTIONS.map((o) => o.value);
24026
24260
  const MULTS = [0.5, 1, 1.5, 2, 2.5, 3, 4, 5];
24027
- bar.appendChild(this.colorButton("VWAP color", BRUSH_ICON, vwap2.midColor, (v) => actions.patch({ "vwap.midColor": v })));
24028
- bar.appendChild(this.dropdown("VWAP style", styles, vwap2.midStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "vwap.midStyle": v }), { label: styleLabel2 }));
24261
+ bar.appendChild(swatch("VWAP color", BRUSH_ICON, (d) => vwap2(d).midColor, "vwap.midColor"));
24262
+ bar.appendChild(this.dropdown("VWAP style", styles, common((d) => vwap2(d).midStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "vwap.midStyle": v }), { label: styleLabel2 }));
24029
24263
  bar.appendChild(
24030
- this.dropdown("Band multiplier", MULTS, vwap2.multiplier, () => BANDS_ICON, (v) => actions.patch({ "vwap.multiplier": v }), {
24264
+ this.dropdown("Band multiplier", MULTS, common((d) => vwap2(d).multiplier), () => BANDS_ICON, (v) => actions.patch({ "vwap.multiplier": v }), {
24031
24265
  label: (v) => `${v}\u03C3`,
24032
24266
  labelInTrigger: true
24033
24267
  })
24034
24268
  );
24035
- bar.appendChild(this.colorButton("Upper band color", BRUSH_ICON, vwap2.upperColor, (v) => actions.patch({ "vwap.upperColor": v })));
24036
- bar.appendChild(this.colorButton("Lower band color", BRUSH_ICON, vwap2.lowerColor, (v) => actions.patch({ "vwap.lowerColor": v })));
24037
- bar.appendChild(this.colorButton("Band fill", BUCKET_ICON, vwap2.bandFill, (v) => actions.patch({ "vwap.bandFill": v })));
24269
+ bar.appendChild(swatch("Upper band color", BRUSH_ICON, (d) => vwap2(d).upperColor, "vwap.upperColor"));
24270
+ bar.appendChild(swatch("Lower band color", BRUSH_ICON, (d) => vwap2(d).lowerColor, "vwap.lowerColor"));
24271
+ bar.appendChild(swatch("Band fill", BUCKET_ICON, (d) => vwap2(d).bandFill, "vwap.bandFill"));
24038
24272
  }
24039
24273
  if (paths.has("maxCurvature")) {
24040
- const cur = drawing.maxCurvature ?? 24;
24274
+ const cur = common((d) => d.maxCurvature ?? 24);
24041
24275
  bar.appendChild(
24042
24276
  this.dropdown("Max curvature", DEDEKIND_CURVATURE_OPTIONS, cur, () => DEDEKIND_ICON2, (v) => actions.patch({ maxCurvature: v }), {
24043
24277
  label: (v) => `n=${v}`,
@@ -24046,7 +24280,7 @@ var DrawingSettingsPopup = class {
24046
24280
  );
24047
24281
  }
24048
24282
  if (paths.has("mach")) {
24049
- const cur = drawing.mach ?? 2;
24283
+ const cur = common((d) => d.mach ?? 2);
24050
24284
  bar.appendChild(
24051
24285
  this.dropdown("Mach number", MACH_NUMBER_OPTIONS, cur, () => SUPERSONIC_ICON2, (v) => actions.patch({ mach: v }), {
24052
24286
  label: (v) => `M=${v}`,
@@ -24055,7 +24289,7 @@ var DrawingSettingsPopup = class {
24055
24289
  );
24056
24290
  }
24057
24291
  if (paths.has("waveCount")) {
24058
- const cur = drawing.waveCount ?? 6;
24292
+ const cur = common((d) => d.waveCount ?? 6);
24059
24293
  bar.appendChild(
24060
24294
  this.dropdown("Waves", MACH_WAVE_COUNT_OPTIONS, cur, () => SONIC_ICON2, (v) => actions.patch({ waveCount: v }), {
24061
24295
  label: (v) => `${v}`,
@@ -24063,26 +24297,27 @@ var DrawingSettingsPopup = class {
24063
24297
  })
24064
24298
  );
24065
24299
  }
24066
- const toggles = drawing;
24067
- if (paths.has("showPrice")) bar.appendChild(this.toggle("Show price", PRICE_DELTA_ICON, toggles.showPrice !== false, (v) => actions.patch({ showPrice: v })));
24068
- if (paths.has("showDate")) bar.appendChild(this.toggle("Show date", DATE_DELTA_ICON, toggles.showDate !== false, (v) => actions.patch({ showDate: v })));
24069
- 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 })));
24070
- 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 }));
24071
- 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 })));
24072
- 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 })));
24073
- if (paths.has("text.value")) bar.appendChild(this.iconBtn("Text", TYPE_ICON, () => this.toggleTextPanel(drawing, actions, !textOnBar)));
24300
+ const flags = (d) => d;
24301
+ if (paths.has("showPrice")) bar.appendChild(this.toggle("Show price", PRICE_DELTA_ICON, common((d) => flags(d).showPrice !== false), (v) => actions.patch({ showPrice: v })));
24302
+ if (paths.has("showDate")) bar.appendChild(this.toggle("Show date", DATE_DELTA_ICON, common((d) => flags(d).showDate !== false), (v) => actions.patch({ showDate: v })));
24303
+ if (paths.has("text.color") && textOnBar) bar.appendChild(swatch("Text color", TYPE_ICON, (d) => d.text?.color || t.textColor, "text.color"));
24304
+ 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 }));
24305
+ const textField = !multi && paths.has("text.value");
24306
+ if (paths.has("text.bold") && !textField) bar.appendChild(this.toggle("Bold", BOLD_ICON, common((d) => !!d.text?.bold), (v) => actions.patch({ "text.bold": v })));
24307
+ if (paths.has("text.italic") && !textField) bar.appendChild(this.toggle("Italic", ITALIC_ICON, common((d) => !!d.text?.italic), (v) => actions.patch({ "text.italic": v })));
24308
+ if (textField) bar.appendChild(this.iconBtn("Text", TYPE_ICON, () => this.toggleTextPanel(drawing, actions, !textOnBar)));
24074
24309
  const editableLevels = drawing.editableLevels();
24075
- if (editableLevels && !(drawing instanceof MachFigure)) {
24076
- const fib = drawing;
24310
+ if (editableLevels && every((d) => d.editableLevels() != null && !(d instanceof MachFigure))) {
24311
+ const fib = (d) => d;
24077
24312
  const sizes = TEXT_SIZE_OPTIONS.map((o) => o.value);
24078
- bar.appendChild(this.dropdown("Numbers size", sizes, fib.numbersSize ?? "small", (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel }));
24079
- bar.appendChild(this.dropdown("Label size", sizes, fib.labelsSize ?? "normal", (s) => labelSizeIcon(s), (v) => actions.patch({ labelsSize: v }), { label: sizeLabel }));
24313
+ bar.appendChild(this.dropdown("Numbers size", sizes, common((d) => fib(d).numbersSize ?? "small"), (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel }));
24314
+ bar.appendChild(this.dropdown("Label size", sizes, common((d) => fib(d).labelsSize ?? "normal"), (s) => labelSizeIcon(s), (v) => actions.patch({ labelsSize: v }), { label: sizeLabel }));
24080
24315
  }
24081
24316
  bar.appendChild(this.divider());
24082
- if (isFrvp) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "frvp")));
24083
- if (isPosition) bar.appendChild(this.iconBtn("Position size", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "position")));
24084
- if (editableLevels) bar.appendChild(this.iconBtn("Levels", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "levels")));
24085
- bar.appendChild(this.toggle("Lock", LOCK_ICON, drawing.locked, (v) => actions.setLocked(v)));
24317
+ if (!multi && isFrvp) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "frvp")));
24318
+ if (!multi && isPosition) bar.appendChild(this.iconBtn("Position size", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "position")));
24319
+ if (!multi && editableLevels) bar.appendChild(this.iconBtn("Levels", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "levels")));
24320
+ bar.appendChild(this.toggle("Lock", LOCK_ICON, common((d) => d.locked), (v) => actions.setLocked(v), UNLOCK_ICON));
24086
24321
  const del = this.iconBtn("Delete", TRASH_ICON, () => actions.remove());
24087
24322
  del.style.color = "var(--vela-danger)";
24088
24323
  bar.appendChild(del);
@@ -24268,7 +24503,7 @@ var DrawingSettingsPopup = class {
24268
24503
  });
24269
24504
  return b;
24270
24505
  }
24271
- /** The trailing kebab overflow — z-order actions + reset settings. */
24506
+ /** The trailing kebab overflow — duplicate, z-order actions, reset settings. */
24272
24507
  kebabButton(actions) {
24273
24508
  const b = this.base("More");
24274
24509
  b.innerHTML = sized(KEBAB_ICON);
@@ -24280,6 +24515,7 @@ var DrawingSettingsPopup = class {
24280
24515
  return;
24281
24516
  }
24282
24517
  this.openActionMenu(b, [
24518
+ { icon: CLONE_ICON, label: "Duplicate", onClick: () => actions.duplicate() },
24283
24519
  { icon: FRONT_ICON, label: "Bring to front", onClick: () => actions.reorder("front") },
24284
24520
  { icon: BACK_ICON, label: "Send to back", onClick: () => actions.reorder("back") },
24285
24521
  { icon: RESET_ICON, label: "Reset settings", onClick: () => actions.resetSettings() }
@@ -24436,26 +24672,30 @@ var DrawingSettingsPopup = class {
24436
24672
  this.tipEl?.remove();
24437
24673
  this.tipEl = null;
24438
24674
  }
24439
- toggle(tip, icon2, active, onChange) {
24675
+ /** An on/off button. A mixed state (the selected drawings disagree) reads as half-lit and
24676
+ * the first click turns it ON for all of them. `off` swaps in a distinct glyph while the
24677
+ * toggle is off (a lock reads as an open padlock until it is engaged); mixed shows `icon`. */
24678
+ toggle(tip, icon2, active, onChange, off) {
24440
24679
  const b = this.base(tip);
24441
- b.innerHTML = sized(icon2);
24442
24680
  const set = (on2) => {
24443
- b.dataset.active = on2 ? "1" : "0";
24681
+ b.dataset.active = on2 === MIXED ? "mixed" : on2 ? "1" : "0";
24682
+ b.innerHTML = sized(off && on2 === false ? off : icon2);
24444
24683
  };
24445
24684
  set(active);
24446
24685
  let on = active;
24447
24686
  b.addEventListener("click", () => {
24448
- on = !on;
24687
+ on = on === MIXED ? true : !on;
24449
24688
  set(on);
24450
24689
  onChange(on);
24451
24690
  });
24452
24691
  return b;
24453
24692
  }
24454
24693
  /** An inline numeric width field for tools whose stroke range outgrows the 1–4px
24455
- * ladder — the value is clamped to the schema's declared min/max on commit. */
24694
+ * ladder — the value is clamped to the schema's declared min/max on commit. A mixed
24695
+ * value shows an empty field with a dash until a number is typed for all. */
24456
24696
  widthInput(tip, value, min, max, step, onChange) {
24457
24697
  const ni = new NumberInput({
24458
- value,
24698
+ value: value === MIXED ? min : value,
24459
24699
  min,
24460
24700
  max,
24461
24701
  step,
@@ -24467,13 +24707,18 @@ var DrawingSettingsPopup = class {
24467
24707
  title: tip,
24468
24708
  onChange
24469
24709
  });
24710
+ if (value === MIXED) {
24711
+ ni.input.value = "";
24712
+ ni.input.placeholder = "\u2014";
24713
+ }
24470
24714
  ni.el.dataset.tip = tip;
24471
24715
  trapChartKeys(ni.el);
24472
24716
  return ni.el;
24473
24717
  }
24474
24718
  /** A pick-one dropdown: the trigger shows the current value's glyph (plus an optional inline
24475
24719
  * text label — e.g. `2px` — for controls that would otherwise look alike), and clicking it
24476
- * opens a floating list of every option so a value is one click away (no cycling through). */
24720
+ * opens a floating list of every option so a value is one click away (no cycling through).
24721
+ * A mixed value shows a dash in the trigger and highlights no row. */
24477
24722
  dropdown(tip, values, current, render, onChange, opts = {}) {
24478
24723
  const b = this.base(tip);
24479
24724
  b.style.width = "auto";
@@ -24483,14 +24728,20 @@ var DrawingSettingsPopup = class {
24483
24728
  let cur = current;
24484
24729
  const paint = (v) => {
24485
24730
  b.replaceChildren();
24486
- const glyph = render(v);
24731
+ if (v === MIXED) {
24732
+ const dash = document.createElement("span");
24733
+ dash.textContent = "\u2014";
24734
+ dash.style.cssText = "min-width:18px;text-align:center;opacity:0.85;";
24735
+ b.appendChild(dash);
24736
+ }
24737
+ const glyph = v === MIXED ? "" : render(v);
24487
24738
  if (glyph) {
24488
24739
  const ic = document.createElement("span");
24489
24740
  ic.style.cssText = "display:flex;";
24490
24741
  ic.innerHTML = sized(glyph);
24491
24742
  b.appendChild(ic);
24492
24743
  }
24493
- if (opts.label && opts.labelInTrigger) {
24744
+ if (opts.label && opts.labelInTrigger && v !== MIXED) {
24494
24745
  const tx = document.createElement("span");
24495
24746
  tx.textContent = opts.label(v);
24496
24747
  tx.style.cssText = "font-size:11px;opacity:0.85;font-variant-numeric:tabular-nums;";
@@ -24560,8 +24811,10 @@ var DrawingSettingsPopup = class {
24560
24811
  }
24561
24812
  /** A color control: an `icon` over a thin **colored underline** showing the current
24562
24813
  * value (the common idiom), with an invisible native picker overlaid to edit it.
24563
- * `iconSize` shrinks it for the floating text controls. */
24564
- colorButton(tip, icon2, color, onChange, iconSize = 17) {
24814
+ * `iconSize` shrinks it for the floating text controls. A mixed color paints the
24815
+ * underline striped with the colors `used` across the selection, and the picker then
24816
+ * leads with those colors so unifying onto one of them is a single click. */
24817
+ colorButton(tip, icon2, color, onChange, iconSize = 17, used = []) {
24565
24818
  const b = document.createElement("button");
24566
24819
  b.type = "button";
24567
24820
  b.dataset.tip = tip;
@@ -24571,13 +24824,14 @@ var DrawingSettingsPopup = class {
24571
24824
  ic.style.cssText = "display:flex;";
24572
24825
  ic.innerHTML = sized(icon2, iconSize);
24573
24826
  const bar = document.createElement("span");
24574
- bar.style.cssText = `display:block;height:3px;width:${Math.round(iconSize * 0.85)}px;border-radius:2px;background:${color};`;
24827
+ bar.style.cssText = `display:block;height:3px;width:${Math.round(iconSize * 0.85)}px;border-radius:2px;background:${color === MIXED ? stripes(used) : color};`;
24575
24828
  b.append(ic, bar);
24576
24829
  let cur = color;
24577
24830
  b.addEventListener("pointerdown", (e) => e.stopPropagation());
24578
24831
  b.addEventListener("click", (e) => {
24579
24832
  e.stopPropagation();
24580
- this.toggleColorPopover(b, cur, (v) => {
24833
+ const seed = cur === MIXED ? used[0] ?? DEFAULT_DRAWING_COLOR : cur;
24834
+ this.toggleColorPopover(b, seed, cur === MIXED ? used : [], (v) => {
24581
24835
  cur = v;
24582
24836
  bar.style.background = v;
24583
24837
  onChange(v);
@@ -24586,20 +24840,42 @@ var DrawingSettingsPopup = class {
24586
24840
  return b;
24587
24841
  }
24588
24842
  /** Open the color popover for `anchor`, or close it if it's already this anchor's (toggle). */
24589
- toggleColorPopover(anchor, color, onChange) {
24843
+ toggleColorPopover(anchor, color, used, onChange) {
24590
24844
  if (this.colorOwner === anchor) {
24591
24845
  this.closeColorPopover();
24592
24846
  return;
24593
24847
  }
24594
- this.openColorPopover(anchor, color, onChange);
24848
+ this.openColorPopover(anchor, color, used, onChange);
24595
24849
  }
24596
- /** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`. */
24597
- openColorPopover(anchor, color, onChange) {
24850
+ /** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`.
24851
+ * `used` (a mixed swatch's colors) leads the picker as one-click unify shortcuts. */
24852
+ openColorPopover(anchor, color, used, onChange) {
24598
24853
  const t = this.theme;
24599
24854
  this.colorPop = this.hostFloat(anchor, {
24600
24855
  zIndex: 25,
24601
24856
  padding: "10px",
24602
- fill: (el) => {
24857
+ fill: (el, pop) => {
24858
+ if (used.length > 1) {
24859
+ const row = document.createElement("div");
24860
+ row.style.cssText = "display:flex;align-items:center;gap:6px;padding-bottom:9px;border-bottom:1px solid var(--vela-border);";
24861
+ const lbl = document.createElement("span");
24862
+ lbl.textContent = "In use";
24863
+ lbl.style.cssText = "font:var(--vela-font-size-sm) inherit;opacity:0.7;margin-right:2px;";
24864
+ row.appendChild(lbl);
24865
+ for (const c of used) {
24866
+ const sw = document.createElement("button");
24867
+ sw.type = "button";
24868
+ sw.dataset.tip = c;
24869
+ sw.style.cssText = `width:18px;height:18px;border-radius:4px;border:1px solid var(--vela-border-strong);cursor:pointer;background:${c};padding:0;`;
24870
+ sw.addEventListener("click", (e) => {
24871
+ e.stopPropagation();
24872
+ pop.hide();
24873
+ onChange(c);
24874
+ });
24875
+ row.appendChild(sw);
24876
+ }
24877
+ el.appendChild(row);
24878
+ }
24603
24879
  el.appendChild(buildColorPicker(color, t, onChange));
24604
24880
  }
24605
24881
  });
@@ -24620,6 +24896,7 @@ var TYPE_ICON = icon("type");
24620
24896
  var PRICE_DELTA_ICON = icon("price-delta");
24621
24897
  var DATE_DELTA_ICON = icon("date-delta");
24622
24898
  var LOCK_ICON = icon("lock");
24899
+ var UNLOCK_ICON = icon("unlock");
24623
24900
  var FRONT_ICON = icon("bring-front");
24624
24901
  var BACK_ICON = icon("send-back");
24625
24902
  var TRASH_ICON = icon("trash");
@@ -24627,6 +24904,7 @@ var BOLD_ICON = icon("bold");
24627
24904
  var ITALIC_ICON = icon("italic");
24628
24905
  var GRIP_ICON = icon("grip");
24629
24906
  var KEBAB_ICON = icon("kebab");
24907
+ var CLONE_ICON = icon("clone");
24630
24908
  var RESET_ICON = icon("reset");
24631
24909
  var GEAR_ICON = icon("gear");
24632
24910
  var CHEVRON_ICON = icon("chevron-down");
@@ -24635,6 +24913,12 @@ var BANDS_ICON = icon("bands");
24635
24913
  var DEDEKIND_ICON2 = icon("dedekind");
24636
24914
  var SONIC_ICON2 = icon("sonic");
24637
24915
  var SUPERSONIC_ICON2 = icon("supersonic");
24916
+ function stripes(colors) {
24917
+ const bands = colors.length > 1 ? colors : ["var(--vela-fg-faint)", "var(--vela-fg-muted)"];
24918
+ const step = 100 / bands.length;
24919
+ const stops = bands.map((c, i) => `${c} ${i * step}% ${(i + 1) * step}%`).join(",");
24920
+ return `linear-gradient(90deg,${stops})`;
24921
+ }
24638
24922
  function lineIcon(width, style) {
24639
24923
  const dash = style === "dashed" ? "5,3" : style === "dotted" ? "1.5,3" : "";
24640
24924
  return svg24(`<line x1="3.5" y1="12" x2="20.5" y2="12" stroke-width="${Number(width) || 2}" stroke-dasharray="${dash}"/>`);
@@ -25550,6 +25834,20 @@ var UserDrawingController = class {
25550
25834
  // reported exactly when it actually changes).
25551
25835
  this.textEditor = null;
25552
25836
  this.painter = new DrawingPainter();
25837
+ /** The open popup is the multi-selection bar (vs one drawing's own) — decides what a
25838
+ * shrinking selection does with it. */
25839
+ this.popupMulti = false;
25840
+ /** Dismiss-on-outside-press also clears the selection — except when the press is ABOUT the
25841
+ * selection: it carries a multi-select modifier (Shift / Ctrl / Cmd — it will add to the
25842
+ * selection or sweep a marquee onto it), or it landed on a drawing that is already selected
25843
+ * (the canvas handler ran first and is holding it for a drag or a click), so the drawings
25844
+ * whose bar just closed must stay selected. */
25845
+ this.onPopupDismissed = (e) => {
25846
+ if (e && (e.shiftKey || e.ctrlKey || e.metaKey)) return;
25847
+ const pressed = this.interaction.pressedId();
25848
+ if (pressed && this.selectedIds.has(pressed)) return;
25849
+ this.clearSelection();
25850
+ };
25553
25851
  this.ctx = canvas.getContext("2d");
25554
25852
  this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), () => deps.chartBarMs());
25555
25853
  this.toolbar = new DrawingToolbar(
@@ -25726,6 +26024,12 @@ var UserDrawingController = class {
25726
26024
  }
25727
26025
  setSelection(ids) {
25728
26026
  this.selectedIds = new Set(ids);
26027
+ if (ids.length >= 2) this.openSettingsForSelection(ids);
26028
+ else if (this.popupMulti) {
26029
+ this.popupMulti = false;
26030
+ this.popup.close();
26031
+ if (ids.length === 1) this.openSettingsById(ids[0], 0, 0);
26032
+ }
25729
26033
  this.render();
25730
26034
  }
25731
26035
  /** Programmatic twin of clicking the drawing: highlight it + float its settings popup. */
@@ -25771,12 +26075,16 @@ var UserDrawingController = class {
25771
26075
  });
25772
26076
  }
25773
26077
  /** Delete the (unlocked) drawing under the cursor. True when one was removed.
25774
- * Shared by the eraser (click + drag) and the middle-click shortcut. */
25775
- deleteAt(x, y) {
26078
+ * Shared by the eraser (click + drag) and the middle-click shortcut; the latter passes
26079
+ * `withSelection` so a hit on a member of a multi-selection removes the selection's
26080
+ * unlocked members — whichever member was hit, locked or not. */
26081
+ deleteAt(x, y, withSelection = false) {
25776
26082
  const hit = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE);
25777
- if (!hit || hit.locked) return false;
26083
+ if (!hit) return false;
26084
+ const ids = deleteTargets(hit, this.selectedIds, this.drawings, withSelection);
26085
+ if (ids.length === 0) return false;
25778
26086
  this.popup.close();
25779
- this.emit({ kind: "delete", ids: [hit.id] });
26087
+ this.emit({ kind: "delete", ids });
25780
26088
  return true;
25781
26089
  }
25782
26090
  /** Shift+press on the empty plot: arm the measure ruler AND start it at (x, y) in one
@@ -25802,7 +26110,14 @@ var UserDrawingController = class {
25802
26110
  this.render();
25803
26111
  }
25804
26112
  }
25805
- pointerDown(x, y, snap = "off", shift2 = false) {
26113
+ /** Ctrl/Cmd+press on the empty plot: sweep a selection box (adds the drawings it touches to
26114
+ * the selection). Returns false when a mode/tool is active (the normal press path owns it). */
26115
+ beginMarqueeAt(x, y) {
26116
+ if (this.measureMode || this.eraserMode || !this.interaction.beginMarquee(x, y)) return false;
26117
+ this.render();
26118
+ return true;
26119
+ }
26120
+ pointerDown(x, y, snap = "off", shift2 = false, mod = false) {
25806
26121
  if (this.eraserMode) {
25807
26122
  this.erasing = true;
25808
26123
  this.deleteAt(x, y);
@@ -25822,9 +26137,9 @@ var UserDrawingController = class {
25822
26137
  return;
25823
26138
  }
25824
26139
  }
25825
- this.interaction.down(x, y, snap, shift2);
26140
+ this.interaction.down(x, y, snap, shift2, mod);
25826
26141
  }
25827
- pointerMove(x, y, snap = "off", shift2 = false) {
26142
+ pointerMove(x, y, snap = "off", shift2 = false, mod = false) {
25828
26143
  if (this.eraserMode) {
25829
26144
  if (this.erasing) this.deleteAt(x, y);
25830
26145
  return;
@@ -25836,12 +26151,14 @@ var UserDrawingController = class {
25836
26151
  return;
25837
26152
  }
25838
26153
  this.interaction.move(x, y, snap, shift2);
25839
- this.updateHover(x, y);
26154
+ this.updateHover(x, y, mod);
25840
26155
  }
25841
- /** Track which drawing is hovered so its handles appear on hover (and only then). */
25842
- updateHover(x, y) {
26156
+ /** Track which drawing is hovered so its handles appear on hover (and only then). While
26157
+ * Ctrl/Cmd is held the cursor is picking a selection — handles then mark only what IS
26158
+ * selected, so a hovered candidate doesn't already read as selected. */
26159
+ updateHover(x, y, mod = false) {
25843
26160
  let id = null;
25844
- if (this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
26161
+ if (!mod && this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
25845
26162
  id = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE)?.id ?? null;
25846
26163
  }
25847
26164
  if (id !== this.hoveredId) {
@@ -25861,7 +26178,12 @@ var UserDrawingController = class {
25861
26178
  this.render();
25862
26179
  return;
25863
26180
  }
26181
+ const dragged = this.interaction.isDragging() ? this.interaction.pressedId() : null;
25864
26182
  this.interaction.up(x, y);
26183
+ if (dragged && this.selectedIds.has(dragged) && !this.popup.isOpen()) {
26184
+ if (this.selectedIds.size >= 2) this.openSettingsForSelection(this.selectionIds());
26185
+ else this.openSettingsById(dragged, x, y);
26186
+ }
25865
26187
  }
25866
26188
  /** Toggle the transient ruler. Arming it clears any drawing tool + selection. */
25867
26189
  toggleMeasure() {
@@ -26126,7 +26448,7 @@ var UserDrawingController = class {
26126
26448
  this.emit({ kind: "duplicate", ids: this.selectionIds() });
26127
26449
  break;
26128
26450
  case "delete": {
26129
- const ids = this.selectedIds.size ? this.selectionIds() : this.hoveredId ? [this.hoveredId] : [];
26451
+ const ids = this.selectedIds.size ? this.deletableSelection() : this.hoveredId ? [this.hoveredId] : [];
26130
26452
  if (ids.length) {
26131
26453
  this.popup.close();
26132
26454
  this.emit({ kind: "delete", ids });
@@ -26142,6 +26464,9 @@ var UserDrawingController = class {
26142
26464
  selectionIds() {
26143
26465
  return [...this.selectedIds];
26144
26466
  }
26467
+ deletableSelection() {
26468
+ return deletableSelection(this.selectedIds, this.drawings);
26469
+ }
26145
26470
  /** Move every selected (unlocked) drawing by a pixel delta — one edit/edit-many → one undo step. */
26146
26471
  nudgeSelection(dx, dy) {
26147
26472
  const proj = this.deps.projector();
@@ -26250,6 +26575,8 @@ var UserDrawingController = class {
26250
26575
  this.painter.seriesLook = this.deps.seriesLook();
26251
26576
  this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
26252
26577
  this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
26578
+ const clones = this.interaction.dragClones();
26579
+ if (clones) this.painter.paintAll(ctx, clones, proj, this.deps.theme(), { selected: new Set(clones.map((c) => c.id)) });
26253
26580
  this.layoutTextEditor();
26254
26581
  const ghost = this.interaction.ghost();
26255
26582
  if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
@@ -26265,6 +26592,8 @@ var UserDrawingController = class {
26265
26592
  const my = m ? proj.yOf(m.point.price, m.paneId) : null;
26266
26593
  if (m && my != null) this.painter.paintSnapRing(ctx, proj.xOf(m.point.time), my, this.deps.theme());
26267
26594
  if (this.measure.isActive()) this.measure.paint(ctx, proj, this.deps.theme());
26595
+ const marquee = this.interaction.marqueeRect();
26596
+ if (marquee) this.painter.paintMarquee(ctx, marquee);
26268
26597
  }
26269
26598
  destroy() {
26270
26599
  this.closeTextEditor();
@@ -26282,15 +26611,80 @@ var UserDrawingController = class {
26282
26611
  if (this.selectedIds.size) this.emit({ kind: "select", ids: [] });
26283
26612
  this.render();
26284
26613
  }
26614
+ /** Report the live drawings' current state as one edit (one undo step, however many). */
26615
+ emitEdits(drawings) {
26616
+ const docs = drawings.map((d) => d.serialize());
26617
+ if (docs.length === 1) this.emit({ kind: "edit", doc: docs[0] });
26618
+ else if (docs.length > 1) this.emit({ kind: "edit-many", docs });
26619
+ }
26620
+ /** The one bar for a multi-selection: it shows the controls all of its drawings share and
26621
+ * every action applies to all of them. Floats clear of their combined bounds. */
26622
+ openSettingsForSelection(ids) {
26623
+ const live = () => ids.map((id) => this.drawings.find((d) => d.id === id)).filter((d) => d != null);
26624
+ const drawings = live();
26625
+ if (drawings.length < 2) return;
26626
+ const proj = this.deps.projector();
26627
+ let anchor = null;
26628
+ for (const d of drawings) {
26629
+ const b = d.bounds(proj);
26630
+ if (!b) continue;
26631
+ if (!anchor) anchor = { ...b };
26632
+ else {
26633
+ const right = Math.max(anchor.x + anchor.w, b.x + b.w);
26634
+ const bottom = Math.max(anchor.y + anchor.h, b.y + b.h);
26635
+ anchor.x = Math.min(anchor.x, b.x);
26636
+ anchor.y = Math.min(anchor.y, b.y);
26637
+ anchor.w = right - anchor.x;
26638
+ anchor.h = bottom - anchor.y;
26639
+ }
26640
+ }
26641
+ this.popupMulti = true;
26642
+ this.emit({ kind: "settings", id: drawings[0].id });
26643
+ this.popup.open(drawings, anchor, {
26644
+ resolve: () => live()[0] ?? null,
26645
+ patch: (p) => {
26646
+ const ds = live();
26647
+ for (const d of ds) d.applySettings(p);
26648
+ this.render();
26649
+ this.emitEdits(ds);
26650
+ },
26651
+ setLocked: (v) => {
26652
+ const ds = live();
26653
+ for (const d of ds) d.locked = v;
26654
+ this.emitEdits(ds);
26655
+ },
26656
+ reorder: (to) => {
26657
+ for (const d of live()) this.emit({ kind: "reorder", id: d.id, to });
26658
+ },
26659
+ duplicate: () => {
26660
+ this.popup.close();
26661
+ this.emit({ kind: "duplicate", ids: live().map((d) => d.id) });
26662
+ },
26663
+ resetSettings: () => {
26664
+ const ds = live();
26665
+ for (const d of ds) resetDrawingSettings(d);
26666
+ this.render();
26667
+ this.emitEdits(ds);
26668
+ this.openSettingsForSelection(ids);
26669
+ },
26670
+ remove: () => {
26671
+ const ids2 = live().filter((d) => !d.locked).map((d) => d.id);
26672
+ if (ids2.length === 0) return;
26673
+ this.popup.close();
26674
+ this.emit({ kind: "delete", ids: ids2 });
26675
+ }
26676
+ }, this.onPopupDismissed);
26677
+ }
26285
26678
  /** A click on a drawing opens its settings toolbar (text labels edit text here too). */
26286
26679
  openSettingsById(id, _x, _y) {
26287
26680
  const drawing = this.drawings.find((d) => d.id === id);
26288
26681
  if (!drawing) return;
26289
26682
  const live = () => this.drawings.find((d) => d.id === id);
26683
+ this.popupMulti = false;
26290
26684
  this.emit({ kind: "select", ids: [id] });
26291
26685
  this.emit({ kind: "settings", id });
26292
26686
  const anchor = drawing.bounds(this.deps.projector());
26293
- this.popup.open(drawing, anchor, {
26687
+ this.popup.open([drawing], anchor, {
26294
26688
  // Sync rebuilds instances, so a panel that reads values back after a patch (e.g. the
26295
26689
  // position tool's price fields, where one edit can flip another level) resolves fresh.
26296
26690
  resolve: () => live() ?? null,
@@ -26308,6 +26702,11 @@ var UserDrawingController = class {
26308
26702
  this.emit({ kind: "edit", doc: d.serialize() });
26309
26703
  },
26310
26704
  reorder: (to) => this.emit({ kind: "reorder", id, to }),
26705
+ duplicate: () => {
26706
+ this.finishTextEditor(true);
26707
+ this.popup.close();
26708
+ this.emit({ kind: "duplicate", ids: [id] });
26709
+ },
26311
26710
  resetSettings: () => {
26312
26711
  const d = live();
26313
26712
  if (!d) return;
@@ -26330,7 +26729,13 @@ var UserDrawingController = class {
26330
26729
  this.popup.close();
26331
26730
  this.emit({ kind: "delete", ids: [id] });
26332
26731
  }
26333
- }, () => this.clearSelection());
26732
+ }, this.onPopupDismissed);
26733
+ }
26734
+ /** A plain click on the empty plot: drop the selection. The popup's dismiss-on-outside-press
26735
+ * does the same, but a selection can exist with no bar open (a lone Ctrl-click pick, a
26736
+ * selection whose bar was dismissed by a modifier press) — this covers those. */
26737
+ deselect() {
26738
+ this.clearSelection();
26334
26739
  }
26335
26740
  /** Report placement progress upstream (`draft` intent) so the drawings sync can
26336
26741
  * mirror the ghost live on linked charts. Called from every render; only actual
@@ -26356,6 +26761,13 @@ var UserDrawingController = class {
26356
26761
  }, 0);
26357
26762
  return;
26358
26763
  }
26764
+ if (i.kind === "clone") {
26765
+ const before = new Set(this.drawings.map((d) => d.id));
26766
+ this.intentCb?.(i);
26767
+ const fresh = this.drawings.filter((d) => !before.has(d.id));
26768
+ if (fresh.length === 1) this.openSettingsById(fresh[0].id, 0, 0);
26769
+ return;
26770
+ }
26359
26771
  this.intentCb?.(i);
26360
26772
  }
26361
26773
  };
@@ -27520,7 +27932,6 @@ var ZOOM_OUT_MARGIN_BARS = 6;
27520
27932
  var ZOOM_TAU_MS = 70;
27521
27933
  var SCALE_TAU_MS = 80;
27522
27934
  var FLING_TAU_MS = 110;
27523
- var LIVE_BAR_TAU_MS = 90;
27524
27935
  var SCROLL_TO_TAU_MS = 130;
27525
27936
  var FLING_STOP_PX = 0.02;
27526
27937
  var PRICE_SCALE_K = 4e-3;
@@ -27591,9 +28002,13 @@ var NativeRenderer = class {
27591
28002
  /** Drawings layer self-serves Ctrl+Z/Y (see the `historyChords` feature). */
27592
28003
  this.historyChordsEnabled = true;
27593
28004
  this.liveRegion = null;
27594
- // ── animation state (eased zoom + inertial pan) ──
28005
+ // ── animation state (eased zoom + inertial pan + live-bar glide) ──
27595
28006
  this.animZoom = true;
27596
28007
  this.animPan = true;
28008
+ this.animLiveBarMs = 0;
28009
+ // forming-bar OHLC glide time-constant; 0 = each tick snaps
28010
+ this.animLiveBarOnMs = LIVE_BAR_EASE_DEFAULT_MS;
28011
+ // the duration the settings-dialog on/off toggle restores (last non-zero value configured)
27597
28012
  // Brand default candles.
27598
28013
  this.candleUp = BULLISH;
27599
28014
  this.candleDown = BEARISH;
@@ -27718,7 +28133,7 @@ var NativeRenderer = class {
27718
28133
  this.moveIndicatorCbs = /* @__PURE__ */ new Set();
27719
28134
  this.priceStyleCbs = /* @__PURE__ */ new Set();
27720
28135
  this.name = "native";
27721
- 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"];
28136
+ 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"];
27722
28137
  /** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
27723
28138
  * so moving onto the button doesn't count as leaving). */
27724
28139
  this.onScrollProximityMove = (e) => {
@@ -27752,6 +28167,7 @@ var NativeRenderer = class {
27752
28167
  this.backendMode = opts.nativeBackend;
27753
28168
  this.animZoom = opts.animZoom;
27754
28169
  this.animPan = opts.animPan;
28170
+ this.setLiveBarEase(resolveLiveBarEaseMs(opts.animLiveBar));
27755
28171
  this.glowAmount = opts.glow;
27756
28172
  this.candleUp = opts.upColor;
27757
28173
  this.candleDown = opts.downColor;
@@ -27801,6 +28217,10 @@ var NativeRenderer = class {
27801
28217
  case "animPan":
27802
28218
  this.animPan = Boolean(value);
27803
28219
  return;
28220
+ case "animLiveBar":
28221
+ this.setLiveBarEase(resolveLiveBarEaseMs(value));
28222
+ return;
28223
+ // affects the next tick only; a glide in flight finishes at the new rate (or snaps at 0)
27804
28224
  case "intro": {
27805
28225
  const s = value === false || value === "none" || value === "off" || value == null ? "" : String(value);
27806
28226
  this.introStyle = s;
@@ -27937,6 +28357,8 @@ var NativeRenderer = class {
27937
28357
  return this.animZoom;
27938
28358
  case "animPan":
27939
28359
  return this.animPan;
28360
+ case "animLiveBar":
28361
+ return this.animLiveBarMs;
27940
28362
  case "intro":
27941
28363
  return this.introStyle;
27942
28364
  case "zoomAnchor":
@@ -28105,7 +28527,8 @@ var NativeRenderer = class {
28105
28527
  labelsVisible: this.scene.showAxisLabels,
28106
28528
  currentPriceLine: this.scene.showPriceLine,
28107
28529
  priceLabel: this.scene.showPriceLabel,
28108
- countdown: this.scene.showCountdown
28530
+ countdown: this.scene.showCountdown,
28531
+ animateLastPrice: this.animLiveBarMs > 0
28109
28532
  },
28110
28533
  panes: { separatorColor: s.separatorColor ?? t.borderColor },
28111
28534
  trades: {
@@ -28212,6 +28635,7 @@ var NativeRenderer = class {
28212
28635
  this.scene.showPriceLabel = next.priceScale.priceLabel;
28213
28636
  this.scene.showCountdown = next.priceScale.countdown;
28214
28637
  this.syncCountdownTimer();
28638
+ this.animLiveBarMs = next.priceScale.animateLastPrice ? this.animLiveBarOnMs : 0;
28215
28639
  s.separatorColor = keepInherit(s.separatorColor, next.panes.separatorColor, prevTheme.borderColor);
28216
28640
  this.scene.tradeMarkers = {
28217
28641
  visible: next.trades.visible,
@@ -28655,7 +29079,10 @@ var NativeRenderer = class {
28655
29079
  zoomTo: (target, anchorLogical, anchorX) => this.zoomTo(target, anchorLogical, anchorX),
28656
29080
  fling: (v) => this.fling(v),
28657
29081
  onPointerMove: (x, y) => this.handlePointerMove(x, y),
28658
- onClick: (x) => this.handleClick(x),
29082
+ onClick: (x) => {
29083
+ this.userDrawings?.deselect();
29084
+ this.handleClick(x);
29085
+ },
28659
29086
  onAxisLongPress: (axis, x, y) => {
28660
29087
  for (const cb of this.axisLongPressCbs) cb({ axis, x, y });
28661
29088
  },
@@ -28673,11 +29100,13 @@ var NativeRenderer = class {
28673
29100
  // User drawings claim a gesture before pan when armed / over a drawing.
28674
29101
  drawingsClaim: (x, y) => this.userDrawings?.claim(x, y) ?? false,
28675
29102
  drawingsMeasureStart: (x, y, snap) => this.userDrawings?.beginMeasureAt(x, y, snap) ?? false,
28676
- drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y) ?? false,
29103
+ drawingsMarqueeStart: (x, y) => this.userDrawings?.beginMarqueeAt(x, y) ?? false,
29104
+ drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y, true) ?? false,
29105
+ // middle-click: a selected hit takes the whole selection
28677
29106
  drawingsCancelPlacement: () => this.userDrawings?.cancelPlacement() ?? false,
28678
29107
  drawingsSnapMode: () => this.snapMode,
28679
- drawingsPointerDown: (x, y, snap, shift2) => this.userDrawings?.pointerDown(x, y, snap, shift2),
28680
- drawingsPointerMove: (x, y, snap, shift2) => this.userDrawings?.pointerMove(x, y, snap, shift2),
29108
+ drawingsPointerDown: (x, y, snap, shift2, mod) => this.userDrawings?.pointerDown(x, y, snap, shift2, mod),
29109
+ drawingsPointerMove: (x, y, snap, shift2, mod) => this.userDrawings?.pointerMove(x, y, snap, shift2, mod),
28681
29110
  drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
28682
29111
  drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
28683
29112
  drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
@@ -29028,8 +29457,11 @@ var NativeRenderer = class {
29028
29457
  const last = this.bars[n - 1];
29029
29458
  if (last && bar.time === last.time) {
29030
29459
  this.bars[n - 1] = bar;
29031
- if (this.liveEaseTime !== bar.time) this.syncLiveEase(bar);
29032
- this.animator.start();
29460
+ if (this.animLiveBarMs <= 0 || this.liveEaseTime !== bar.time) {
29461
+ this.syncLiveEase(bar);
29462
+ } else {
29463
+ this.animator.start();
29464
+ }
29033
29465
  } else if (!last || bar.time > last.time) {
29034
29466
  this.bars.push(bar);
29035
29467
  this.syncLiveEase(bar);
@@ -29041,6 +29473,12 @@ var NativeRenderer = class {
29041
29473
  this.scene.bars = this.bars;
29042
29474
  this.scheduler.invalidate(4 /* Full */);
29043
29475
  }
29476
+ /** Set the live-bar glide duration (0 = off). A non-zero value is also remembered as
29477
+ * what the config's on/off toggle (`priceScale.animateLastPrice`) switches back on to. */
29478
+ setLiveBarEase(ms) {
29479
+ this.animLiveBarMs = ms;
29480
+ if (ms > 0) this.animLiveBarOnMs = ms;
29481
+ }
29044
29482
  /** Snap the eased forming-bar state to `bar` — no glide (a fresh bar or the first tick of one). */
29045
29483
  syncLiveEase(bar) {
29046
29484
  this.liveEaseTime = bar.time;
@@ -29053,9 +29491,10 @@ var NativeRenderer = class {
29053
29491
  const target = this.bars[this.bars.length - 1];
29054
29492
  if (!target || this.liveEaseTime !== target.time) return false;
29055
29493
  const eps = Math.max(1e-9, Math.abs(target.close) * 1e-6);
29056
- const nh = easeToward(this.liveEaseHigh, target.high, dtMs, LIVE_BAR_TAU_MS);
29057
- const nl = easeToward(this.liveEaseLow, target.low, dtMs, LIVE_BAR_TAU_MS);
29058
- const nc = easeToward(this.liveEaseClose, target.close, dtMs, LIVE_BAR_TAU_MS);
29494
+ const tau = this.animLiveBarMs;
29495
+ const nh = easeToward(this.liveEaseHigh, target.high, dtMs, tau);
29496
+ const nl = easeToward(this.liveEaseLow, target.low, dtMs, tau);
29497
+ const nc = easeToward(this.liveEaseClose, target.close, dtMs, tau);
29059
29498
  const active = Math.abs(nh - target.high) > eps || Math.abs(nl - target.low) > eps || Math.abs(nc - target.close) > eps;
29060
29499
  this.liveEaseHigh = active ? nh : target.high;
29061
29500
  this.liveEaseLow = active ? nl : target.low;
@@ -31030,6 +31469,10 @@ function str3(inputs, key, fallback) {
31030
31469
  const v = inputs[key];
31031
31470
  return typeof v === "string" && v !== "" ? v : fallback;
31032
31471
  }
31472
+ function bool(inputs, key, fallback) {
31473
+ const v = inputs[key];
31474
+ return typeof v === "boolean" ? v : fallback;
31475
+ }
31033
31476
  function toPoints(bars, values, colors) {
31034
31477
  const pts = new Array(bars.length);
31035
31478
  for (let i = 0; i < bars.length; i++) {
@@ -32561,10 +33004,6 @@ var aroon = {
32561
33004
  plots: [
32562
33005
  { key: "up", title: "Aroon up", values: up, color: BULLISH, width: 2 },
32563
33006
  { key: "down", title: "Aroon down", values: down, color: BEARISH, width: 2 }
32564
- ],
32565
- levels: [
32566
- { key: "upper", price: 100, color: NEUTRAL, lineStyle: "dotted" },
32567
- { key: "lower", price: 0, color: NEUTRAL, lineStyle: "dotted" }
32568
33007
  ]
32569
33008
  };
32570
33009
  }
@@ -33062,8 +33501,10 @@ var volumeSpecs = [
33062
33501
  var DAY_MS2 = 864e5;
33063
33502
  function periodKey(anchor, time) {
33064
33503
  if (anchor === "Week") return Math.floor((Math.floor(time / DAY_MS2) + 3) / 7);
33065
- if (anchor === "Month") {
33504
+ if (anchor === "Month" || anchor === "Quarter" || anchor === "Year") {
33066
33505
  const d = new Date(time);
33506
+ if (anchor === "Year") return d.getUTCFullYear();
33507
+ if (anchor === "Quarter") return d.getUTCFullYear() * 4 + Math.floor(d.getUTCMonth() / 3);
33067
33508
  return d.getUTCFullYear() * 12 + d.getUTCMonth();
33068
33509
  }
33069
33510
  return Math.floor(time / DAY_MS2);
@@ -33115,40 +33556,86 @@ var parabolicSar = {
33115
33556
  return { plots: [{ key: "sar", title: "PSAR", values, kind: "circles", color: SERIES_LINE, colors }] };
33116
33557
  }
33117
33558
  };
33559
+ var VWAP_BANDS_GROUP = "Bands";
33560
+ var VWAP_STYLE = "Style";
33561
+ var VWAP_BANDS = [
33562
+ { on: "band1", mult: "band1Mult", color: "band1Color", fill: "band1Fill", fillColor: "band1FillColor", defMult: 1, defOn: true, defColor: BULLISH },
33563
+ { on: "band2", mult: "band2Mult", color: "band2Color", fill: "band2Fill", fillColor: "band2FillColor", defMult: 2, defOn: false, defColor: WARNING },
33564
+ { on: "band3", mult: "band3Mult", color: "band3Color", fill: "band3Fill", fillColor: "band3FillColor", defMult: 3, defOn: false, defColor: CATEGORICAL[4] }
33565
+ ];
33118
33566
  var vwap = {
33119
33567
  type: "vwap",
33120
33568
  title: "Volume Weighted Average Price",
33121
33569
  shortTitle: "VWAP",
33122
33570
  overlay: true,
33123
33571
  inputs: [
33124
- { key: "anchor", title: "Anchor", type: "string", defval: "Day", options: ["Day", "Week", "Month"], tooltip: "Where the accumulation resets (UTC periods)" },
33125
- { key: "source", title: "Source", type: "string", defval: "HLC3", options: ["HLC3", "OHLC4", "Close"] },
33126
- colorInput(INFO)
33572
+ { key: "anchor", title: "Period", type: "string", defval: "Day", options: ["Day", "Week", "Month", "Quarter", "Year"], tooltip: "Where the accumulation resets (UTC periods)" },
33573
+ sourceInput("HLC3"),
33574
+ // Each band is one row: its toggle leads, the multiplier follows unlabeled.
33575
+ ...VWAP_BANDS.flatMap((b, i) => [
33576
+ { key: b.on, title: `Band ${i + 1} multiplier`, type: "bool", defval: b.defOn, inline: b.on, group: VWAP_BANDS_GROUP },
33577
+ { key: b.mult, title: "", type: "float", defval: b.defMult, min: 0.1, max: 10, step: 0.1, inline: b.on, group: VWAP_BANDS_GROUP }
33578
+ ]),
33579
+ { ...colorInput(INFO, "VWAP color"), group: VWAP_STYLE },
33580
+ // One row per band: its ink, then the fill toggle with the fill's own (translucent) color.
33581
+ ...VWAP_BANDS.flatMap((b, i) => [
33582
+ { ...colorInput(b.defColor, `Band ${i + 1} color`, b.color), inline: b.color, group: VWAP_STYLE },
33583
+ { key: b.fill, title: "Fill", type: "bool", defval: true, inline: b.color, group: VWAP_STYLE },
33584
+ { ...colorInput(withAlpha2(b.defColor), "", b.fillColor), inline: b.color, group: VWAP_STYLE }
33585
+ ])
33127
33586
  ],
33128
33587
  compute: (bars, inputs) => {
33129
33588
  const anchor = str3(inputs, "anchor", "Day");
33130
- const source = str3(inputs, "source", "HLC3");
33131
- const values = new Array(bars.length).fill(Number.NaN);
33589
+ const src = sourceValues(bars, str3(inputs, "source", "HLC3"));
33590
+ const n = bars.length;
33591
+ const values = new Array(n).fill(Number.NaN);
33592
+ const bands = VWAP_BANDS.filter((b) => bool(inputs, b.on, b.defOn)).map((b) => ({
33593
+ mult: Math.max(0, num3(inputs, b.mult, b.defMult)),
33594
+ color: str3(inputs, b.color, b.defColor),
33595
+ fill: bool(inputs, b.fill, true),
33596
+ fillColor: str3(inputs, b.fillColor, withAlpha2(b.defColor)),
33597
+ up: new Array(n).fill(Number.NaN),
33598
+ down: new Array(n).fill(Number.NaN)
33599
+ }));
33132
33600
  let period = Number.NaN;
33133
33601
  let cumPV = 0;
33602
+ let cumPV2 = 0;
33134
33603
  let cumV = 0;
33135
- for (let i = 0; i < bars.length; i++) {
33604
+ for (let i = 0; i < n; i++) {
33136
33605
  const b = bars[i];
33137
33606
  const key = periodKey(anchor, b.time);
33138
33607
  if (key !== period) {
33139
33608
  period = key;
33140
33609
  cumPV = 0;
33610
+ cumPV2 = 0;
33141
33611
  cumV = 0;
33142
33612
  }
33143
33613
  const v = b.volume;
33144
33614
  if (v != null && Number.isFinite(v) && v > 0) {
33145
- const tp = source === "Close" ? b.close : source === "OHLC4" ? (b.open + b.high + b.low + b.close) / 4 : (b.high + b.low + b.close) / 3;
33615
+ const tp = src[i];
33146
33616
  cumPV += tp * v;
33617
+ cumPV2 += tp * tp * v;
33147
33618
  cumV += v;
33148
33619
  }
33149
- if (cumV > 0) values[i] = cumPV / cumV;
33150
- }
33151
- return { plots: [{ key: "vwap", title: "VWAP", values, color: str3(inputs, "color", INFO), width: 2 }] };
33620
+ if (cumV <= 0) continue;
33621
+ const mean = cumPV / cumV;
33622
+ const sd = Math.sqrt(Math.max(0, cumPV2 / cumV - mean * mean));
33623
+ values[i] = mean;
33624
+ for (const band of bands) {
33625
+ band.up[i] = mean + band.mult * sd;
33626
+ band.down[i] = mean - band.mult * sd;
33627
+ }
33628
+ }
33629
+ const plots = [{ key: "vwap", title: "VWAP", values, color: str3(inputs, "color", INFO), width: 2 }];
33630
+ const fills = [];
33631
+ bands.forEach((band, i) => {
33632
+ plots.push(
33633
+ { key: `up${i}`, title: `Upper ${band.mult}\u03C3`, values: band.up, color: band.color },
33634
+ { key: `down${i}`, title: `Lower ${band.mult}\u03C3`, values: band.down, color: band.color }
33635
+ );
33636
+ if (band.fill) fills.push({ key: `band${i}`, from: `up${i}`, to: `down${i}`, color: band.fillColor });
33637
+ });
33638
+ return { plots, bands: fills };
33152
33639
  }
33153
33640
  };
33154
33641
  var pivotPoints = {
@@ -33339,21 +33826,11 @@ var Vela = class {
33339
33826
  registerClassicIndicators();
33340
33827
  const element = resolveElement(container);
33341
33828
  const theme = resolveTheme(options.theme);
33342
- let animZoom = true;
33343
- let animPan = true;
33344
- if (typeof options.animations === "boolean") {
33345
- animZoom = options.animations;
33346
- animPan = options.animations;
33347
- } else if (options.animations) {
33348
- animZoom = options.animations.zoom ?? true;
33349
- animPan = options.animations.pan ?? true;
33350
- }
33351
33829
  const display = {
33352
33830
  currentPriceLine: options.currentPriceLine ?? true,
33353
33831
  logScale: options.logScale ?? false,
33354
33832
  nativeBackend: options.nativeBackend ?? "auto",
33355
- animZoom,
33356
- animPan,
33833
+ ...resolveAnimations(options.animations),
33357
33834
  glow: options.glow ?? 0,
33358
33835
  upColor: options.upColor ?? BULLISH,
33359
33836
  downColor: options.downColor ?? BEARISH,