@luxalgo/vela 0.6.17 → 0.6.19

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 (37) hide show
  1. package/dist/{DataProvider-B-Jz59Rf.d.ts → DataProvider-Cb-2lC5O.d.ts} +1 -1
  2. package/dist/{DataProvider-OfR3fXg9.d.cts → DataProvider-UtWw2CgJ.d.cts} +1 -1
  3. package/dist/{chunk-YCD72KGK.js → chunk-22QJKGXX.js} +565 -151
  4. package/dist/{chunk-OSZDSH7F.js → chunk-627A5HEV.js} +252 -120
  5. package/dist/{contributions-C3b0Sx5P.d.cts → contributions-5nz3PLRe.d.ts} +9 -4
  6. package/dist/{contributions-Swzz10fG.d.ts → contributions-DEPRtkmG.d.cts} +9 -4
  7. package/dist/index.cjs +563 -149
  8. package/dist/index.d.cts +6 -6
  9. package/dist/index.d.ts +6 -6
  10. package/dist/index.js +1 -1
  11. package/dist/{options-CX1lSYWA.d.cts → options-BCRmYALw.d.cts} +12 -3
  12. package/dist/{options-CX1lSYWA.d.ts → options-BCRmYALw.d.ts} +12 -3
  13. package/dist/{plugin-mvddudqd.d.ts → plugin-CUtfLeqz.d.ts} +3 -3
  14. package/dist/{plugin-BjnEE1zA.d.cts → plugin-DVUaROPo.d.cts} +3 -3
  15. package/dist/plugin.d.cts +4 -4
  16. package/dist/plugin.d.ts +4 -4
  17. package/dist/providers/binance.d.cts +2 -2
  18. package/dist/providers/binance.d.ts +2 -2
  19. package/dist/providers/coinbase.d.cts +2 -2
  20. package/dist/providers/coinbase.d.ts +2 -2
  21. package/dist/providers/hyperliquid.d.cts +2 -2
  22. package/dist/providers/hyperliquid.d.ts +2 -2
  23. package/dist/{statusline-model-IURCya0H.d.ts → statusline-model-hY9cK4BS.d.cts} +17 -7
  24. package/dist/{statusline-model-D91cU2fF.d.cts → statusline-model-wW3m4in1.d.ts} +17 -7
  25. package/dist/ui.d.cts +1 -1
  26. package/dist/ui.d.ts +1 -1
  27. package/dist/vela.global.js +563 -149
  28. package/dist/vela.global.min.js +53 -51
  29. package/dist/widget.cjs +814 -268
  30. package/dist/widget.d.cts +39 -12
  31. package/dist/widget.d.ts +39 -12
  32. package/dist/widget.js +3 -3
  33. package/dist/workspace.cjs +814 -268
  34. package/dist/workspace.d.cts +14 -5
  35. package/dist/workspace.d.ts +14 -5
  36. package/dist/workspace.js +2 -2
  37. package/package.json +1 -1
@@ -6900,15 +6900,18 @@ var Vela = (function (exports) {
6900
6900
  next2 = [...ids];
6901
6901
  }
6902
6902
  this.selectedIds = next2.filter((id) => this.store.has(id));
6903
+ this.announceSelection();
6904
+ }
6905
+ /** Push the selection to the renderer and announce it (primary + the full list). */
6906
+ announceSelection() {
6903
6907
  this.port?.setSelection(this.selectedIds);
6904
- this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null });
6908
+ this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null, ids: [...this.selectedIds] });
6905
6909
  }
6906
6910
  /** Restore a history snapshot, reconciling selection against what survived. */
6907
6911
  restoreSnapshot(doc) {
6908
6912
  this.store.load(doc);
6909
6913
  this.selectedIds = this.selectedIds.filter((id) => this.store.has(id));
6910
- this.port?.setSelection(this.selectedIds);
6911
- this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null });
6914
+ this.announceSelection();
6912
6915
  }
6913
6916
  /** Delete drawings as one undo step; prune them from the selection. */
6914
6917
  deleteMany(ids) {
@@ -7041,6 +7044,9 @@ var Vela = (function (exports) {
7041
7044
  case "duplicate":
7042
7045
  this.duplicate(i.ids);
7043
7046
  break;
7047
+ case "clone":
7048
+ if (this.enabled && i.docs.length) this.insertClones(i.docs);
7049
+ break;
7044
7050
  case "copy":
7045
7051
  this.copy(i.ids);
7046
7052
  break;
@@ -8877,7 +8883,12 @@ var Vela = (function (exports) {
8877
8883
  if (!model) continue;
8878
8884
  const paneId = model.paneId ?? "price";
8879
8885
  if (!byPane.has(paneId)) byPane.set(paneId, []);
8880
- byPane.get(paneId).push({ id: r.id, title: model.title || r.title, ownScale: model.ownScale === true });
8886
+ byPane.get(paneId).push({
8887
+ id: r.id,
8888
+ title: model.title || r.title,
8889
+ ...model.shorttitle ? { shorttitle: model.shorttitle } : {},
8890
+ ownScale: model.ownScale === true
8891
+ });
8881
8892
  }
8882
8893
  const panes = [];
8883
8894
  for (const paneId of this.paneOrder) {
@@ -22948,15 +22959,15 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
22948
22959
  if (!hasOwnCandlePaint(style)) return null;
22949
22960
  const bag = bags[style] ?? {};
22950
22961
  const color = (v) => isColor(v) ? v : null;
22951
- const bool = (v) => isBool(v) ? v : null;
22962
+ const bool2 = (v) => isBool(v) ? v : null;
22952
22963
  return {
22953
22964
  upColor: color(bag.candleUpColor),
22954
22965
  downColor: color(bag.candleDownColor),
22955
- bodyVisible: bool(bag.candleBodyVisible),
22956
- borderVisible: bool(bag.candleBorderVisible),
22966
+ bodyVisible: bool2(bag.candleBodyVisible),
22967
+ borderVisible: bool2(bag.candleBorderVisible),
22957
22968
  borderUpColor: color(bag.candleBorderUpColor),
22958
22969
  borderDownColor: color(bag.candleBorderDownColor),
22959
- wickVisible: bool(bag.candleWickVisible),
22970
+ wickVisible: bool2(bag.candleWickVisible),
22960
22971
  wickUpColor: color(bag.candleWickUpColor),
22961
22972
  wickDownColor: color(bag.candleWickDownColor)
22962
22973
  };
@@ -24747,7 +24758,7 @@ void main() {
24747
24758
  if (e.repeat || e.key !== "Shift" && e.key !== "Control" && e.key !== "Meta") return;
24748
24759
  if (Number.isNaN(this.cursorX)) return;
24749
24760
  if (this.dragging && this.region !== "drawing") return;
24750
- this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey);
24761
+ this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
24751
24762
  };
24752
24763
  this.onDown = (e) => {
24753
24764
  if (e.button === 1) {
@@ -24779,7 +24790,7 @@ void main() {
24779
24790
  this.startY = y;
24780
24791
  if (this.deps.drawingsClaim?.(x, y)) {
24781
24792
  this.region = "drawing";
24782
- this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey);
24793
+ this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
24783
24794
  this.capture(e.pointerId);
24784
24795
  return;
24785
24796
  }
@@ -24788,6 +24799,11 @@ void main() {
24788
24799
  this.capture(e.pointerId);
24789
24800
  return;
24790
24801
  }
24802
+ if ((e.ctrlKey || e.metaKey) && this.regionAt(x, y) === "data" && this.deps.drawingsMarqueeStart?.(x, y)) {
24803
+ this.region = "drawing";
24804
+ this.capture(e.pointerId);
24805
+ return;
24806
+ }
24791
24807
  const vp = this.deps.getCoords().getViewport();
24792
24808
  this.deps.apply(vp);
24793
24809
  this.startRightOffset = vp.rightOffset;
@@ -24836,7 +24852,7 @@ void main() {
24836
24852
  this.deps.apply({ barSpacing, rightOffset: this.startRightOffset });
24837
24853
  } else if (this.region === "drawing") {
24838
24854
  if (Math.abs(x - this.startX) > slop || Math.abs(y - this.startY) > slop) this.moved = true;
24839
- this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
24855
+ this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
24840
24856
  } else {
24841
24857
  const coords = this.deps.getCoords();
24842
24858
  const vp = coords.getViewport();
@@ -24859,7 +24875,7 @@ void main() {
24859
24875
  const drawCursor = this.deps.drawingsCursor?.(x, y);
24860
24876
  const r = this.regionAt(x, y);
24861
24877
  this.el.style.cursor = drawCursor ?? (r === "price" ? "ns-resize" : r === "time" ? "ew-resize" : r === "separator" ? "row-resize" : "");
24862
- this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
24878
+ this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
24863
24879
  }
24864
24880
  if (e.pointerType !== "touch") this.deps.onPointerMove(x, y);
24865
24881
  };
@@ -25200,12 +25216,12 @@ void main() {
25200
25216
  function mergeTradeMarkersState(base, patch) {
25201
25217
  const p = patch && typeof patch === "object" ? patch : {};
25202
25218
  const c = p.colors && typeof p.colors === "object" ? p.colors : {};
25203
- const bool = (v, fb) => typeof v === "boolean" ? v : fb;
25219
+ const bool2 = (v, fb) => typeof v === "boolean" ? v : fb;
25204
25220
  const color = (v, fb) => typeof v === "string" && v.trim().length > 0 ? v : fb;
25205
25221
  return {
25206
- visible: bool(p.visible, base.visible),
25207
- labels: bool(p.labels, base.labels),
25208
- qty: bool(p.qty, base.qty),
25222
+ visible: bool2(p.visible, base.visible),
25223
+ labels: bool2(p.labels, base.labels),
25224
+ qty: bool2(p.qty, base.qty),
25209
25225
  colors: {
25210
25226
  long: color(c.long, base.colors.long),
25211
25227
  short: color(c.short, base.colors.short),
@@ -29155,6 +29171,18 @@ ${overlayScrollbarCss(".vela-sd-pane")}
29155
29171
  }
29156
29172
  this.paintHandles(ctx, pts);
29157
29173
  }
29174
+ /** The selection box a Ctrl/Cmd-drag on the empty plot sweeps: a faint fill in the handle
29175
+ * accent under a thin dashed outline. */
29176
+ paintMarquee(ctx, rect) {
29177
+ ctx.save();
29178
+ ctx.fillStyle = withAlpha(HANDLE_BORDER, 0.08);
29179
+ ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
29180
+ ctx.strokeStyle = HANDLE_BORDER;
29181
+ ctx.lineWidth = 1;
29182
+ ctx.setLineDash([4, 3]);
29183
+ ctx.strokeRect(rect.x + 0.5, rect.y + 0.5, rect.w, rect.h);
29184
+ ctx.restore();
29185
+ }
29158
29186
  /** A hollow ring marking the candle point the magnet (Ctrl) will snap the next anchor to. */
29159
29187
  paintSnapRing(ctx, x, y, theme) {
29160
29188
  ctx.setLineDash([]);
@@ -30623,6 +30651,15 @@ ${overlayScrollbarCss(".vela-sd-pane")}
30623
30651
  }
30624
30652
  return null;
30625
30653
  }
30654
+ function deletableSelection(selected, drawings) {
30655
+ const ids = [...selected];
30656
+ if (ids.length < 2) return ids;
30657
+ return ids.filter((id) => !drawings.find((d) => d.id === id)?.locked);
30658
+ }
30659
+ function deleteTargets(hit, selected, drawings, withSelection) {
30660
+ if (withSelection && selected.size >= 2 && selected.has(hit.id)) return deletableSelection(selected, drawings);
30661
+ return hit.locked ? [] : [hit.id];
30662
+ }
30626
30663
 
30627
30664
  // src/renderers/native/drawings/DrawingInteraction.ts
30628
30665
  var DRAG_SLOP2 = 3;
@@ -30726,10 +30763,32 @@ ${overlayScrollbarCss(".vela-sd-pane")}
30726
30763
  activeDragId() {
30727
30764
  return this.state.kind === "pressed" && this.state.moved ? this.state.id : null;
30728
30765
  }
30766
+ /** The drawing under a press that has not been released yet (drag or click undecided), or null. */
30767
+ pressedId() {
30768
+ return this.state.kind === "pressed" ? this.state.id : null;
30769
+ }
30770
+ /** The copies a Ctrl-drag is moving (transient — not yet in the store), or null. */
30771
+ dragClones() {
30772
+ return this.state.kind === "pressed" && this.state.moved && this.state.clones ? this.state.clones.map((c) => c.d) : null;
30773
+ }
30774
+ /** The marquee box being swept, normalized (or null when none is in progress). */
30775
+ marqueeRect() {
30776
+ if (this.state.kind !== "marquee") return null;
30777
+ const { x0, y0, x1, y1 } = this.state;
30778
+ return { x: Math.min(x0, x1), y: Math.min(y0, y1), w: Math.abs(x1 - x0), h: Math.abs(y1 - y0) };
30779
+ }
30780
+ /** Ctrl/Cmd-press on the empty plot: start sweeping a selection box. False when a tool is
30781
+ * armed or a gesture is in flight (that path owns the press). */
30782
+ beginMarquee(x, y) {
30783
+ if (this.state.kind !== "idle" || this.deps.activeTool() != null) return false;
30784
+ this.state = { kind: "marquee", x0: x, y0: y, x1: x, y1: y };
30785
+ return true;
30786
+ }
30729
30787
  /** A press: place the next anchor, or grab a drawing/handle (resolved as click or drag on release).
30730
30788
  * `shift` over a drawing toggles it in/out of the selection (multi-select) instead of dragging;
30731
- * while placing a line tool it locks the segment angle to 45° steps. */
30732
- down(x, y, mode = "off", shift4 = false) {
30789
+ * while placing a line tool it locks the segment angle to 45° steps. `mod` (Ctrl/Cmd) over a
30790
+ * drawing also toggles on a click, but a body drag then moves a COPY. */
30791
+ down(x, y, mode = "off", shift4 = false, mod = false) {
30733
30792
  const proj = this.deps.projector();
30734
30793
  if (this.state.kind === "placing") {
30735
30794
  if (this.state.draft.placementMode() !== "click") return;
@@ -30757,7 +30816,50 @@ ${overlayScrollbarCss(".vela-sd-pane")}
30757
30816
  return;
30758
30817
  }
30759
30818
  const handle = hit.hitHandle(x, y, proj, HIT_TOLERANCE);
30760
- this.state = { kind: "pressed", id: hit.id, handle, grab: proj.pxToPoint(x, y, hit.paneId), orig: cloneAnchors(hit), px: x, py: y, moved: false };
30819
+ const riders = [];
30820
+ if (handle < 0 && this.deps.selectedIds().has(hit.id)) {
30821
+ for (const id of this.deps.selectedIds()) {
30822
+ const d = id === hit.id ? null : this.byId(id);
30823
+ if (d && !d.locked && d.visible) riders.push({ id, orig: cloneAnchors(d) });
30824
+ }
30825
+ }
30826
+ this.state = { kind: "pressed", id: hit.id, handle, orig: cloneAnchors(hit), px: x, py: y, moved: false, riders, mod, clones: null };
30827
+ }
30828
+ }
30829
+ /** The data-space delta a body drag from the press to (x, y) means on `paneId` — resolved per
30830
+ * pane so drawings riding along on another pane move by the same PIXELS, not the same price. */
30831
+ bodyDelta(paneId, x, y) {
30832
+ if (this.state.kind !== "pressed") return { dt: 0, dp: 0 };
30833
+ const proj = this.deps.projector();
30834
+ const from = proj.pxToPoint(this.state.px, this.state.py, paneId);
30835
+ const to = proj.pxToPoint(x, y, paneId);
30836
+ return { dt: to.time - from.time, dp: to.price - from.price };
30837
+ }
30838
+ /** The pressed drawing and its riders, each with its anchors at the press. */
30839
+ dragSources(d) {
30840
+ if (this.state.kind !== "pressed") return [];
30841
+ const out = [{ d, orig: this.state.orig }];
30842
+ for (const r of this.state.riders) {
30843
+ const rd = this.byId(r.id);
30844
+ if (rd) out.push({ d: rd, orig: r.orig });
30845
+ }
30846
+ return out;
30847
+ }
30848
+ /** Move the pressed drawing's body (and its riders) — or, on a Ctrl-drag, their copies — to the
30849
+ * cursor. The copies are built on the first move (so a Ctrl-click never creates anything), from
30850
+ * sources that have not moved yet, so their own anchors are the origin to translate from. */
30851
+ dragBody(d, x, y) {
30852
+ if (this.state.kind !== "pressed") return;
30853
+ this.snapAt = null;
30854
+ if (this.state.mod && !this.state.clones) {
30855
+ this.state.clones = this.dragSources(d).flatMap((s) => {
30856
+ const c = deserializeDrawing(s.d.serialize());
30857
+ return c ? [{ d: c, orig: cloneAnchors(c) }] : [];
30858
+ });
30859
+ }
30860
+ for (const t of this.state.clones ?? this.dragSources(d)) {
30861
+ const { dt, dp } = this.bodyDelta(t.d.paneId, x, y);
30862
+ t.d.anchors = t.d.translateBody(dt, dp, t.orig);
30761
30863
  }
30762
30864
  }
30763
30865
  /** Pointer move: advance the placing ghost, or live-preview a drag once past the slop.
@@ -30781,12 +30883,20 @@ ${overlayScrollbarCss(".vela-sd-pane")}
30781
30883
  this.deps.changed();
30782
30884
  return;
30783
30885
  }
30886
+ if (this.state.kind === "marquee") {
30887
+ this.state.x1 = x;
30888
+ this.state.y1 = y;
30889
+ this.deps.changed();
30890
+ return;
30891
+ }
30784
30892
  if (this.state.kind !== "pressed") return;
30785
30893
  const d = this.byId(this.state.id);
30786
30894
  if (!d || d.locked) return;
30787
30895
  if (!this.state.moved && Math.abs(x - this.state.px) <= DRAG_SLOP2 && Math.abs(y - this.state.py) <= DRAG_SLOP2) return;
30788
30896
  this.state.moved = true;
30789
- if (this.state.handle >= 0) {
30897
+ if (this.state.handle < 0) {
30898
+ this.dragBody(d, x, y);
30899
+ } else {
30790
30900
  const a = d.anchors[this.state.handle];
30791
30901
  if (a) {
30792
30902
  const pivot = shift4 && ANGLE_SNAP_TYPES.has(d.type) && d.anchors.length === 2 ? d.anchors[1 - this.state.handle] : void 0;
@@ -30794,12 +30904,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
30794
30904
  applyFree(a, point, d.anchorSchema().slots[this.state.handle]?.free ?? "both");
30795
30905
  }
30796
30906
  d.constrainHandleDrag(this.state.handle);
30797
- } else {
30798
- const pt2 = this.deps.projector().pxToPoint(x, y, d.paneId);
30799
- this.snapAt = null;
30800
- const dt = pt2.time - this.state.grab.time;
30801
- const dp = pt2.price - this.state.grab.price;
30802
- d.anchors = d.translateBody(dt, dp, this.state.orig);
30803
30907
  }
30804
30908
  this.deps.changed();
30805
30909
  }
@@ -30819,16 +30923,46 @@ ${overlayScrollbarCss(".vela-sd-pane")}
30819
30923
  }
30820
30924
  return;
30821
30925
  }
30926
+ if (this.state.kind === "marquee") {
30927
+ const rect = this.marqueeRect();
30928
+ this.state = { kind: "idle" };
30929
+ if (rect.w > DRAG_SLOP2 || rect.h > DRAG_SLOP2) this.selectWithin(rect);
30930
+ this.deps.changed();
30931
+ return;
30932
+ }
30822
30933
  if (this.state.kind !== "pressed") return;
30823
- const { id, moved } = this.state;
30934
+ const { id, moved, riders, mod, clones } = this.state;
30824
30935
  this.state = { kind: "idle" };
30825
30936
  const d = this.byId(id);
30826
- if (moved) {
30827
- if (d) this.deps.emit({ kind: "edit", doc: d.serialize() });
30828
- this.deps.changed();
30829
- } else {
30830
- this.deps.openSettings(id, x, y);
30937
+ if (!moved) {
30938
+ if (mod) this.deps.emit({ kind: "select", ids: [id], additive: true });
30939
+ else this.deps.openSettings(id, x, y);
30940
+ return;
30941
+ }
30942
+ if (clones) {
30943
+ this.deps.emit({ kind: "clone", docs: clones.map((c) => c.d.serialize()) });
30944
+ } else if (d) {
30945
+ const docs = [d.serialize()];
30946
+ for (const r of riders) {
30947
+ const rd = this.byId(r.id);
30948
+ if (rd) docs.push(rd.serialize());
30949
+ }
30950
+ if (docs.length > 1) this.deps.emit({ kind: "edit-many", docs });
30951
+ else this.deps.emit({ kind: "edit", doc: docs[0] });
30952
+ }
30953
+ this.deps.changed();
30954
+ }
30955
+ /** Add every visible drawing whose on-screen bounds touch `rect` to the selection (a union,
30956
+ * so successive sweeps accumulate). No change when the box touches nothing new. */
30957
+ selectWithin(rect) {
30958
+ const proj = this.deps.projector();
30959
+ const ids = [...this.deps.selectedIds()];
30960
+ for (const d of this.deps.drawings()) {
30961
+ if (!d.visible || ids.includes(d.id)) continue;
30962
+ const b = d.bounds(proj);
30963
+ if (b && rectsIntersect(rect, b)) ids.push(d.id);
30831
30964
  }
30965
+ if (ids.length > this.deps.selectedIds().size) this.deps.emit({ kind: "select", ids });
30832
30966
  }
30833
30967
  /** Cancel an in-progress placement or drag (Escape). Returns whether anything was cancelled. */
30834
30968
  cancel() {
@@ -30841,8 +30975,15 @@ ${overlayScrollbarCss(".vela-sd-pane")}
30841
30975
  return true;
30842
30976
  }
30843
30977
  if (this.state.kind === "pressed") {
30844
- const d = this.byId(this.state.id);
30845
- if (d && this.state.moved) d.anchors = this.state.orig.map((o) => ({ time: o.time, price: o.price }));
30978
+ if (this.state.moved && !this.state.clones) {
30979
+ const d = this.byId(this.state.id);
30980
+ if (d) for (const s of this.dragSources(d)) s.d.anchors = s.orig.map((o) => ({ time: o.time, price: o.price }));
30981
+ }
30982
+ this.state = { kind: "idle" };
30983
+ this.deps.changed();
30984
+ return true;
30985
+ }
30986
+ if (this.state.kind === "marquee") {
30846
30987
  this.state = { kind: "idle" };
30847
30988
  this.deps.changed();
30848
30989
  return true;
@@ -30957,6 +31098,9 @@ ${overlayScrollbarCss(".vela-sd-pane")}
30957
31098
  function cloneAnchors(d) {
30958
31099
  return d.anchors.map((a) => ({ time: a.time, price: a.price }));
30959
31100
  }
31101
+ function rectsIntersect(a, b) {
31102
+ return a.x <= b.x + b.w && b.x <= a.x + a.w && a.y <= b.y + b.h && b.y <= a.y + a.h;
31103
+ }
30960
31104
  function applyFree(anchor, pt2, free) {
30961
31105
  if (free === "both" || free === "x") anchor.time = pt2.time;
30962
31106
  if (free === "both" || free === "y") anchor.price = pt2.price;
@@ -31342,10 +31486,27 @@ ${overlayScrollbarCss(".vela-sd-pane")}
31342
31486
  }
31343
31487
 
31344
31488
  // src/renderers/native/drawings/DrawingSettingsPopup.ts
31489
+ var MIXED = /* @__PURE__ */ Symbol("mixed");
31490
+ function sharedPaths(drawings) {
31491
+ const [first2, ...rest] = drawings;
31492
+ const paths = new Set(first2?.schema().fields.map((f) => f.path) ?? []);
31493
+ for (const d of rest) {
31494
+ const own = new Set(d.schema().fields.map((f) => f.path));
31495
+ for (const p of paths) if (!own.has(p)) paths.delete(p);
31496
+ }
31497
+ return paths;
31498
+ }
31499
+ function commonValue(drawings, read) {
31500
+ const first2 = read(drawings[0]);
31501
+ return drawings.every((d) => read(d) === first2) ? first2 : MIXED;
31502
+ }
31503
+ function distinctValues(drawings, read) {
31504
+ return [...new Set(drawings.map(read))];
31505
+ }
31345
31506
  var BTN = 30;
31346
31507
  var ICON = 21;
31347
31508
  var STYLE_ID3 = "vela-drawing-popup-styles";
31348
- var STYLE_REV = "3";
31509
+ var STYLE_REV = "4";
31349
31510
  function ensureStyles3() {
31350
31511
  if (typeof document === "undefined") return;
31351
31512
  const existing = document.getElementById(STYLE_ID3);
@@ -31357,6 +31518,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
31357
31518
  .vela-dpop-btn{background:transparent;color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
31358
31519
  .vela-dpop-btn:hover{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
31359
31520
  .vela-dpop-btn[data-active='1']{background:var(--vela-active);color:var(--vela-fg-bright);}
31521
+ /* Mixed (a multi-selection disagrees): the active fill at half strength behind a dashed ring. */
31522
+ .vela-dpop-btn[data-active='mixed']{background:var(--vela-hover-strong);color:var(--vela-fg-bright);outline:1px dashed var(--vela-fg-muted);outline-offset:-2px;}
31360
31523
  .vela-dpop-item{background:transparent;transition:background var(--vela-dur-fast) ease;}
31361
31524
  .vela-dpop-item:hover{background:var(--vela-hover-strong);}
31362
31525
  .vela-dpop-item[data-active='1']{background:var(--vela-active);}
@@ -31388,7 +31551,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
31388
31551
  if (this.isOwnChrome(node)) return;
31389
31552
  const cb = this.onClose;
31390
31553
  this.close();
31391
- cb?.();
31554
+ cb?.(e);
31392
31555
  };
31393
31556
  this.theme = theme;
31394
31557
  this.settingsDialog = new DrawingSettingsDialog(host, theme);
@@ -31413,16 +31576,27 @@ ${overlayScrollbarCss(".vela-sd-pane")}
31413
31576
  contains(node) {
31414
31577
  return node != null && (this.isOwnChrome(node) || this.settingsDialog.contains(node));
31415
31578
  }
31416
- /** Open the quick toolbar for `drawing`, floating clear of its `anchor` box.
31417
- * `onClose` fires on an outside-click dismissal (not a programmatic close). */
31418
- open(drawing, anchor, actions, onClose) {
31579
+ /** Open the quick toolbar for `drawings` (one, or a whole multi-selection), floating clear of
31580
+ * their `anchor` box. With several drawings the bar shows only the controls they ALL support,
31581
+ * a control whose values differ reads as mixed, and every edit applies to all of them —
31582
+ * per-drawing panels (levels, position sizing, the text field) stay single-drawing only.
31583
+ * `onClose` fires on a dismissal (an outside press, or Escape in the text field — not a
31584
+ * programmatic close), with the dismissing press when there is one, so the caller can tell a
31585
+ * modifier press (multi-select) from a plain one. */
31586
+ open(drawings, anchor, actions, onClose) {
31419
31587
  this.close();
31588
+ const drawing = drawings[0];
31589
+ if (!drawing) return;
31420
31590
  ensureStyles3();
31421
31591
  this.onClose = onClose ?? null;
31422
31592
  const t = this.theme;
31593
+ const multi = drawings.length > 1;
31423
31594
  const schema = drawing.schema();
31424
- const paths = new Set(schema.fields.map((f) => f.path));
31425
- const textOnBar = schema.textIsContent === true || !paths.has("text.value");
31595
+ const paths = sharedPaths(drawings);
31596
+ const common = (read) => commonValue(drawings, read);
31597
+ const every = (pred) => drawings.every(pred);
31598
+ const swatch = (tip, glyph, read, path) => this.colorButton(tip, glyph, common(read), (v) => actions.patch({ [path]: v }), void 0, distinctValues(drawings, read));
31599
+ const textOnBar = multi || schema.textIsContent === true || !paths.has("text.value");
31426
31600
  const el = document.createElement("div");
31427
31601
  el.className = "vela-dpop";
31428
31602
  el.style.cssText = `position:absolute;z-index:22;background:${t.background};border:1px solid var(--vela-border);border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow);color:${t.textColor};font:var(--vela-font-size-md) ${t.fontFamily};display:flex;flex-direction:column;pointer-events:auto;overflow:hidden;`;
@@ -31436,75 +31610,84 @@ ${overlayScrollbarCss(".vela-sd-pane")}
31436
31610
  bar.style.cssText = "display:flex;align-items:center;gap:2px;padding:4px;";
31437
31611
  bar.appendChild(this.dragHandle());
31438
31612
  if (paths.has("glyph")) {
31439
- const cur = drawing.glyph ?? GLYPH_OPTIONS[0];
31613
+ const cur = common((d) => d.glyph ?? GLYPH_OPTIONS[0]);
31440
31614
  bar.appendChild(this.dropdown("Icon", GLYPH_OPTIONS, cur, (g) => glyphIcon(g), (v) => actions.patch({ glyph: v })));
31441
31615
  }
31442
31616
  if (paths.has("size")) {
31443
- const sz = drawing.size ?? "normal";
31617
+ const sz = common((d) => d.size ?? "normal");
31444
31618
  bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
31445
31619
  }
31446
- if (paths.has("magnifier.timeframe") && drawing instanceof Magnifier) {
31620
+ if (paths.has("magnifier.timeframe") && every((d) => d instanceof Magnifier)) {
31621
+ const mag = (d) => d;
31447
31622
  const options = this.lowerTimeframeOptions();
31448
31623
  if (options.length > 0) {
31449
31624
  bar.appendChild(
31450
- this.dropdown("Lower timeframe", options.map((o) => o.value), drawing.magnifier.timeframe, () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
31625
+ this.dropdown("Lower timeframe", options.map((o) => o.value), common((d) => mag(d).magnifier.timeframe), () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
31451
31626
  label: (v) => magnifierTimeframeLabel(String(v)),
31452
31627
  labelInTrigger: true
31453
31628
  })
31454
31629
  );
31455
31630
  }
31456
- bar.appendChild(this.colorButton("Up candles", BUCKET_ICON, drawing.magnifier.upColor || t.upColor, (v) => actions.patch({ "magnifier.upColor": v })));
31457
- bar.appendChild(this.colorButton("Down candles", BUCKET_ICON, drawing.magnifier.downColor || t.downColor, (v) => actions.patch({ "magnifier.downColor": v })));
31631
+ bar.appendChild(swatch("Up candles", BUCKET_ICON, (d) => mag(d).magnifier.upColor || t.upColor, "magnifier.upColor"));
31632
+ bar.appendChild(swatch("Down candles", BUCKET_ICON, (d) => mag(d).magnifier.downColor || t.downColor, "magnifier.downColor"));
31458
31633
  }
31459
- if (paths.has("style.lineColor")) bar.appendChild(this.colorButton("Line color", BRUSH_ICON, drawing.style.lineColor || (drawing instanceof Magnifier ? contrastColor(this.theme.background) : DEFAULT_DRAWING_COLOR), (v) => actions.patch({ "style.lineColor": v })));
31634
+ if (paths.has("style.lineColor")) bar.appendChild(swatch("Line color", BRUSH_ICON, (d) => d.style.lineColor || (d instanceof Magnifier ? contrastColor(this.theme.background) : DEFAULT_DRAWING_COLOR), "style.lineColor"));
31460
31635
  if (paths.has("style.lineWidth")) {
31461
- const wf = schema.fields.find((f) => f.path === "style.lineWidth");
31462
- if (wf?.kind === "number" && (wf.min ?? 1) > 1) {
31463
- bar.appendChild(this.widthInput("Line width", drawing.style.lineWidth, wf.min ?? 1, wf.max ?? 60, wf.step ?? 1, (v) => actions.patch({ "style.lineWidth": v })));
31636
+ const widthField2 = (d) => d.schema().fields.find((f) => f.path === "style.lineWidth");
31637
+ const wf = widthField2(drawing);
31638
+ const width = common((d) => d.style.lineWidth);
31639
+ const markers = every((d) => {
31640
+ const f = widthField2(d);
31641
+ return f?.kind === "number" && (f.min ?? 1) > 1;
31642
+ });
31643
+ if (markers && wf?.kind === "number") {
31644
+ bar.appendChild(this.widthInput("Line width", width, wf.min ?? 1, wf.max ?? 60, wf.step ?? 1, (v) => actions.patch({ "style.lineWidth": v })));
31464
31645
  } else {
31465
- bar.appendChild(this.dropdown("Line width", [1, 2, 3, 4], drawing.style.lineWidth, (w) => lineIcon(w, "solid"), (v) => actions.patch({ "style.lineWidth": v }), { label: (v) => `${v}px`, labelInTrigger: true }));
31646
+ bar.appendChild(this.dropdown("Line width", [1, 2, 3, 4], width, (w) => lineIcon(w, "solid"), (v) => actions.patch({ "style.lineWidth": v }), { label: (v) => `${v}px`, labelInTrigger: true }));
31466
31647
  }
31467
31648
  }
31468
- 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 }));
31469
- if (paths.has("style.fillColor")) bar.appendChild(this.colorButton("Fill", BUCKET_ICON, effectiveFillColor(drawing, this.theme) ?? drawing.style.fillColor ?? DEFAULT_DRAWING_COLOR, (v) => actions.patch({ "style.fillColor": v })));
31649
+ if (paths.has("style.lineStyle")) bar.appendChild(this.dropdown("Line style", LINE_STYLE_OPTIONS.map((o) => o.value), common((d) => d.style.lineStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "style.lineStyle": v }), { label: styleLabel2 }));
31650
+ if (paths.has("style.fillColor")) bar.appendChild(swatch("Fill", BUCKET_ICON, (d) => effectiveFillColor(d, this.theme) ?? d.style.fillColor ?? DEFAULT_DRAWING_COLOR, "style.fillColor"));
31470
31651
  const isFrvp = paths.has("frvp.rows") && drawing instanceof FixedRangeVolumeProfile;
31471
- const isPosition = paths.has("riskPercent") && drawing instanceof PositionTool;
31652
+ const isPosition = paths.has("riskPercent") && every((d) => d instanceof PositionTool);
31472
31653
  if (isPosition) {
31473
- const pos = drawing;
31474
- bar.appendChild(this.colorButton("Profit zone", BUCKET_ICON, pos.profitColor, (v) => actions.patch({ profitColor: v })));
31475
- bar.appendChild(this.colorButton("Loss zone", BUCKET_ICON, pos.lossColor, (v) => actions.patch({ lossColor: v })));
31654
+ const pos = (d) => d;
31655
+ bar.appendChild(swatch("Profit zone", BUCKET_ICON, (d) => pos(d).profitColor, "profitColor"));
31656
+ bar.appendChild(swatch("Loss zone", BUCKET_ICON, (d) => pos(d).lossColor, "lossColor"));
31476
31657
  }
31477
- const reg = drawing.reg;
31478
- if (paths.has("reg.midColor") && reg) {
31658
+ const regOf = (d) => d.reg;
31659
+ if (paths.has("reg.midColor") && every((d) => regOf(d) != null)) {
31660
+ const reg = (d) => regOf(d);
31479
31661
  const styles = LINE_STYLE_OPTIONS.map((o) => o.value);
31480
- bar.appendChild(this.colorButton("Midline color", BRUSH_ICON, reg.midColor, (v) => actions.patch({ "reg.midColor": v })));
31481
- bar.appendChild(this.dropdown("Midline style", styles, reg.midStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.midStyle": v }), { label: styleLabel2 }));
31482
- bar.appendChild(this.colorButton("Upper line color", BRUSH_ICON, reg.upperColor, (v) => actions.patch({ "reg.upperColor": v })));
31483
- bar.appendChild(this.dropdown("Upper line style", styles, reg.upperStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.upperStyle": v }), { label: styleLabel2 }));
31484
- bar.appendChild(this.colorButton("Lower line color", BRUSH_ICON, reg.lowerColor, (v) => actions.patch({ "reg.lowerColor": v })));
31485
- bar.appendChild(this.dropdown("Lower line style", styles, reg.lowerStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.lowerStyle": v }), { label: styleLabel2 }));
31486
- bar.appendChild(this.colorButton("Upper fill", BUCKET_ICON, reg.upperFill, (v) => actions.patch({ "reg.upperFill": v })));
31487
- bar.appendChild(this.colorButton("Lower fill", BUCKET_ICON, reg.lowerFill, (v) => actions.patch({ "reg.lowerFill": v })));
31488
- bar.appendChild(this.toggle("Show R\xB2", R2_ICON, reg.showR2, (v) => actions.patch({ "reg.showR2": v })));
31489
- }
31490
- const vwap2 = drawing.vwap;
31491
- if (paths.has("vwap.midColor") && vwap2) {
31662
+ bar.appendChild(swatch("Midline color", BRUSH_ICON, (d) => reg(d).midColor, "reg.midColor"));
31663
+ bar.appendChild(this.dropdown("Midline style", styles, common((d) => reg(d).midStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "reg.midStyle": v }), { label: styleLabel2 }));
31664
+ bar.appendChild(swatch("Upper line color", BRUSH_ICON, (d) => reg(d).upperColor, "reg.upperColor"));
31665
+ bar.appendChild(this.dropdown("Upper line style", styles, common((d) => reg(d).upperStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "reg.upperStyle": v }), { label: styleLabel2 }));
31666
+ bar.appendChild(swatch("Lower line color", BRUSH_ICON, (d) => reg(d).lowerColor, "reg.lowerColor"));
31667
+ bar.appendChild(this.dropdown("Lower line style", styles, common((d) => reg(d).lowerStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "reg.lowerStyle": v }), { label: styleLabel2 }));
31668
+ bar.appendChild(swatch("Upper fill", BUCKET_ICON, (d) => reg(d).upperFill, "reg.upperFill"));
31669
+ bar.appendChild(swatch("Lower fill", BUCKET_ICON, (d) => reg(d).lowerFill, "reg.lowerFill"));
31670
+ bar.appendChild(this.toggle("Show R\xB2", R2_ICON, common((d) => reg(d).showR2), (v) => actions.patch({ "reg.showR2": v })));
31671
+ }
31672
+ const vwapOf = (d) => d.vwap;
31673
+ if (paths.has("vwap.midColor") && every((d) => vwapOf(d) != null)) {
31674
+ const vwap2 = (d) => vwapOf(d);
31492
31675
  const styles = LINE_STYLE_OPTIONS.map((o) => o.value);
31493
31676
  const MULTS = [0.5, 1, 1.5, 2, 2.5, 3, 4, 5];
31494
- bar.appendChild(this.colorButton("VWAP color", BRUSH_ICON, vwap2.midColor, (v) => actions.patch({ "vwap.midColor": v })));
31495
- bar.appendChild(this.dropdown("VWAP style", styles, vwap2.midStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "vwap.midStyle": v }), { label: styleLabel2 }));
31677
+ bar.appendChild(swatch("VWAP color", BRUSH_ICON, (d) => vwap2(d).midColor, "vwap.midColor"));
31678
+ bar.appendChild(this.dropdown("VWAP style", styles, common((d) => vwap2(d).midStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "vwap.midStyle": v }), { label: styleLabel2 }));
31496
31679
  bar.appendChild(
31497
- this.dropdown("Band multiplier", MULTS, vwap2.multiplier, () => BANDS_ICON, (v) => actions.patch({ "vwap.multiplier": v }), {
31680
+ this.dropdown("Band multiplier", MULTS, common((d) => vwap2(d).multiplier), () => BANDS_ICON, (v) => actions.patch({ "vwap.multiplier": v }), {
31498
31681
  label: (v) => `${v}\u03C3`,
31499
31682
  labelInTrigger: true
31500
31683
  })
31501
31684
  );
31502
- bar.appendChild(this.colorButton("Upper band color", BRUSH_ICON, vwap2.upperColor, (v) => actions.patch({ "vwap.upperColor": v })));
31503
- bar.appendChild(this.colorButton("Lower band color", BRUSH_ICON, vwap2.lowerColor, (v) => actions.patch({ "vwap.lowerColor": v })));
31504
- bar.appendChild(this.colorButton("Band fill", BUCKET_ICON, vwap2.bandFill, (v) => actions.patch({ "vwap.bandFill": v })));
31685
+ bar.appendChild(swatch("Upper band color", BRUSH_ICON, (d) => vwap2(d).upperColor, "vwap.upperColor"));
31686
+ bar.appendChild(swatch("Lower band color", BRUSH_ICON, (d) => vwap2(d).lowerColor, "vwap.lowerColor"));
31687
+ bar.appendChild(swatch("Band fill", BUCKET_ICON, (d) => vwap2(d).bandFill, "vwap.bandFill"));
31505
31688
  }
31506
31689
  if (paths.has("maxCurvature")) {
31507
- const cur = drawing.maxCurvature ?? 24;
31690
+ const cur = common((d) => d.maxCurvature ?? 24);
31508
31691
  bar.appendChild(
31509
31692
  this.dropdown("Max curvature", DEDEKIND_CURVATURE_OPTIONS, cur, () => DEDEKIND_ICON2, (v) => actions.patch({ maxCurvature: v }), {
31510
31693
  label: (v) => `n=${v}`,
@@ -31513,7 +31696,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
31513
31696
  );
31514
31697
  }
31515
31698
  if (paths.has("mach")) {
31516
- const cur = drawing.mach ?? 2;
31699
+ const cur = common((d) => d.mach ?? 2);
31517
31700
  bar.appendChild(
31518
31701
  this.dropdown("Mach number", MACH_NUMBER_OPTIONS, cur, () => SUPERSONIC_ICON2, (v) => actions.patch({ mach: v }), {
31519
31702
  label: (v) => `M=${v}`,
@@ -31522,7 +31705,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
31522
31705
  );
31523
31706
  }
31524
31707
  if (paths.has("waveCount")) {
31525
- const cur = drawing.waveCount ?? 6;
31708
+ const cur = common((d) => d.waveCount ?? 6);
31526
31709
  bar.appendChild(
31527
31710
  this.dropdown("Waves", MACH_WAVE_COUNT_OPTIONS, cur, () => SONIC_ICON2, (v) => actions.patch({ waveCount: v }), {
31528
31711
  label: (v) => `${v}`,
@@ -31530,26 +31713,27 @@ ${overlayScrollbarCss(".vela-sd-pane")}
31530
31713
  })
31531
31714
  );
31532
31715
  }
31533
- const toggles = drawing;
31534
- if (paths.has("showPrice")) bar.appendChild(this.toggle("Show price", PRICE_DELTA_ICON, toggles.showPrice !== false, (v) => actions.patch({ showPrice: v })));
31535
- if (paths.has("showDate")) bar.appendChild(this.toggle("Show date", DATE_DELTA_ICON, toggles.showDate !== false, (v) => actions.patch({ showDate: v })));
31536
- 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 })));
31537
- 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 }));
31538
- 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 })));
31539
- 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 })));
31540
- if (paths.has("text.value")) bar.appendChild(this.iconBtn("Text", TYPE_ICON, () => this.toggleTextPanel(drawing, actions, !textOnBar)));
31716
+ const flags = (d) => d;
31717
+ if (paths.has("showPrice")) bar.appendChild(this.toggle("Show price", PRICE_DELTA_ICON, common((d) => flags(d).showPrice !== false), (v) => actions.patch({ showPrice: v })));
31718
+ if (paths.has("showDate")) bar.appendChild(this.toggle("Show date", DATE_DELTA_ICON, common((d) => flags(d).showDate !== false), (v) => actions.patch({ showDate: v })));
31719
+ if (paths.has("text.color") && textOnBar) bar.appendChild(swatch("Text color", TYPE_ICON, (d) => d.text?.color || t.textColor, "text.color"));
31720
+ if (paths.has("text.size") && textOnBar) bar.appendChild(this.dropdown("Text size", TEXT_SIZE_OPTIONS.map((o) => o.value), common((d) => d.text?.size ?? "normal"), (s) => labelSizeIcon(s), (v) => actions.patch({ "text.size": v }), { label: sizeLabel }));
31721
+ const textField = !multi && paths.has("text.value");
31722
+ if (paths.has("text.bold") && !textField) bar.appendChild(this.toggle("Bold", BOLD_ICON, common((d) => !!d.text?.bold), (v) => actions.patch({ "text.bold": v })));
31723
+ if (paths.has("text.italic") && !textField) bar.appendChild(this.toggle("Italic", ITALIC_ICON, common((d) => !!d.text?.italic), (v) => actions.patch({ "text.italic": v })));
31724
+ if (textField) bar.appendChild(this.iconBtn("Text", TYPE_ICON, () => this.toggleTextPanel(drawing, actions, !textOnBar)));
31541
31725
  const editableLevels = drawing.editableLevels();
31542
- if (editableLevels && !(drawing instanceof MachFigure)) {
31543
- const fib = drawing;
31726
+ if (editableLevels && every((d) => d.editableLevels() != null && !(d instanceof MachFigure))) {
31727
+ const fib = (d) => d;
31544
31728
  const sizes = TEXT_SIZE_OPTIONS.map((o) => o.value);
31545
- bar.appendChild(this.dropdown("Numbers size", sizes, fib.numbersSize ?? "small", (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel }));
31546
- bar.appendChild(this.dropdown("Label size", sizes, fib.labelsSize ?? "normal", (s) => labelSizeIcon(s), (v) => actions.patch({ labelsSize: v }), { label: sizeLabel }));
31729
+ bar.appendChild(this.dropdown("Numbers size", sizes, common((d) => fib(d).numbersSize ?? "small"), (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel }));
31730
+ bar.appendChild(this.dropdown("Label size", sizes, common((d) => fib(d).labelsSize ?? "normal"), (s) => labelSizeIcon(s), (v) => actions.patch({ labelsSize: v }), { label: sizeLabel }));
31547
31731
  }
31548
31732
  bar.appendChild(this.divider());
31549
- if (isFrvp) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "frvp")));
31550
- if (isPosition) bar.appendChild(this.iconBtn("Position size", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "position")));
31551
- if (editableLevels) bar.appendChild(this.iconBtn("Levels", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "levels")));
31552
- bar.appendChild(this.toggle("Lock", LOCK_ICON, drawing.locked, (v) => actions.setLocked(v)));
31733
+ if (!multi && isFrvp) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "frvp")));
31734
+ if (!multi && isPosition) bar.appendChild(this.iconBtn("Position size", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "position")));
31735
+ if (!multi && editableLevels) bar.appendChild(this.iconBtn("Levels", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "levels")));
31736
+ bar.appendChild(this.toggle("Lock", LOCK_ICON, common((d) => d.locked), (v) => actions.setLocked(v), UNLOCK_ICON));
31553
31737
  const del = this.iconBtn("Delete", TRASH_ICON, () => actions.remove());
31554
31738
  del.style.color = "var(--vela-danger)";
31555
31739
  bar.appendChild(del);
@@ -31735,7 +31919,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
31735
31919
  });
31736
31920
  return b;
31737
31921
  }
31738
- /** The trailing kebab overflow — z-order actions + reset settings. */
31922
+ /** The trailing kebab overflow — duplicate, z-order actions, reset settings. */
31739
31923
  kebabButton(actions) {
31740
31924
  const b = this.base("More");
31741
31925
  b.innerHTML = sized(KEBAB_ICON);
@@ -31747,6 +31931,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
31747
31931
  return;
31748
31932
  }
31749
31933
  this.openActionMenu(b, [
31934
+ { icon: CLONE_ICON, label: "Duplicate", onClick: () => actions.duplicate() },
31750
31935
  { icon: FRONT_ICON, label: "Bring to front", onClick: () => actions.reorder("front") },
31751
31936
  { icon: BACK_ICON, label: "Send to back", onClick: () => actions.reorder("back") },
31752
31937
  { icon: RESET_ICON, label: "Reset settings", onClick: () => actions.resetSettings() }
@@ -31903,26 +32088,30 @@ ${overlayScrollbarCss(".vela-sd-pane")}
31903
32088
  this.tipEl?.remove();
31904
32089
  this.tipEl = null;
31905
32090
  }
31906
- toggle(tip, icon2, active, onChange) {
32091
+ /** An on/off button. A mixed state (the selected drawings disagree) reads as half-lit and
32092
+ * the first click turns it ON for all of them. `off` swaps in a distinct glyph while the
32093
+ * toggle is off (a lock reads as an open padlock until it is engaged); mixed shows `icon`. */
32094
+ toggle(tip, icon2, active, onChange, off) {
31907
32095
  const b = this.base(tip);
31908
- b.innerHTML = sized(icon2);
31909
32096
  const set = (on2) => {
31910
- b.dataset.active = on2 ? "1" : "0";
32097
+ b.dataset.active = on2 === MIXED ? "mixed" : on2 ? "1" : "0";
32098
+ b.innerHTML = sized(off && on2 === false ? off : icon2);
31911
32099
  };
31912
32100
  set(active);
31913
32101
  let on = active;
31914
32102
  b.addEventListener("click", () => {
31915
- on = !on;
32103
+ on = on === MIXED ? true : !on;
31916
32104
  set(on);
31917
32105
  onChange(on);
31918
32106
  });
31919
32107
  return b;
31920
32108
  }
31921
32109
  /** An inline numeric width field for tools whose stroke range outgrows the 1–4px
31922
- * ladder — the value is clamped to the schema's declared min/max on commit. */
32110
+ * ladder — the value is clamped to the schema's declared min/max on commit. A mixed
32111
+ * value shows an empty field with a dash until a number is typed for all. */
31923
32112
  widthInput(tip, value, min2, max2, step, onChange) {
31924
32113
  const ni = new NumberInput({
31925
- value,
32114
+ value: value === MIXED ? min2 : value,
31926
32115
  min: min2,
31927
32116
  max: max2,
31928
32117
  step,
@@ -31934,13 +32123,18 @@ ${overlayScrollbarCss(".vela-sd-pane")}
31934
32123
  title: tip,
31935
32124
  onChange
31936
32125
  });
32126
+ if (value === MIXED) {
32127
+ ni.input.value = "";
32128
+ ni.input.placeholder = "\u2014";
32129
+ }
31937
32130
  ni.el.dataset.tip = tip;
31938
32131
  trapChartKeys(ni.el);
31939
32132
  return ni.el;
31940
32133
  }
31941
32134
  /** A pick-one dropdown: the trigger shows the current value's glyph (plus an optional inline
31942
32135
  * text label — e.g. `2px` — for controls that would otherwise look alike), and clicking it
31943
- * opens a floating list of every option so a value is one click away (no cycling through). */
32136
+ * opens a floating list of every option so a value is one click away (no cycling through).
32137
+ * A mixed value shows a dash in the trigger and highlights no row. */
31944
32138
  dropdown(tip, values, current, render, onChange, opts = {}) {
31945
32139
  const b = this.base(tip);
31946
32140
  b.style.width = "auto";
@@ -31950,14 +32144,20 @@ ${overlayScrollbarCss(".vela-sd-pane")}
31950
32144
  let cur = current;
31951
32145
  const paint = (v) => {
31952
32146
  b.replaceChildren();
31953
- const glyph = render(v);
32147
+ if (v === MIXED) {
32148
+ const dash = document.createElement("span");
32149
+ dash.textContent = "\u2014";
32150
+ dash.style.cssText = "min-width:18px;text-align:center;opacity:0.85;";
32151
+ b.appendChild(dash);
32152
+ }
32153
+ const glyph = v === MIXED ? "" : render(v);
31954
32154
  if (glyph) {
31955
32155
  const ic = document.createElement("span");
31956
32156
  ic.style.cssText = "display:flex;";
31957
32157
  ic.innerHTML = sized(glyph);
31958
32158
  b.appendChild(ic);
31959
32159
  }
31960
- if (opts.label && opts.labelInTrigger) {
32160
+ if (opts.label && opts.labelInTrigger && v !== MIXED) {
31961
32161
  const tx = document.createElement("span");
31962
32162
  tx.textContent = opts.label(v);
31963
32163
  tx.style.cssText = "font-size:11px;opacity:0.85;font-variant-numeric:tabular-nums;";
@@ -32027,8 +32227,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
32027
32227
  }
32028
32228
  /** A color control: an `icon` over a thin **colored underline** showing the current
32029
32229
  * value (the common idiom), with an invisible native picker overlaid to edit it.
32030
- * `iconSize` shrinks it for the floating text controls. */
32031
- colorButton(tip, icon2, color, onChange, iconSize = 17) {
32230
+ * `iconSize` shrinks it for the floating text controls. A mixed color paints the
32231
+ * underline striped with the colors `used` across the selection, and the picker then
32232
+ * leads with those colors so unifying onto one of them is a single click. */
32233
+ colorButton(tip, icon2, color, onChange, iconSize = 17, used = []) {
32032
32234
  const b = document.createElement("button");
32033
32235
  b.type = "button";
32034
32236
  b.dataset.tip = tip;
@@ -32038,13 +32240,14 @@ ${overlayScrollbarCss(".vela-sd-pane")}
32038
32240
  ic.style.cssText = "display:flex;";
32039
32241
  ic.innerHTML = sized(icon2, iconSize);
32040
32242
  const bar = document.createElement("span");
32041
- bar.style.cssText = `display:block;height:3px;width:${Math.round(iconSize * 0.85)}px;border-radius:2px;background:${color};`;
32243
+ bar.style.cssText = `display:block;height:3px;width:${Math.round(iconSize * 0.85)}px;border-radius:2px;background:${color === MIXED ? stripes(used) : color};`;
32042
32244
  b.append(ic, bar);
32043
32245
  let cur = color;
32044
32246
  b.addEventListener("pointerdown", (e) => e.stopPropagation());
32045
32247
  b.addEventListener("click", (e) => {
32046
32248
  e.stopPropagation();
32047
- this.toggleColorPopover(b, cur, (v) => {
32249
+ const seed = cur === MIXED ? used[0] ?? DEFAULT_DRAWING_COLOR : cur;
32250
+ this.toggleColorPopover(b, seed, cur === MIXED ? used : [], (v) => {
32048
32251
  cur = v;
32049
32252
  bar.style.background = v;
32050
32253
  onChange(v);
@@ -32053,20 +32256,42 @@ ${overlayScrollbarCss(".vela-sd-pane")}
32053
32256
  return b;
32054
32257
  }
32055
32258
  /** Open the color popover for `anchor`, or close it if it's already this anchor's (toggle). */
32056
- toggleColorPopover(anchor, color, onChange) {
32259
+ toggleColorPopover(anchor, color, used, onChange) {
32057
32260
  if (this.colorOwner === anchor) {
32058
32261
  this.closeColorPopover();
32059
32262
  return;
32060
32263
  }
32061
- this.openColorPopover(anchor, color, onChange);
32264
+ this.openColorPopover(anchor, color, used, onChange);
32062
32265
  }
32063
- /** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`. */
32064
- openColorPopover(anchor, color, onChange) {
32266
+ /** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`.
32267
+ * `used` (a mixed swatch's colors) leads the picker as one-click unify shortcuts. */
32268
+ openColorPopover(anchor, color, used, onChange) {
32065
32269
  const t = this.theme;
32066
32270
  this.colorPop = this.hostFloat(anchor, {
32067
32271
  zIndex: 25,
32068
32272
  padding: "10px",
32069
- fill: (el) => {
32273
+ fill: (el, pop) => {
32274
+ if (used.length > 1) {
32275
+ const row = document.createElement("div");
32276
+ row.style.cssText = "display:flex;align-items:center;gap:6px;padding-bottom:9px;border-bottom:1px solid var(--vela-border);";
32277
+ const lbl = document.createElement("span");
32278
+ lbl.textContent = "In use";
32279
+ lbl.style.cssText = "font:var(--vela-font-size-sm) inherit;opacity:0.7;margin-right:2px;";
32280
+ row.appendChild(lbl);
32281
+ for (const c of used) {
32282
+ const sw = document.createElement("button");
32283
+ sw.type = "button";
32284
+ sw.dataset.tip = c;
32285
+ sw.style.cssText = `width:18px;height:18px;border-radius:4px;border:1px solid var(--vela-border-strong);cursor:pointer;background:${c};padding:0;`;
32286
+ sw.addEventListener("click", (e) => {
32287
+ e.stopPropagation();
32288
+ pop.hide();
32289
+ onChange(c);
32290
+ });
32291
+ row.appendChild(sw);
32292
+ }
32293
+ el.appendChild(row);
32294
+ }
32070
32295
  el.appendChild(buildColorPicker(color, t, onChange));
32071
32296
  }
32072
32297
  });
@@ -32087,6 +32312,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
32087
32312
  var PRICE_DELTA_ICON = icon("price-delta");
32088
32313
  var DATE_DELTA_ICON = icon("date-delta");
32089
32314
  var LOCK_ICON = icon("lock");
32315
+ var UNLOCK_ICON = icon("unlock");
32090
32316
  var FRONT_ICON = icon("bring-front");
32091
32317
  var BACK_ICON = icon("send-back");
32092
32318
  var TRASH_ICON = icon("trash");
@@ -32094,6 +32320,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
32094
32320
  var ITALIC_ICON = icon("italic");
32095
32321
  var GRIP_ICON = icon("grip");
32096
32322
  var KEBAB_ICON = icon("kebab");
32323
+ var CLONE_ICON = icon("clone");
32097
32324
  var RESET_ICON = icon("reset");
32098
32325
  var GEAR_ICON = icon("gear");
32099
32326
  var CHEVRON_ICON = icon("chevron-down");
@@ -32102,6 +32329,12 @@ ${overlayScrollbarCss(".vela-sd-pane")}
32102
32329
  var DEDEKIND_ICON2 = icon("dedekind");
32103
32330
  var SONIC_ICON2 = icon("sonic");
32104
32331
  var SUPERSONIC_ICON2 = icon("supersonic");
32332
+ function stripes(colors) {
32333
+ const bands = colors.length > 1 ? colors : ["var(--vela-fg-faint)", "var(--vela-fg-muted)"];
32334
+ const step = 100 / bands.length;
32335
+ const stops = bands.map((c, i) => `${c} ${i * step}% ${(i + 1) * step}%`).join(",");
32336
+ return `linear-gradient(90deg,${stops})`;
32337
+ }
32105
32338
  function lineIcon(width, style) {
32106
32339
  const dash = style === "dashed" ? "5,3" : style === "dotted" ? "1.5,3" : "";
32107
32340
  return svg24(`<line x1="3.5" y1="12" x2="20.5" y2="12" stroke-width="${Number(width) || 2}" stroke-dasharray="${dash}"/>`);
@@ -33017,6 +33250,20 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33017
33250
  // reported exactly when it actually changes).
33018
33251
  this.textEditor = null;
33019
33252
  this.painter = new DrawingPainter();
33253
+ /** The open popup is the multi-selection bar (vs one drawing's own) — decides what a
33254
+ * shrinking selection does with it. */
33255
+ this.popupMulti = false;
33256
+ /** Dismiss-on-outside-press also clears the selection — except when the press is ABOUT the
33257
+ * selection: it carries a multi-select modifier (Shift / Ctrl / Cmd — it will add to the
33258
+ * selection or sweep a marquee onto it), or it landed on a drawing that is already selected
33259
+ * (the canvas handler ran first and is holding it for a drag or a click), so the drawings
33260
+ * whose bar just closed must stay selected. */
33261
+ this.onPopupDismissed = (e) => {
33262
+ if (e && (e.shiftKey || e.ctrlKey || e.metaKey)) return;
33263
+ const pressed = this.interaction.pressedId();
33264
+ if (pressed && this.selectedIds.has(pressed)) return;
33265
+ this.clearSelection();
33266
+ };
33020
33267
  this.ctx = canvas.getContext("2d");
33021
33268
  this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), () => deps.chartBarMs());
33022
33269
  this.toolbar = new DrawingToolbar(
@@ -33193,6 +33440,12 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33193
33440
  }
33194
33441
  setSelection(ids) {
33195
33442
  this.selectedIds = new Set(ids);
33443
+ if (ids.length >= 2) this.openSettingsForSelection(ids);
33444
+ else if (this.popupMulti) {
33445
+ this.popupMulti = false;
33446
+ this.popup.close();
33447
+ if (ids.length === 1) this.openSettingsById(ids[0], 0, 0);
33448
+ }
33196
33449
  this.render();
33197
33450
  }
33198
33451
  /** Programmatic twin of clicking the drawing: highlight it + float its settings popup. */
@@ -33238,12 +33491,16 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33238
33491
  });
33239
33492
  }
33240
33493
  /** Delete the (unlocked) drawing under the cursor. True when one was removed.
33241
- * Shared by the eraser (click + drag) and the middle-click shortcut. */
33242
- deleteAt(x, y) {
33494
+ * Shared by the eraser (click + drag) and the middle-click shortcut; the latter passes
33495
+ * `withSelection` so a hit on a member of a multi-selection removes the selection's
33496
+ * unlocked members — whichever member was hit, locked or not. */
33497
+ deleteAt(x, y, withSelection = false) {
33243
33498
  const hit = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE);
33244
- if (!hit || hit.locked) return false;
33499
+ if (!hit) return false;
33500
+ const ids = deleteTargets(hit, this.selectedIds, this.drawings, withSelection);
33501
+ if (ids.length === 0) return false;
33245
33502
  this.popup.close();
33246
- this.emit({ kind: "delete", ids: [hit.id] });
33503
+ this.emit({ kind: "delete", ids });
33247
33504
  return true;
33248
33505
  }
33249
33506
  /** Shift+press on the empty plot: arm the measure ruler AND start it at (x, y) in one
@@ -33269,7 +33526,14 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33269
33526
  this.render();
33270
33527
  }
33271
33528
  }
33272
- pointerDown(x, y, snap = "off", shift4 = false) {
33529
+ /** Ctrl/Cmd+press on the empty plot: sweep a selection box (adds the drawings it touches to
33530
+ * the selection). Returns false when a mode/tool is active (the normal press path owns it). */
33531
+ beginMarqueeAt(x, y) {
33532
+ if (this.measureMode || this.eraserMode || !this.interaction.beginMarquee(x, y)) return false;
33533
+ this.render();
33534
+ return true;
33535
+ }
33536
+ pointerDown(x, y, snap = "off", shift4 = false, mod = false) {
33273
33537
  if (this.eraserMode) {
33274
33538
  this.erasing = true;
33275
33539
  this.deleteAt(x, y);
@@ -33289,9 +33553,9 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33289
33553
  return;
33290
33554
  }
33291
33555
  }
33292
- this.interaction.down(x, y, snap, shift4);
33556
+ this.interaction.down(x, y, snap, shift4, mod);
33293
33557
  }
33294
- pointerMove(x, y, snap = "off", shift4 = false) {
33558
+ pointerMove(x, y, snap = "off", shift4 = false, mod = false) {
33295
33559
  if (this.eraserMode) {
33296
33560
  if (this.erasing) this.deleteAt(x, y);
33297
33561
  return;
@@ -33303,12 +33567,14 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33303
33567
  return;
33304
33568
  }
33305
33569
  this.interaction.move(x, y, snap, shift4);
33306
- this.updateHover(x, y);
33570
+ this.updateHover(x, y, mod);
33307
33571
  }
33308
- /** Track which drawing is hovered so its handles appear on hover (and only then). */
33309
- updateHover(x, y) {
33572
+ /** Track which drawing is hovered so its handles appear on hover (and only then). While
33573
+ * Ctrl/Cmd is held the cursor is picking a selection — handles then mark only what IS
33574
+ * selected, so a hovered candidate doesn't already read as selected. */
33575
+ updateHover(x, y, mod = false) {
33310
33576
  let id = null;
33311
- if (this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
33577
+ if (!mod && this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
33312
33578
  id = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE)?.id ?? null;
33313
33579
  }
33314
33580
  if (id !== this.hoveredId) {
@@ -33328,7 +33594,12 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33328
33594
  this.render();
33329
33595
  return;
33330
33596
  }
33597
+ const dragged = this.interaction.isDragging() ? this.interaction.pressedId() : null;
33331
33598
  this.interaction.up(x, y);
33599
+ if (dragged && this.selectedIds.has(dragged) && !this.popup.isOpen()) {
33600
+ if (this.selectedIds.size >= 2) this.openSettingsForSelection(this.selectionIds());
33601
+ else this.openSettingsById(dragged, x, y);
33602
+ }
33332
33603
  }
33333
33604
  /** Toggle the transient ruler. Arming it clears any drawing tool + selection. */
33334
33605
  toggleMeasure() {
@@ -33593,7 +33864,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33593
33864
  this.emit({ kind: "duplicate", ids: this.selectionIds() });
33594
33865
  break;
33595
33866
  case "delete": {
33596
- const ids = this.selectedIds.size ? this.selectionIds() : this.hoveredId ? [this.hoveredId] : [];
33867
+ const ids = this.selectedIds.size ? this.deletableSelection() : this.hoveredId ? [this.hoveredId] : [];
33597
33868
  if (ids.length) {
33598
33869
  this.popup.close();
33599
33870
  this.emit({ kind: "delete", ids });
@@ -33609,6 +33880,9 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33609
33880
  selectionIds() {
33610
33881
  return [...this.selectedIds];
33611
33882
  }
33883
+ deletableSelection() {
33884
+ return deletableSelection(this.selectedIds, this.drawings);
33885
+ }
33612
33886
  /** Move every selected (unlocked) drawing by a pixel delta — one edit/edit-many → one undo step. */
33613
33887
  nudgeSelection(dx, dy) {
33614
33888
  const proj = this.deps.projector();
@@ -33717,6 +33991,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33717
33991
  this.painter.seriesLook = this.deps.seriesLook();
33718
33992
  this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
33719
33993
  this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
33994
+ const clones = this.interaction.dragClones();
33995
+ if (clones) this.painter.paintAll(ctx, clones, proj, this.deps.theme(), { selected: new Set(clones.map((c) => c.id)) });
33720
33996
  this.layoutTextEditor();
33721
33997
  const ghost = this.interaction.ghost();
33722
33998
  if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
@@ -33732,6 +34008,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33732
34008
  const my = m ? proj.yOf(m.point.price, m.paneId) : null;
33733
34009
  if (m && my != null) this.painter.paintSnapRing(ctx, proj.xOf(m.point.time), my, this.deps.theme());
33734
34010
  if (this.measure.isActive()) this.measure.paint(ctx, proj, this.deps.theme());
34011
+ const marquee = this.interaction.marqueeRect();
34012
+ if (marquee) this.painter.paintMarquee(ctx, marquee);
33735
34013
  }
33736
34014
  destroy() {
33737
34015
  this.closeTextEditor();
@@ -33749,15 +34027,80 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33749
34027
  if (this.selectedIds.size) this.emit({ kind: "select", ids: [] });
33750
34028
  this.render();
33751
34029
  }
34030
+ /** Report the live drawings' current state as one edit (one undo step, however many). */
34031
+ emitEdits(drawings) {
34032
+ const docs = drawings.map((d) => d.serialize());
34033
+ if (docs.length === 1) this.emit({ kind: "edit", doc: docs[0] });
34034
+ else if (docs.length > 1) this.emit({ kind: "edit-many", docs });
34035
+ }
34036
+ /** The one bar for a multi-selection: it shows the controls all of its drawings share and
34037
+ * every action applies to all of them. Floats clear of their combined bounds. */
34038
+ openSettingsForSelection(ids) {
34039
+ const live = () => ids.map((id) => this.drawings.find((d) => d.id === id)).filter((d) => d != null);
34040
+ const drawings = live();
34041
+ if (drawings.length < 2) return;
34042
+ const proj = this.deps.projector();
34043
+ let anchor = null;
34044
+ for (const d of drawings) {
34045
+ const b = d.bounds(proj);
34046
+ if (!b) continue;
34047
+ if (!anchor) anchor = { ...b };
34048
+ else {
34049
+ const right = Math.max(anchor.x + anchor.w, b.x + b.w);
34050
+ const bottom = Math.max(anchor.y + anchor.h, b.y + b.h);
34051
+ anchor.x = Math.min(anchor.x, b.x);
34052
+ anchor.y = Math.min(anchor.y, b.y);
34053
+ anchor.w = right - anchor.x;
34054
+ anchor.h = bottom - anchor.y;
34055
+ }
34056
+ }
34057
+ this.popupMulti = true;
34058
+ this.emit({ kind: "settings", id: drawings[0].id });
34059
+ this.popup.open(drawings, anchor, {
34060
+ resolve: () => live()[0] ?? null,
34061
+ patch: (p) => {
34062
+ const ds = live();
34063
+ for (const d of ds) d.applySettings(p);
34064
+ this.render();
34065
+ this.emitEdits(ds);
34066
+ },
34067
+ setLocked: (v) => {
34068
+ const ds = live();
34069
+ for (const d of ds) d.locked = v;
34070
+ this.emitEdits(ds);
34071
+ },
34072
+ reorder: (to) => {
34073
+ for (const d of live()) this.emit({ kind: "reorder", id: d.id, to });
34074
+ },
34075
+ duplicate: () => {
34076
+ this.popup.close();
34077
+ this.emit({ kind: "duplicate", ids: live().map((d) => d.id) });
34078
+ },
34079
+ resetSettings: () => {
34080
+ const ds = live();
34081
+ for (const d of ds) resetDrawingSettings(d);
34082
+ this.render();
34083
+ this.emitEdits(ds);
34084
+ this.openSettingsForSelection(ids);
34085
+ },
34086
+ remove: () => {
34087
+ const ids2 = live().filter((d) => !d.locked).map((d) => d.id);
34088
+ if (ids2.length === 0) return;
34089
+ this.popup.close();
34090
+ this.emit({ kind: "delete", ids: ids2 });
34091
+ }
34092
+ }, this.onPopupDismissed);
34093
+ }
33752
34094
  /** A click on a drawing opens its settings toolbar (text labels edit text here too). */
33753
34095
  openSettingsById(id, _x, _y) {
33754
34096
  const drawing = this.drawings.find((d) => d.id === id);
33755
34097
  if (!drawing) return;
33756
34098
  const live = () => this.drawings.find((d) => d.id === id);
34099
+ this.popupMulti = false;
33757
34100
  this.emit({ kind: "select", ids: [id] });
33758
34101
  this.emit({ kind: "settings", id });
33759
34102
  const anchor = drawing.bounds(this.deps.projector());
33760
- this.popup.open(drawing, anchor, {
34103
+ this.popup.open([drawing], anchor, {
33761
34104
  // Sync rebuilds instances, so a panel that reads values back after a patch (e.g. the
33762
34105
  // position tool's price fields, where one edit can flip another level) resolves fresh.
33763
34106
  resolve: () => live() ?? null,
@@ -33775,6 +34118,11 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33775
34118
  this.emit({ kind: "edit", doc: d.serialize() });
33776
34119
  },
33777
34120
  reorder: (to) => this.emit({ kind: "reorder", id, to }),
34121
+ duplicate: () => {
34122
+ this.finishTextEditor(true);
34123
+ this.popup.close();
34124
+ this.emit({ kind: "duplicate", ids: [id] });
34125
+ },
33778
34126
  resetSettings: () => {
33779
34127
  const d = live();
33780
34128
  if (!d) return;
@@ -33797,7 +34145,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33797
34145
  this.popup.close();
33798
34146
  this.emit({ kind: "delete", ids: [id] });
33799
34147
  }
33800
- }, () => this.clearSelection());
34148
+ }, this.onPopupDismissed);
34149
+ }
34150
+ /** A plain click on the empty plot: drop the selection. The popup's dismiss-on-outside-press
34151
+ * does the same, but a selection can exist with no bar open (a lone Ctrl-click pick, a
34152
+ * selection whose bar was dismissed by a modifier press) — this covers those. */
34153
+ deselect() {
34154
+ this.clearSelection();
33801
34155
  }
33802
34156
  /** Report placement progress upstream (`draft` intent) so the drawings sync can
33803
34157
  * mirror the ghost live on linked charts. Called from every render; only actual
@@ -33823,6 +34177,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33823
34177
  }, 0);
33824
34178
  return;
33825
34179
  }
34180
+ if (i.kind === "clone") {
34181
+ const before = new Set(this.drawings.map((d) => d.id));
34182
+ this.intentCb?.(i);
34183
+ const fresh = this.drawings.filter((d) => !before.has(d.id));
34184
+ if (fresh.length === 1) this.openSettingsById(fresh[0].id, 0, 0);
34185
+ return;
34186
+ }
33826
34187
  this.intentCb?.(i);
33827
34188
  }
33828
34189
  };
@@ -36134,7 +36495,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36134
36495
  zoomTo: (target, anchorLogical, anchorX) => this.zoomTo(target, anchorLogical, anchorX),
36135
36496
  fling: (v) => this.fling(v),
36136
36497
  onPointerMove: (x, y) => this.handlePointerMove(x, y),
36137
- onClick: (x) => this.handleClick(x),
36498
+ onClick: (x) => {
36499
+ this.userDrawings?.deselect();
36500
+ this.handleClick(x);
36501
+ },
36138
36502
  onAxisLongPress: (axis, x, y) => {
36139
36503
  for (const cb of this.axisLongPressCbs) cb({ axis, x, y });
36140
36504
  },
@@ -36152,11 +36516,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36152
36516
  // User drawings claim a gesture before pan when armed / over a drawing.
36153
36517
  drawingsClaim: (x, y) => this.userDrawings?.claim(x, y) ?? false,
36154
36518
  drawingsMeasureStart: (x, y, snap) => this.userDrawings?.beginMeasureAt(x, y, snap) ?? false,
36155
- drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y) ?? false,
36519
+ drawingsMarqueeStart: (x, y) => this.userDrawings?.beginMarqueeAt(x, y) ?? false,
36520
+ drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y, true) ?? false,
36521
+ // middle-click: a selected hit takes the whole selection
36156
36522
  drawingsCancelPlacement: () => this.userDrawings?.cancelPlacement() ?? false,
36157
36523
  drawingsSnapMode: () => this.snapMode,
36158
- drawingsPointerDown: (x, y, snap, shift4) => this.userDrawings?.pointerDown(x, y, snap, shift4),
36159
- drawingsPointerMove: (x, y, snap, shift4) => this.userDrawings?.pointerMove(x, y, snap, shift4),
36524
+ drawingsPointerDown: (x, y, snap, shift4, mod) => this.userDrawings?.pointerDown(x, y, snap, shift4, mod),
36525
+ drawingsPointerMove: (x, y, snap, shift4, mod) => this.userDrawings?.pointerMove(x, y, snap, shift4, mod),
36160
36526
  drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
36161
36527
  drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
36162
36528
  drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
@@ -38519,6 +38885,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
38519
38885
  const v = inputs[key];
38520
38886
  return typeof v === "string" && v !== "" ? v : fallback;
38521
38887
  }
38888
+ function bool(inputs, key, fallback) {
38889
+ const v = inputs[key];
38890
+ return typeof v === "boolean" ? v : fallback;
38891
+ }
38522
38892
  function toPoints(bars, values, colors) {
38523
38893
  const pts = new Array(bars.length);
38524
38894
  for (let i = 0; i < bars.length; i++) {
@@ -40050,10 +40420,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
40050
40420
  plots: [
40051
40421
  { key: "up", title: "Aroon up", values: up, color: BULLISH, width: 2 },
40052
40422
  { key: "down", title: "Aroon down", values: down, color: BEARISH, width: 2 }
40053
- ],
40054
- levels: [
40055
- { key: "upper", price: 100, color: NEUTRAL, lineStyle: "dotted" },
40056
- { key: "lower", price: 0, color: NEUTRAL, lineStyle: "dotted" }
40057
40423
  ]
40058
40424
  };
40059
40425
  }
@@ -40551,8 +40917,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
40551
40917
  var DAY_MS2 = 864e5;
40552
40918
  function periodKey(anchor, time) {
40553
40919
  if (anchor === "Week") return Math.floor((Math.floor(time / DAY_MS2) + 3) / 7);
40554
- if (anchor === "Month") {
40920
+ if (anchor === "Month" || anchor === "Quarter" || anchor === "Year") {
40555
40921
  const d = new Date(time);
40922
+ if (anchor === "Year") return d.getUTCFullYear();
40923
+ if (anchor === "Quarter") return d.getUTCFullYear() * 4 + Math.floor(d.getUTCMonth() / 3);
40556
40924
  return d.getUTCFullYear() * 12 + d.getUTCMonth();
40557
40925
  }
40558
40926
  return Math.floor(time / DAY_MS2);
@@ -40604,40 +40972,86 @@ ${overlayScrollbarCss(".vela-sd-pane")}
40604
40972
  return { plots: [{ key: "sar", title: "PSAR", values, kind: "circles", color: SERIES_LINE, colors }] };
40605
40973
  }
40606
40974
  };
40975
+ var VWAP_BANDS_GROUP = "Bands";
40976
+ var VWAP_STYLE = "Style";
40977
+ var VWAP_BANDS = [
40978
+ { on: "band1", mult: "band1Mult", color: "band1Color", fill: "band1Fill", fillColor: "band1FillColor", defMult: 1, defOn: true, defColor: BULLISH },
40979
+ { on: "band2", mult: "band2Mult", color: "band2Color", fill: "band2Fill", fillColor: "band2FillColor", defMult: 2, defOn: false, defColor: WARNING },
40980
+ { on: "band3", mult: "band3Mult", color: "band3Color", fill: "band3Fill", fillColor: "band3FillColor", defMult: 3, defOn: false, defColor: CATEGORICAL[4] }
40981
+ ];
40607
40982
  var vwap = {
40608
40983
  type: "vwap",
40609
40984
  title: "Volume Weighted Average Price",
40610
40985
  shortTitle: "VWAP",
40611
40986
  overlay: true,
40612
40987
  inputs: [
40613
- { key: "anchor", title: "Anchor", type: "string", defval: "Day", options: ["Day", "Week", "Month"], tooltip: "Where the accumulation resets (UTC periods)" },
40614
- { key: "source", title: "Source", type: "string", defval: "HLC3", options: ["HLC3", "OHLC4", "Close"] },
40615
- colorInput(INFO)
40988
+ { key: "anchor", title: "Period", type: "string", defval: "Day", options: ["Day", "Week", "Month", "Quarter", "Year"], tooltip: "Where the accumulation resets (UTC periods)" },
40989
+ sourceInput("HLC3"),
40990
+ // Each band is one row: its toggle leads, the multiplier follows unlabeled.
40991
+ ...VWAP_BANDS.flatMap((b, i) => [
40992
+ { key: b.on, title: `Band ${i + 1} multiplier`, type: "bool", defval: b.defOn, inline: b.on, group: VWAP_BANDS_GROUP },
40993
+ { key: b.mult, title: "", type: "float", defval: b.defMult, min: 0.1, max: 10, step: 0.1, inline: b.on, group: VWAP_BANDS_GROUP }
40994
+ ]),
40995
+ { ...colorInput(INFO, "VWAP color"), group: VWAP_STYLE },
40996
+ // One row per band: its ink, then the fill toggle with the fill's own (translucent) color.
40997
+ ...VWAP_BANDS.flatMap((b, i) => [
40998
+ { ...colorInput(b.defColor, `Band ${i + 1} color`, b.color), inline: b.color, group: VWAP_STYLE },
40999
+ { key: b.fill, title: "Fill", type: "bool", defval: true, inline: b.color, group: VWAP_STYLE },
41000
+ { ...colorInput(withAlpha2(b.defColor), "", b.fillColor), inline: b.color, group: VWAP_STYLE }
41001
+ ])
40616
41002
  ],
40617
41003
  compute: (bars, inputs) => {
40618
41004
  const anchor = str3(inputs, "anchor", "Day");
40619
- const source = str3(inputs, "source", "HLC3");
40620
- const values = new Array(bars.length).fill(Number.NaN);
41005
+ const src = sourceValues(bars, str3(inputs, "source", "HLC3"));
41006
+ const n = bars.length;
41007
+ const values = new Array(n).fill(Number.NaN);
41008
+ const bands = VWAP_BANDS.filter((b) => bool(inputs, b.on, b.defOn)).map((b) => ({
41009
+ mult: Math.max(0, num3(inputs, b.mult, b.defMult)),
41010
+ color: str3(inputs, b.color, b.defColor),
41011
+ fill: bool(inputs, b.fill, true),
41012
+ fillColor: str3(inputs, b.fillColor, withAlpha2(b.defColor)),
41013
+ up: new Array(n).fill(Number.NaN),
41014
+ down: new Array(n).fill(Number.NaN)
41015
+ }));
40621
41016
  let period = Number.NaN;
40622
41017
  let cumPV = 0;
41018
+ let cumPV2 = 0;
40623
41019
  let cumV = 0;
40624
- for (let i = 0; i < bars.length; i++) {
41020
+ for (let i = 0; i < n; i++) {
40625
41021
  const b = bars[i];
40626
41022
  const key = periodKey(anchor, b.time);
40627
41023
  if (key !== period) {
40628
41024
  period = key;
40629
41025
  cumPV = 0;
41026
+ cumPV2 = 0;
40630
41027
  cumV = 0;
40631
41028
  }
40632
41029
  const v = b.volume;
40633
41030
  if (v != null && Number.isFinite(v) && v > 0) {
40634
- const tp = source === "Close" ? b.close : source === "OHLC4" ? (b.open + b.high + b.low + b.close) / 4 : (b.high + b.low + b.close) / 3;
41031
+ const tp = src[i];
40635
41032
  cumPV += tp * v;
41033
+ cumPV2 += tp * tp * v;
40636
41034
  cumV += v;
40637
41035
  }
40638
- if (cumV > 0) values[i] = cumPV / cumV;
40639
- }
40640
- return { plots: [{ key: "vwap", title: "VWAP", values, color: str3(inputs, "color", INFO), width: 2 }] };
41036
+ if (cumV <= 0) continue;
41037
+ const mean = cumPV / cumV;
41038
+ const sd = Math.sqrt(Math.max(0, cumPV2 / cumV - mean * mean));
41039
+ values[i] = mean;
41040
+ for (const band of bands) {
41041
+ band.up[i] = mean + band.mult * sd;
41042
+ band.down[i] = mean - band.mult * sd;
41043
+ }
41044
+ }
41045
+ const plots = [{ key: "vwap", title: "VWAP", values, color: str3(inputs, "color", INFO), width: 2 }];
41046
+ const fills = [];
41047
+ bands.forEach((band, i) => {
41048
+ plots.push(
41049
+ { key: `up${i}`, title: `Upper ${band.mult}\u03C3`, values: band.up, color: band.color },
41050
+ { key: `down${i}`, title: `Lower ${band.mult}\u03C3`, values: band.down, color: band.color }
41051
+ );
41052
+ if (band.fill) fills.push({ key: `band${i}`, from: `up${i}`, to: `down${i}`, color: band.fillColor });
41053
+ });
41054
+ return { plots, bands: fills };
40641
41055
  }
40642
41056
  };
40643
41057
  var pivotPoints = {