@luxalgo/vela 0.6.17 → 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 (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-BXF7VNNE.js} +177 -104
  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-D91cU2fF.d.cts → statusline-model-7ECSDtgt.d.cts} +3 -3
  24. package/dist/{statusline-model-IURCya0H.d.ts → statusline-model-D1Fhs8Dk.d.ts} +3 -3
  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 +739 -252
  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 +739 -252
  34. package/dist/workspace.d.cts +5 -5
  35. package/dist/workspace.d.ts +5 -5
  36. package/dist/workspace.js +2 -2
  37. package/package.json +1 -1
@@ -5039,15 +5039,15 @@ function candleOverrideFor(style, bags) {
5039
5039
  if (!hasOwnCandlePaint(style)) return null;
5040
5040
  const bag = bags[style] ?? {};
5041
5041
  const color = (v) => isColor(v) ? v : null;
5042
- const bool = (v) => isBool(v) ? v : null;
5042
+ const bool2 = (v) => isBool(v) ? v : null;
5043
5043
  return {
5044
5044
  upColor: color(bag.candleUpColor),
5045
5045
  downColor: color(bag.candleDownColor),
5046
- bodyVisible: bool(bag.candleBodyVisible),
5047
- borderVisible: bool(bag.candleBorderVisible),
5046
+ bodyVisible: bool2(bag.candleBodyVisible),
5047
+ borderVisible: bool2(bag.candleBorderVisible),
5048
5048
  borderUpColor: color(bag.candleBorderUpColor),
5049
5049
  borderDownColor: color(bag.candleBorderDownColor),
5050
- wickVisible: bool(bag.candleWickVisible),
5050
+ wickVisible: bool2(bag.candleWickVisible),
5051
5051
  wickUpColor: color(bag.candleWickUpColor),
5052
5052
  wickDownColor: color(bag.candleWickDownColor)
5053
5053
  };
@@ -12034,6 +12034,9 @@ function paneLabel(pane, fallbackTitle) {
12034
12034
  function indicatorLabel(i, fallbackTitle) {
12035
12035
  return i.title || fallbackTitle(i.id) || i.id;
12036
12036
  }
12037
+ function indicatorRowLabel(i, fallbackTitle) {
12038
+ return i.shorttitle || indicatorLabel(i, fallbackTitle);
12039
+ }
12037
12040
  function paneRows(pane) {
12038
12041
  return pane.items.flatMap((it) => it.kind === "row" ? [it.row] : []);
12039
12042
  }
@@ -12165,7 +12168,7 @@ function paneItems(snap, pane, units) {
12165
12168
  const indicatorRow = (i) => ({
12166
12169
  kind: "indicator",
12167
12170
  id: i.id,
12168
- label: indicatorLabel(i, snap.handleTitle),
12171
+ label: indicatorRowLabel(i, snap.handleTitle),
12169
12172
  visible: snap.indicatorVisible(i.id),
12170
12173
  ownScale: i.ownScale
12171
12174
  });
@@ -12578,9 +12581,13 @@ var CSS6 = `
12578
12581
  .vela-ot-empty { padding: 20px 10px; text-align: center; color: var(--vela-fg-muted); font-size: 12px; }
12579
12582
 
12580
12583
  /* \u2500\u2500 drawing groups \u2500\u2500 */
12581
- /* One top-level entry in a pane's drawing list: a lone drawing, or a whole group block. The
12582
- transparent border reserves the outline used when a drawing is dropped into the group. */
12583
- .vela-ot-unit { border: 1px solid transparent; border-radius: 6px; }
12584
+ /* One top-level entry in a pane's drawing list: a lone drawing, or a whole group block. No
12585
+ border of its own \u2014 units stack flush, so consecutive highlighted rows read as one block; the
12586
+ drop-into-group outline is drawn inside the box instead (see the drag-and-drop rules). */
12587
+ .vela-ot-unit { border-radius: 6px; }
12588
+ /* Adjacent picked rows merge into one contiguous highlight: the shared edge loses its rounding. */
12589
+ .vela-ot-unit:has(> .vela-ot-row[data-picked]) + .vela-ot-unit > .vela-ot-row[data-picked] { border-top-left-radius: 0; border-top-right-radius: 0; }
12590
+ .vela-ot-unit:has(+ .vela-ot-unit > .vela-ot-row[data-picked]) > .vela-ot-row[data-picked] { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
12584
12591
  .vela-ot-row[data-row-kind='group'] > .vela-icon { width: 12px; font-size: 11px; }
12585
12592
  /* A member sits indented under its group's header. */
12586
12593
  .vela-ot-subrow { padding-left: 26px; }
@@ -12637,8 +12644,10 @@ var CSS6 = `
12637
12644
  .vela-ot-gap[data-drop] { height: 4px; background: var(--vela-fg-bright); }
12638
12645
  .vela-ot-gap[data-drop]::before { display: none; }
12639
12646
  /* A whole container accepts the drop: merging into a pane, or a pane with no drawings yet.
12640
- The pane block's transparent border reserves the room for this outline. */
12647
+ The pane block's transparent border reserves the room for this outline; a group unit has no
12648
+ border to color, so it draws the same line as an inset outline (no layout footprint). */
12641
12649
  .vela-ot [data-drop='target'] { border-color: var(--vela-fg-bright); background: var(--vela-hover); }
12650
+ .vela-ot-unit[data-drop='target'] { outline: 1px solid var(--vela-fg-bright); outline-offset: -1px; }
12642
12651
  /* Where a reorder would insert. */
12643
12652
  .vela-ot [data-drop='before'] { box-shadow: inset 0 2px 0 var(--vela-fg-bright); }
12644
12653
  .vela-ot [data-drop='after'] { box-shadow: inset 0 -2px 0 var(--vela-fg-bright); }
@@ -12701,7 +12710,8 @@ var ObjectTree = class extends SidePanel {
12701
12710
  super(host, "Object tree", "vela-ot");
12702
12711
  this.iconFor = iconFor;
12703
12712
  this.chart = null;
12704
- this.selectedDrawing = null;
12713
+ /** What the CHART has selected — every member of a multi-selection, mirrored as rows. */
12714
+ this.selectedDrawings = /* @__PURE__ */ new Set();
12705
12715
  this.symbolName = "";
12706
12716
  /** The raw (possibly venue-prefixed) symbol — what icon resolution routes on. */
12707
12717
  this.symbolRaw = "";
@@ -12752,12 +12762,9 @@ var ObjectTree = class extends SidePanel {
12752
12762
  this.unsubs.push(chart.on(ev, () => this.refresh()));
12753
12763
  }
12754
12764
  this.unsubs.push(
12755
- chart.on("drawing:selected", ({ id }) => {
12756
- this.selectedDrawing = id;
12757
- if (!this.syncingSelection) {
12758
- this.picked.clear();
12759
- if (id) this.picked.add(id);
12760
- }
12765
+ chart.on("drawing:selected", ({ ids }) => {
12766
+ this.selectedDrawings = new Set(ids);
12767
+ if (!this.syncingSelection) this.picked = new Set(ids);
12761
12768
  this.refresh();
12762
12769
  })
12763
12770
  );
@@ -13105,7 +13112,7 @@ var ObjectTree = class extends SidePanel {
13105
13112
  el.dataset.drag = "1";
13106
13113
  el.title = "Drag to restack, or onto another pane to move it there";
13107
13114
  if (inGroup) el.classList.add("vela-ot-subrow");
13108
- if (d.id === this.selectedDrawing) el.dataset.selected = "1";
13115
+ if (this.selectedDrawings.has(d.id)) el.dataset.selected = "1";
13109
13116
  if (this.picked.has(d.id)) el.dataset.picked = "1";
13110
13117
  el.addEventListener("click", (e) => this.onDrawClick(e, d.id));
13111
13118
  return el;
@@ -14269,6 +14276,20 @@ var SymbolPicker = class {
14269
14276
  this.seed = initialQuery;
14270
14277
  this.dialog.show();
14271
14278
  }
14279
+ get isOpen() {
14280
+ return this.dialog.open;
14281
+ }
14282
+ /** Text typed at the picker that never reached its field: closed, it opens seeded
14283
+ * with the text; open (the field takes focus a tick after the dialog does, and a
14284
+ * fast typist's next key lands in that tick), it extends the query. */
14285
+ type(text) {
14286
+ if (!this.isOpen) {
14287
+ this.open(text);
14288
+ return;
14289
+ }
14290
+ this.input.value = (this.input.value + text).toUpperCase();
14291
+ this.refresh();
14292
+ }
14272
14293
  close() {
14273
14294
  this.dialog.hide();
14274
14295
  }
@@ -14667,6 +14688,20 @@ var TimeframeQuick = class {
14667
14688
  this.input.setSelectionRange(this.input.value.length, this.input.value.length);
14668
14689
  }, 0);
14669
14690
  }
14691
+ get isOpen() {
14692
+ return this.dialog.open;
14693
+ }
14694
+ /** Text typed at the entry that never reached its field: closed, it opens seeded
14695
+ * with the text; open (the field takes focus a tick after the dialog does, and a
14696
+ * fast typist's next key lands in that tick), it extends the entry. */
14697
+ type(text) {
14698
+ if (!this.isOpen) {
14699
+ this.open(text);
14700
+ return;
14701
+ }
14702
+ this.input.value += text;
14703
+ this.renderHint();
14704
+ }
14670
14705
  close() {
14671
14706
  this.dialog.hide();
14672
14707
  }
@@ -16737,15 +16772,18 @@ var DrawingController = class {
16737
16772
  next = [...ids];
16738
16773
  }
16739
16774
  this.selectedIds = next.filter((id) => this.store.has(id));
16775
+ this.announceSelection();
16776
+ }
16777
+ /** Push the selection to the renderer and announce it (primary + the full list). */
16778
+ announceSelection() {
16740
16779
  this.port?.setSelection(this.selectedIds);
16741
- this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null });
16780
+ this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null, ids: [...this.selectedIds] });
16742
16781
  }
16743
16782
  /** Restore a history snapshot, reconciling selection against what survived. */
16744
16783
  restoreSnapshot(doc) {
16745
16784
  this.store.load(doc);
16746
16785
  this.selectedIds = this.selectedIds.filter((id) => this.store.has(id));
16747
- this.port?.setSelection(this.selectedIds);
16748
- this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null });
16786
+ this.announceSelection();
16749
16787
  }
16750
16788
  /** Delete drawings as one undo step; prune them from the selection. */
16751
16789
  deleteMany(ids) {
@@ -16878,6 +16916,9 @@ var DrawingController = class {
16878
16916
  case "duplicate":
16879
16917
  this.duplicate(i.ids);
16880
16918
  break;
16919
+ case "clone":
16920
+ if (this.enabled && i.docs.length) this.insertClones(i.docs);
16921
+ break;
16881
16922
  case "copy":
16882
16923
  this.copy(i.ids);
16883
16924
  break;
@@ -19844,7 +19885,12 @@ var EngineOrchestrator = class _EngineOrchestrator {
19844
19885
  if (!model) continue;
19845
19886
  const paneId = model.paneId ?? "price";
19846
19887
  if (!byPane.has(paneId)) byPane.set(paneId, []);
19847
- byPane.get(paneId).push({ id: r.id, title: model.title || r.title, ownScale: model.ownScale === true });
19888
+ byPane.get(paneId).push({
19889
+ id: r.id,
19890
+ title: model.title || r.title,
19891
+ ...model.shorttitle ? { shorttitle: model.shorttitle } : {},
19892
+ ownScale: model.ownScale === true
19893
+ });
19848
19894
  }
19849
19895
  const panes = [];
19850
19896
  for (const paneId of this.paneOrder) {
@@ -24477,7 +24523,7 @@ var InputController = class {
24477
24523
  if (e.repeat || e.key !== "Shift" && e.key !== "Control" && e.key !== "Meta") return;
24478
24524
  if (Number.isNaN(this.cursorX)) return;
24479
24525
  if (this.dragging && this.region !== "drawing") return;
24480
- this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey);
24526
+ this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
24481
24527
  };
24482
24528
  this.onDown = (e) => {
24483
24529
  if (e.button === 1) {
@@ -24509,7 +24555,7 @@ var InputController = class {
24509
24555
  this.startY = y;
24510
24556
  if (this.deps.drawingsClaim?.(x, y)) {
24511
24557
  this.region = "drawing";
24512
- this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey);
24558
+ this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
24513
24559
  this.capture(e.pointerId);
24514
24560
  return;
24515
24561
  }
@@ -24518,6 +24564,11 @@ var InputController = class {
24518
24564
  this.capture(e.pointerId);
24519
24565
  return;
24520
24566
  }
24567
+ if ((e.ctrlKey || e.metaKey) && this.regionAt(x, y) === "data" && this.deps.drawingsMarqueeStart?.(x, y)) {
24568
+ this.region = "drawing";
24569
+ this.capture(e.pointerId);
24570
+ return;
24571
+ }
24521
24572
  const vp = this.deps.getCoords().getViewport();
24522
24573
  this.deps.apply(vp);
24523
24574
  this.startRightOffset = vp.rightOffset;
@@ -24566,7 +24617,7 @@ var InputController = class {
24566
24617
  this.deps.apply({ barSpacing, rightOffset: this.startRightOffset });
24567
24618
  } else if (this.region === "drawing") {
24568
24619
  if (Math.abs(x - this.startX) > slop || Math.abs(y - this.startY) > slop) this.moved = true;
24569
- this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
24620
+ this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
24570
24621
  } else {
24571
24622
  const coords = this.deps.getCoords();
24572
24623
  const vp = coords.getViewport();
@@ -24589,7 +24640,7 @@ var InputController = class {
24589
24640
  const drawCursor = this.deps.drawingsCursor?.(x, y);
24590
24641
  const r = this.regionAt(x, y);
24591
24642
  this.el.style.cursor = drawCursor ?? (r === "price" ? "ns-resize" : r === "time" ? "ew-resize" : r === "separator" ? "row-resize" : "");
24592
- this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
24643
+ this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
24593
24644
  }
24594
24645
  if (e.pointerType !== "touch") this.deps.onPointerMove(x, y);
24595
24646
  };
@@ -24930,12 +24981,12 @@ function defaultTradeMarkersState() {
24930
24981
  function mergeTradeMarkersState(base, patch) {
24931
24982
  const p = patch && typeof patch === "object" ? patch : {};
24932
24983
  const c = p.colors && typeof p.colors === "object" ? p.colors : {};
24933
- const bool = (v, fb) => typeof v === "boolean" ? v : fb;
24984
+ const bool2 = (v, fb) => typeof v === "boolean" ? v : fb;
24934
24985
  const color = (v, fb) => typeof v === "string" && v.trim().length > 0 ? v : fb;
24935
24986
  return {
24936
- visible: bool(p.visible, base.visible),
24937
- labels: bool(p.labels, base.labels),
24938
- qty: bool(p.qty, base.qty),
24987
+ visible: bool2(p.visible, base.visible),
24988
+ labels: bool2(p.labels, base.labels),
24989
+ qty: bool2(p.qty, base.qty),
24939
24990
  colors: {
24940
24991
  long: color(c.long, base.colors.long),
24941
24992
  short: color(c.short, base.colors.short),
@@ -28795,6 +28846,18 @@ var DrawingPainter = class {
28795
28846
  }
28796
28847
  this.paintHandles(ctx, pts);
28797
28848
  }
28849
+ /** The selection box a Ctrl/Cmd-drag on the empty plot sweeps: a faint fill in the handle
28850
+ * accent under a thin dashed outline. */
28851
+ paintMarquee(ctx, rect) {
28852
+ ctx.save();
28853
+ ctx.fillStyle = withAlpha(HANDLE_BORDER, 0.08);
28854
+ ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
28855
+ ctx.strokeStyle = HANDLE_BORDER;
28856
+ ctx.lineWidth = 1;
28857
+ ctx.setLineDash([4, 3]);
28858
+ ctx.strokeRect(rect.x + 0.5, rect.y + 0.5, rect.w, rect.h);
28859
+ ctx.restore();
28860
+ }
28798
28861
  /** A hollow ring marking the candle point the magnet (Ctrl) will snap the next anchor to. */
28799
28862
  paintSnapRing(ctx, x, y, theme) {
28800
28863
  ctx.setLineDash([]);
@@ -30263,6 +30326,15 @@ function topDrawingAt(drawings, x, y, proj, tol = HIT_TOLERANCE) {
30263
30326
  }
30264
30327
  return null;
30265
30328
  }
30329
+ function deletableSelection(selected, drawings) {
30330
+ const ids = [...selected];
30331
+ if (ids.length < 2) return ids;
30332
+ return ids.filter((id) => !drawings.find((d) => d.id === id)?.locked);
30333
+ }
30334
+ function deleteTargets(hit, selected, drawings, withSelection) {
30335
+ if (withSelection && selected.size >= 2 && selected.has(hit.id)) return deletableSelection(selected, drawings);
30336
+ return hit.locked ? [] : [hit.id];
30337
+ }
30266
30338
 
30267
30339
  // src/renderers/native/drawings/DrawingInteraction.ts
30268
30340
  var DRAG_SLOP3 = 3;
@@ -30366,10 +30438,32 @@ var DrawingInteraction = class {
30366
30438
  activeDragId() {
30367
30439
  return this.state.kind === "pressed" && this.state.moved ? this.state.id : null;
30368
30440
  }
30441
+ /** The drawing under a press that has not been released yet (drag or click undecided), or null. */
30442
+ pressedId() {
30443
+ return this.state.kind === "pressed" ? this.state.id : null;
30444
+ }
30445
+ /** The copies a Ctrl-drag is moving (transient — not yet in the store), or null. */
30446
+ dragClones() {
30447
+ return this.state.kind === "pressed" && this.state.moved && this.state.clones ? this.state.clones.map((c) => c.d) : null;
30448
+ }
30449
+ /** The marquee box being swept, normalized (or null when none is in progress). */
30450
+ marqueeRect() {
30451
+ if (this.state.kind !== "marquee") return null;
30452
+ const { x0, y0, x1, y1 } = this.state;
30453
+ return { x: Math.min(x0, x1), y: Math.min(y0, y1), w: Math.abs(x1 - x0), h: Math.abs(y1 - y0) };
30454
+ }
30455
+ /** Ctrl/Cmd-press on the empty plot: start sweeping a selection box. False when a tool is
30456
+ * armed or a gesture is in flight (that path owns the press). */
30457
+ beginMarquee(x, y) {
30458
+ if (this.state.kind !== "idle" || this.deps.activeTool() != null) return false;
30459
+ this.state = { kind: "marquee", x0: x, y0: y, x1: x, y1: y };
30460
+ return true;
30461
+ }
30369
30462
  /** A press: place the next anchor, or grab a drawing/handle (resolved as click or drag on release).
30370
30463
  * `shift` over a drawing toggles it in/out of the selection (multi-select) instead of dragging;
30371
- * while placing a line tool it locks the segment angle to 45° steps. */
30372
- down(x, y, mode = "off", shift2 = false) {
30464
+ * while placing a line tool it locks the segment angle to 45° steps. `mod` (Ctrl/Cmd) over a
30465
+ * drawing also toggles on a click, but a body drag then moves a COPY. */
30466
+ down(x, y, mode = "off", shift2 = false, mod = false) {
30373
30467
  const proj = this.deps.projector();
30374
30468
  if (this.state.kind === "placing") {
30375
30469
  if (this.state.draft.placementMode() !== "click") return;
@@ -30397,7 +30491,50 @@ var DrawingInteraction = class {
30397
30491
  return;
30398
30492
  }
30399
30493
  const handle = hit.hitHandle(x, y, proj, HIT_TOLERANCE);
30400
- this.state = { kind: "pressed", id: hit.id, handle, grab: proj.pxToPoint(x, y, hit.paneId), orig: cloneAnchors(hit), px: x, py: y, moved: false };
30494
+ const riders = [];
30495
+ if (handle < 0 && this.deps.selectedIds().has(hit.id)) {
30496
+ for (const id of this.deps.selectedIds()) {
30497
+ const d = id === hit.id ? null : this.byId(id);
30498
+ if (d && !d.locked && d.visible) riders.push({ id, orig: cloneAnchors(d) });
30499
+ }
30500
+ }
30501
+ this.state = { kind: "pressed", id: hit.id, handle, orig: cloneAnchors(hit), px: x, py: y, moved: false, riders, mod, clones: null };
30502
+ }
30503
+ }
30504
+ /** The data-space delta a body drag from the press to (x, y) means on `paneId` — resolved per
30505
+ * pane so drawings riding along on another pane move by the same PIXELS, not the same price. */
30506
+ bodyDelta(paneId, x, y) {
30507
+ if (this.state.kind !== "pressed") return { dt: 0, dp: 0 };
30508
+ const proj = this.deps.projector();
30509
+ const from = proj.pxToPoint(this.state.px, this.state.py, paneId);
30510
+ const to = proj.pxToPoint(x, y, paneId);
30511
+ return { dt: to.time - from.time, dp: to.price - from.price };
30512
+ }
30513
+ /** The pressed drawing and its riders, each with its anchors at the press. */
30514
+ dragSources(d) {
30515
+ if (this.state.kind !== "pressed") return [];
30516
+ const out = [{ d, orig: this.state.orig }];
30517
+ for (const r of this.state.riders) {
30518
+ const rd = this.byId(r.id);
30519
+ if (rd) out.push({ d: rd, orig: r.orig });
30520
+ }
30521
+ return out;
30522
+ }
30523
+ /** Move the pressed drawing's body (and its riders) — or, on a Ctrl-drag, their copies — to the
30524
+ * cursor. The copies are built on the first move (so a Ctrl-click never creates anything), from
30525
+ * sources that have not moved yet, so their own anchors are the origin to translate from. */
30526
+ dragBody(d, x, y) {
30527
+ if (this.state.kind !== "pressed") return;
30528
+ this.snapAt = null;
30529
+ if (this.state.mod && !this.state.clones) {
30530
+ this.state.clones = this.dragSources(d).flatMap((s) => {
30531
+ const c = deserializeDrawing(s.d.serialize());
30532
+ return c ? [{ d: c, orig: cloneAnchors(c) }] : [];
30533
+ });
30534
+ }
30535
+ for (const t of this.state.clones ?? this.dragSources(d)) {
30536
+ const { dt, dp } = this.bodyDelta(t.d.paneId, x, y);
30537
+ t.d.anchors = t.d.translateBody(dt, dp, t.orig);
30401
30538
  }
30402
30539
  }
30403
30540
  /** Pointer move: advance the placing ghost, or live-preview a drag once past the slop.
@@ -30421,12 +30558,20 @@ var DrawingInteraction = class {
30421
30558
  this.deps.changed();
30422
30559
  return;
30423
30560
  }
30561
+ if (this.state.kind === "marquee") {
30562
+ this.state.x1 = x;
30563
+ this.state.y1 = y;
30564
+ this.deps.changed();
30565
+ return;
30566
+ }
30424
30567
  if (this.state.kind !== "pressed") return;
30425
30568
  const d = this.byId(this.state.id);
30426
30569
  if (!d || d.locked) return;
30427
30570
  if (!this.state.moved && Math.abs(x - this.state.px) <= DRAG_SLOP3 && Math.abs(y - this.state.py) <= DRAG_SLOP3) return;
30428
30571
  this.state.moved = true;
30429
- if (this.state.handle >= 0) {
30572
+ if (this.state.handle < 0) {
30573
+ this.dragBody(d, x, y);
30574
+ } else {
30430
30575
  const a = d.anchors[this.state.handle];
30431
30576
  if (a) {
30432
30577
  const pivot = shift2 && ANGLE_SNAP_TYPES.has(d.type) && d.anchors.length === 2 ? d.anchors[1 - this.state.handle] : void 0;
@@ -30434,12 +30579,6 @@ var DrawingInteraction = class {
30434
30579
  applyFree(a, point, d.anchorSchema().slots[this.state.handle]?.free ?? "both");
30435
30580
  }
30436
30581
  d.constrainHandleDrag(this.state.handle);
30437
- } else {
30438
- const pt = this.deps.projector().pxToPoint(x, y, d.paneId);
30439
- this.snapAt = null;
30440
- const dt = pt.time - this.state.grab.time;
30441
- const dp = pt.price - this.state.grab.price;
30442
- d.anchors = d.translateBody(dt, dp, this.state.orig);
30443
30582
  }
30444
30583
  this.deps.changed();
30445
30584
  }
@@ -30459,16 +30598,46 @@ var DrawingInteraction = class {
30459
30598
  }
30460
30599
  return;
30461
30600
  }
30601
+ if (this.state.kind === "marquee") {
30602
+ const rect = this.marqueeRect();
30603
+ this.state = { kind: "idle" };
30604
+ if (rect.w > DRAG_SLOP3 || rect.h > DRAG_SLOP3) this.selectWithin(rect);
30605
+ this.deps.changed();
30606
+ return;
30607
+ }
30462
30608
  if (this.state.kind !== "pressed") return;
30463
- const { id, moved } = this.state;
30609
+ const { id, moved, riders, mod, clones } = this.state;
30464
30610
  this.state = { kind: "idle" };
30465
30611
  const d = this.byId(id);
30466
- if (moved) {
30467
- if (d) this.deps.emit({ kind: "edit", doc: d.serialize() });
30468
- this.deps.changed();
30469
- } else {
30470
- this.deps.openSettings(id, x, y);
30612
+ if (!moved) {
30613
+ if (mod) this.deps.emit({ kind: "select", ids: [id], additive: true });
30614
+ else this.deps.openSettings(id, x, y);
30615
+ return;
30471
30616
  }
30617
+ if (clones) {
30618
+ this.deps.emit({ kind: "clone", docs: clones.map((c) => c.d.serialize()) });
30619
+ } else if (d) {
30620
+ const docs = [d.serialize()];
30621
+ for (const r of riders) {
30622
+ const rd = this.byId(r.id);
30623
+ if (rd) docs.push(rd.serialize());
30624
+ }
30625
+ if (docs.length > 1) this.deps.emit({ kind: "edit-many", docs });
30626
+ else this.deps.emit({ kind: "edit", doc: docs[0] });
30627
+ }
30628
+ this.deps.changed();
30629
+ }
30630
+ /** Add every visible drawing whose on-screen bounds touch `rect` to the selection (a union,
30631
+ * so successive sweeps accumulate). No change when the box touches nothing new. */
30632
+ selectWithin(rect) {
30633
+ const proj = this.deps.projector();
30634
+ const ids = [...this.deps.selectedIds()];
30635
+ for (const d of this.deps.drawings()) {
30636
+ if (!d.visible || ids.includes(d.id)) continue;
30637
+ const b = d.bounds(proj);
30638
+ if (b && rectsIntersect(rect, b)) ids.push(d.id);
30639
+ }
30640
+ if (ids.length > this.deps.selectedIds().size) this.deps.emit({ kind: "select", ids });
30472
30641
  }
30473
30642
  /** Cancel an in-progress placement or drag (Escape). Returns whether anything was cancelled. */
30474
30643
  cancel() {
@@ -30481,8 +30650,15 @@ var DrawingInteraction = class {
30481
30650
  return true;
30482
30651
  }
30483
30652
  if (this.state.kind === "pressed") {
30484
- const d = this.byId(this.state.id);
30485
- if (d && this.state.moved) d.anchors = this.state.orig.map((o) => ({ time: o.time, price: o.price }));
30653
+ if (this.state.moved && !this.state.clones) {
30654
+ const d = this.byId(this.state.id);
30655
+ if (d) for (const s of this.dragSources(d)) s.d.anchors = s.orig.map((o) => ({ time: o.time, price: o.price }));
30656
+ }
30657
+ this.state = { kind: "idle" };
30658
+ this.deps.changed();
30659
+ return true;
30660
+ }
30661
+ if (this.state.kind === "marquee") {
30486
30662
  this.state = { kind: "idle" };
30487
30663
  this.deps.changed();
30488
30664
  return true;
@@ -30597,6 +30773,9 @@ var DrawingInteraction = class {
30597
30773
  function cloneAnchors(d) {
30598
30774
  return d.anchors.map((a) => ({ time: a.time, price: a.price }));
30599
30775
  }
30776
+ function rectsIntersect(a, b) {
30777
+ 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;
30778
+ }
30600
30779
  function applyFree(anchor, pt, free) {
30601
30780
  if (free === "both" || free === "x") anchor.time = pt.time;
30602
30781
  if (free === "both" || free === "y") anchor.price = pt.price;
@@ -30982,10 +31161,27 @@ function btn(label, primary, onClick) {
30982
31161
  }
30983
31162
 
30984
31163
  // src/renderers/native/drawings/DrawingSettingsPopup.ts
31164
+ var MIXED = /* @__PURE__ */ Symbol("mixed");
31165
+ function sharedPaths(drawings) {
31166
+ const [first, ...rest] = drawings;
31167
+ const paths = new Set(first?.schema().fields.map((f) => f.path) ?? []);
31168
+ for (const d of rest) {
31169
+ const own = new Set(d.schema().fields.map((f) => f.path));
31170
+ for (const p of paths) if (!own.has(p)) paths.delete(p);
31171
+ }
31172
+ return paths;
31173
+ }
31174
+ function commonValue(drawings, read) {
31175
+ const first = read(drawings[0]);
31176
+ return drawings.every((d) => read(d) === first) ? first : MIXED;
31177
+ }
31178
+ function distinctValues(drawings, read) {
31179
+ return [...new Set(drawings.map(read))];
31180
+ }
30985
31181
  var BTN = 30;
30986
31182
  var ICON = 21;
30987
31183
  var STYLE_ID23 = "vela-drawing-popup-styles";
30988
- var STYLE_REV = "3";
31184
+ var STYLE_REV = "4";
30989
31185
  function ensureStyles5() {
30990
31186
  if (typeof document === "undefined") return;
30991
31187
  const existing = document.getElementById(STYLE_ID23);
@@ -30997,6 +31193,8 @@ function ensureStyles5() {
30997
31193
  .vela-dpop-btn{background:transparent;color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
30998
31194
  .vela-dpop-btn:hover{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
30999
31195
  .vela-dpop-btn[data-active='1']{background:var(--vela-active);color:var(--vela-fg-bright);}
31196
+ /* Mixed (a multi-selection disagrees): the active fill at half strength behind a dashed ring. */
31197
+ .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;}
31000
31198
  .vela-dpop-item{background:transparent;transition:background var(--vela-dur-fast) ease;}
31001
31199
  .vela-dpop-item:hover{background:var(--vela-hover-strong);}
31002
31200
  .vela-dpop-item[data-active='1']{background:var(--vela-active);}
@@ -31028,7 +31226,7 @@ var DrawingSettingsPopup = class {
31028
31226
  if (this.isOwnChrome(node)) return;
31029
31227
  const cb = this.onClose;
31030
31228
  this.close();
31031
- cb?.();
31229
+ cb?.(e);
31032
31230
  };
31033
31231
  this.theme = theme;
31034
31232
  this.settingsDialog = new DrawingSettingsDialog(host, theme);
@@ -31053,16 +31251,27 @@ var DrawingSettingsPopup = class {
31053
31251
  contains(node) {
31054
31252
  return node != null && (this.isOwnChrome(node) || this.settingsDialog.contains(node));
31055
31253
  }
31056
- /** Open the quick toolbar for `drawing`, floating clear of its `anchor` box.
31057
- * `onClose` fires on an outside-click dismissal (not a programmatic close). */
31058
- open(drawing, anchor, actions, onClose) {
31254
+ /** Open the quick toolbar for `drawings` (one, or a whole multi-selection), floating clear of
31255
+ * their `anchor` box. With several drawings the bar shows only the controls they ALL support,
31256
+ * a control whose values differ reads as mixed, and every edit applies to all of them —
31257
+ * per-drawing panels (levels, position sizing, the text field) stay single-drawing only.
31258
+ * `onClose` fires on a dismissal (an outside press, or Escape in the text field — not a
31259
+ * programmatic close), with the dismissing press when there is one, so the caller can tell a
31260
+ * modifier press (multi-select) from a plain one. */
31261
+ open(drawings, anchor, actions, onClose) {
31059
31262
  this.close();
31263
+ const drawing = drawings[0];
31264
+ if (!drawing) return;
31060
31265
  ensureStyles5();
31061
31266
  this.onClose = onClose ?? null;
31062
31267
  const t = this.theme;
31268
+ const multi = drawings.length > 1;
31063
31269
  const schema = drawing.schema();
31064
- const paths = new Set(schema.fields.map((f) => f.path));
31065
- const textOnBar = schema.textIsContent === true || !paths.has("text.value");
31270
+ const paths = sharedPaths(drawings);
31271
+ const common = (read) => commonValue(drawings, read);
31272
+ const every = (pred) => drawings.every(pred);
31273
+ const swatch = (tip, glyph, read, path) => this.colorButton(tip, glyph, common(read), (v) => actions.patch({ [path]: v }), void 0, distinctValues(drawings, read));
31274
+ const textOnBar = multi || schema.textIsContent === true || !paths.has("text.value");
31066
31275
  const el = document.createElement("div");
31067
31276
  el.className = "vela-dpop";
31068
31277
  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;`;
@@ -31076,75 +31285,84 @@ var DrawingSettingsPopup = class {
31076
31285
  bar.style.cssText = "display:flex;align-items:center;gap:2px;padding:4px;";
31077
31286
  bar.appendChild(this.dragHandle());
31078
31287
  if (paths.has("glyph")) {
31079
- const cur = drawing.glyph ?? GLYPH_OPTIONS[0];
31288
+ const cur = common((d) => d.glyph ?? GLYPH_OPTIONS[0]);
31080
31289
  bar.appendChild(this.dropdown("Icon", GLYPH_OPTIONS, cur, (g) => glyphIcon(g), (v) => actions.patch({ glyph: v })));
31081
31290
  }
31082
31291
  if (paths.has("size")) {
31083
- const sz = drawing.size ?? "normal";
31292
+ const sz = common((d) => d.size ?? "normal");
31084
31293
  bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
31085
31294
  }
31086
- if (paths.has("magnifier.timeframe") && drawing instanceof Magnifier) {
31295
+ if (paths.has("magnifier.timeframe") && every((d) => d instanceof Magnifier)) {
31296
+ const mag = (d) => d;
31087
31297
  const options = this.lowerTimeframeOptions();
31088
31298
  if (options.length > 0) {
31089
31299
  bar.appendChild(
31090
- this.dropdown("Lower timeframe", options.map((o) => o.value), drawing.magnifier.timeframe, () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
31300
+ this.dropdown("Lower timeframe", options.map((o) => o.value), common((d) => mag(d).magnifier.timeframe), () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
31091
31301
  label: (v) => magnifierTimeframeLabel(String(v)),
31092
31302
  labelInTrigger: true
31093
31303
  })
31094
31304
  );
31095
31305
  }
31096
- bar.appendChild(this.colorButton("Up candles", BUCKET_ICON, drawing.magnifier.upColor || t.upColor, (v) => actions.patch({ "magnifier.upColor": v })));
31097
- bar.appendChild(this.colorButton("Down candles", BUCKET_ICON, drawing.magnifier.downColor || t.downColor, (v) => actions.patch({ "magnifier.downColor": v })));
31306
+ bar.appendChild(swatch("Up candles", BUCKET_ICON, (d) => mag(d).magnifier.upColor || t.upColor, "magnifier.upColor"));
31307
+ bar.appendChild(swatch("Down candles", BUCKET_ICON, (d) => mag(d).magnifier.downColor || t.downColor, "magnifier.downColor"));
31098
31308
  }
31099
- 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 })));
31309
+ 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"));
31100
31310
  if (paths.has("style.lineWidth")) {
31101
- const wf = schema.fields.find((f) => f.path === "style.lineWidth");
31102
- if (wf?.kind === "number" && (wf.min ?? 1) > 1) {
31103
- bar.appendChild(this.widthInput("Line width", drawing.style.lineWidth, wf.min ?? 1, wf.max ?? 60, wf.step ?? 1, (v) => actions.patch({ "style.lineWidth": v })));
31311
+ const widthField2 = (d) => d.schema().fields.find((f) => f.path === "style.lineWidth");
31312
+ const wf = widthField2(drawing);
31313
+ const width = common((d) => d.style.lineWidth);
31314
+ const markers = every((d) => {
31315
+ const f = widthField2(d);
31316
+ return f?.kind === "number" && (f.min ?? 1) > 1;
31317
+ });
31318
+ if (markers && wf?.kind === "number") {
31319
+ bar.appendChild(this.widthInput("Line width", width, wf.min ?? 1, wf.max ?? 60, wf.step ?? 1, (v) => actions.patch({ "style.lineWidth": v })));
31104
31320
  } else {
31105
- 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 }));
31321
+ 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 }));
31106
31322
  }
31107
31323
  }
31108
- 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 }));
31109
- 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 })));
31324
+ 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 }));
31325
+ if (paths.has("style.fillColor")) bar.appendChild(swatch("Fill", BUCKET_ICON, (d) => effectiveFillColor(d, this.theme) ?? d.style.fillColor ?? DEFAULT_DRAWING_COLOR, "style.fillColor"));
31110
31326
  const isFrvp = paths.has("frvp.rows") && drawing instanceof FixedRangeVolumeProfile;
31111
- const isPosition = paths.has("riskPercent") && drawing instanceof PositionTool;
31327
+ const isPosition = paths.has("riskPercent") && every((d) => d instanceof PositionTool);
31112
31328
  if (isPosition) {
31113
- const pos = drawing;
31114
- bar.appendChild(this.colorButton("Profit zone", BUCKET_ICON, pos.profitColor, (v) => actions.patch({ profitColor: v })));
31115
- bar.appendChild(this.colorButton("Loss zone", BUCKET_ICON, pos.lossColor, (v) => actions.patch({ lossColor: v })));
31329
+ const pos = (d) => d;
31330
+ bar.appendChild(swatch("Profit zone", BUCKET_ICON, (d) => pos(d).profitColor, "profitColor"));
31331
+ bar.appendChild(swatch("Loss zone", BUCKET_ICON, (d) => pos(d).lossColor, "lossColor"));
31116
31332
  }
31117
- const reg = drawing.reg;
31118
- if (paths.has("reg.midColor") && reg) {
31333
+ const regOf = (d) => d.reg;
31334
+ if (paths.has("reg.midColor") && every((d) => regOf(d) != null)) {
31335
+ const reg = (d) => regOf(d);
31119
31336
  const styles = LINE_STYLE_OPTIONS.map((o) => o.value);
31120
- bar.appendChild(this.colorButton("Midline color", BRUSH_ICON, reg.midColor, (v) => actions.patch({ "reg.midColor": v })));
31121
- bar.appendChild(this.dropdown("Midline style", styles, reg.midStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.midStyle": v }), { label: styleLabel2 }));
31122
- bar.appendChild(this.colorButton("Upper line color", BRUSH_ICON, reg.upperColor, (v) => actions.patch({ "reg.upperColor": v })));
31123
- bar.appendChild(this.dropdown("Upper line style", styles, reg.upperStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.upperStyle": v }), { label: styleLabel2 }));
31124
- bar.appendChild(this.colorButton("Lower line color", BRUSH_ICON, reg.lowerColor, (v) => actions.patch({ "reg.lowerColor": v })));
31125
- bar.appendChild(this.dropdown("Lower line style", styles, reg.lowerStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.lowerStyle": v }), { label: styleLabel2 }));
31126
- bar.appendChild(this.colorButton("Upper fill", BUCKET_ICON, reg.upperFill, (v) => actions.patch({ "reg.upperFill": v })));
31127
- bar.appendChild(this.colorButton("Lower fill", BUCKET_ICON, reg.lowerFill, (v) => actions.patch({ "reg.lowerFill": v })));
31128
- bar.appendChild(this.toggle("Show R\xB2", R2_ICON, reg.showR2, (v) => actions.patch({ "reg.showR2": v })));
31129
- }
31130
- const vwap2 = drawing.vwap;
31131
- if (paths.has("vwap.midColor") && vwap2) {
31337
+ bar.appendChild(swatch("Midline color", BRUSH_ICON, (d) => reg(d).midColor, "reg.midColor"));
31338
+ bar.appendChild(this.dropdown("Midline style", styles, common((d) => reg(d).midStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "reg.midStyle": v }), { label: styleLabel2 }));
31339
+ bar.appendChild(swatch("Upper line color", BRUSH_ICON, (d) => reg(d).upperColor, "reg.upperColor"));
31340
+ 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 }));
31341
+ bar.appendChild(swatch("Lower line color", BRUSH_ICON, (d) => reg(d).lowerColor, "reg.lowerColor"));
31342
+ 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 }));
31343
+ bar.appendChild(swatch("Upper fill", BUCKET_ICON, (d) => reg(d).upperFill, "reg.upperFill"));
31344
+ bar.appendChild(swatch("Lower fill", BUCKET_ICON, (d) => reg(d).lowerFill, "reg.lowerFill"));
31345
+ bar.appendChild(this.toggle("Show R\xB2", R2_ICON, common((d) => reg(d).showR2), (v) => actions.patch({ "reg.showR2": v })));
31346
+ }
31347
+ const vwapOf = (d) => d.vwap;
31348
+ if (paths.has("vwap.midColor") && every((d) => vwapOf(d) != null)) {
31349
+ const vwap2 = (d) => vwapOf(d);
31132
31350
  const styles = LINE_STYLE_OPTIONS.map((o) => o.value);
31133
31351
  const MULTS = [0.5, 1, 1.5, 2, 2.5, 3, 4, 5];
31134
- bar.appendChild(this.colorButton("VWAP color", BRUSH_ICON, vwap2.midColor, (v) => actions.patch({ "vwap.midColor": v })));
31135
- bar.appendChild(this.dropdown("VWAP style", styles, vwap2.midStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "vwap.midStyle": v }), { label: styleLabel2 }));
31352
+ bar.appendChild(swatch("VWAP color", BRUSH_ICON, (d) => vwap2(d).midColor, "vwap.midColor"));
31353
+ bar.appendChild(this.dropdown("VWAP style", styles, common((d) => vwap2(d).midStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "vwap.midStyle": v }), { label: styleLabel2 }));
31136
31354
  bar.appendChild(
31137
- this.dropdown("Band multiplier", MULTS, vwap2.multiplier, () => BANDS_ICON, (v) => actions.patch({ "vwap.multiplier": v }), {
31355
+ this.dropdown("Band multiplier", MULTS, common((d) => vwap2(d).multiplier), () => BANDS_ICON, (v) => actions.patch({ "vwap.multiplier": v }), {
31138
31356
  label: (v) => `${v}\u03C3`,
31139
31357
  labelInTrigger: true
31140
31358
  })
31141
31359
  );
31142
- bar.appendChild(this.colorButton("Upper band color", BRUSH_ICON, vwap2.upperColor, (v) => actions.patch({ "vwap.upperColor": v })));
31143
- bar.appendChild(this.colorButton("Lower band color", BRUSH_ICON, vwap2.lowerColor, (v) => actions.patch({ "vwap.lowerColor": v })));
31144
- bar.appendChild(this.colorButton("Band fill", BUCKET_ICON, vwap2.bandFill, (v) => actions.patch({ "vwap.bandFill": v })));
31360
+ bar.appendChild(swatch("Upper band color", BRUSH_ICON, (d) => vwap2(d).upperColor, "vwap.upperColor"));
31361
+ bar.appendChild(swatch("Lower band color", BRUSH_ICON, (d) => vwap2(d).lowerColor, "vwap.lowerColor"));
31362
+ bar.appendChild(swatch("Band fill", BUCKET_ICON, (d) => vwap2(d).bandFill, "vwap.bandFill"));
31145
31363
  }
31146
31364
  if (paths.has("maxCurvature")) {
31147
- const cur = drawing.maxCurvature ?? 24;
31365
+ const cur = common((d) => d.maxCurvature ?? 24);
31148
31366
  bar.appendChild(
31149
31367
  this.dropdown("Max curvature", DEDEKIND_CURVATURE_OPTIONS, cur, () => DEDEKIND_ICON2, (v) => actions.patch({ maxCurvature: v }), {
31150
31368
  label: (v) => `n=${v}`,
@@ -31153,7 +31371,7 @@ var DrawingSettingsPopup = class {
31153
31371
  );
31154
31372
  }
31155
31373
  if (paths.has("mach")) {
31156
- const cur = drawing.mach ?? 2;
31374
+ const cur = common((d) => d.mach ?? 2);
31157
31375
  bar.appendChild(
31158
31376
  this.dropdown("Mach number", MACH_NUMBER_OPTIONS, cur, () => SUPERSONIC_ICON2, (v) => actions.patch({ mach: v }), {
31159
31377
  label: (v) => `M=${v}`,
@@ -31162,7 +31380,7 @@ var DrawingSettingsPopup = class {
31162
31380
  );
31163
31381
  }
31164
31382
  if (paths.has("waveCount")) {
31165
- const cur = drawing.waveCount ?? 6;
31383
+ const cur = common((d) => d.waveCount ?? 6);
31166
31384
  bar.appendChild(
31167
31385
  this.dropdown("Waves", MACH_WAVE_COUNT_OPTIONS, cur, () => SONIC_ICON2, (v) => actions.patch({ waveCount: v }), {
31168
31386
  label: (v) => `${v}`,
@@ -31170,26 +31388,27 @@ var DrawingSettingsPopup = class {
31170
31388
  })
31171
31389
  );
31172
31390
  }
31173
- const toggles = drawing;
31174
- if (paths.has("showPrice")) bar.appendChild(this.toggle("Show price", PRICE_DELTA_ICON, toggles.showPrice !== false, (v) => actions.patch({ showPrice: v })));
31175
- if (paths.has("showDate")) bar.appendChild(this.toggle("Show date", DATE_DELTA_ICON, toggles.showDate !== false, (v) => actions.patch({ showDate: v })));
31176
- 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 })));
31177
- 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 }));
31178
- 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 })));
31179
- 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 })));
31180
- if (paths.has("text.value")) bar.appendChild(this.iconBtn("Text", TYPE_ICON, () => this.toggleTextPanel(drawing, actions, !textOnBar)));
31391
+ const flags = (d) => d;
31392
+ if (paths.has("showPrice")) bar.appendChild(this.toggle("Show price", PRICE_DELTA_ICON, common((d) => flags(d).showPrice !== false), (v) => actions.patch({ showPrice: v })));
31393
+ if (paths.has("showDate")) bar.appendChild(this.toggle("Show date", DATE_DELTA_ICON, common((d) => flags(d).showDate !== false), (v) => actions.patch({ showDate: v })));
31394
+ if (paths.has("text.color") && textOnBar) bar.appendChild(swatch("Text color", TYPE_ICON, (d) => d.text?.color || t.textColor, "text.color"));
31395
+ 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 }));
31396
+ const textField = !multi && paths.has("text.value");
31397
+ if (paths.has("text.bold") && !textField) bar.appendChild(this.toggle("Bold", BOLD_ICON, common((d) => !!d.text?.bold), (v) => actions.patch({ "text.bold": v })));
31398
+ if (paths.has("text.italic") && !textField) bar.appendChild(this.toggle("Italic", ITALIC_ICON, common((d) => !!d.text?.italic), (v) => actions.patch({ "text.italic": v })));
31399
+ if (textField) bar.appendChild(this.iconBtn("Text", TYPE_ICON, () => this.toggleTextPanel(drawing, actions, !textOnBar)));
31181
31400
  const editableLevels = drawing.editableLevels();
31182
- if (editableLevels && !(drawing instanceof MachFigure)) {
31183
- const fib = drawing;
31401
+ if (editableLevels && every((d) => d.editableLevels() != null && !(d instanceof MachFigure))) {
31402
+ const fib = (d) => d;
31184
31403
  const sizes = TEXT_SIZE_OPTIONS.map((o) => o.value);
31185
- bar.appendChild(this.dropdown("Numbers size", sizes, fib.numbersSize ?? "small", (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel }));
31186
- bar.appendChild(this.dropdown("Label size", sizes, fib.labelsSize ?? "normal", (s) => labelSizeIcon(s), (v) => actions.patch({ labelsSize: v }), { label: sizeLabel }));
31404
+ bar.appendChild(this.dropdown("Numbers size", sizes, common((d) => fib(d).numbersSize ?? "small"), (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel }));
31405
+ bar.appendChild(this.dropdown("Label size", sizes, common((d) => fib(d).labelsSize ?? "normal"), (s) => labelSizeIcon(s), (v) => actions.patch({ labelsSize: v }), { label: sizeLabel }));
31187
31406
  }
31188
31407
  bar.appendChild(this.divider());
31189
- if (isFrvp) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "frvp")));
31190
- if (isPosition) bar.appendChild(this.iconBtn("Position size", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "position")));
31191
- if (editableLevels) bar.appendChild(this.iconBtn("Levels", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "levels")));
31192
- bar.appendChild(this.toggle("Lock", LOCK_ICON, drawing.locked, (v) => actions.setLocked(v)));
31408
+ if (!multi && isFrvp) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "frvp")));
31409
+ if (!multi && isPosition) bar.appendChild(this.iconBtn("Position size", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "position")));
31410
+ if (!multi && editableLevels) bar.appendChild(this.iconBtn("Levels", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "levels")));
31411
+ bar.appendChild(this.toggle("Lock", LOCK_ICON, common((d) => d.locked), (v) => actions.setLocked(v), UNLOCK_ICON));
31193
31412
  const del = this.iconBtn("Delete", TRASH_ICON, () => actions.remove());
31194
31413
  del.style.color = "var(--vela-danger)";
31195
31414
  bar.appendChild(del);
@@ -31375,7 +31594,7 @@ var DrawingSettingsPopup = class {
31375
31594
  });
31376
31595
  return b;
31377
31596
  }
31378
- /** The trailing kebab overflow — z-order actions + reset settings. */
31597
+ /** The trailing kebab overflow — duplicate, z-order actions, reset settings. */
31379
31598
  kebabButton(actions) {
31380
31599
  const b = this.base("More");
31381
31600
  b.innerHTML = sized(KEBAB_ICON);
@@ -31387,6 +31606,7 @@ var DrawingSettingsPopup = class {
31387
31606
  return;
31388
31607
  }
31389
31608
  this.openActionMenu(b, [
31609
+ { icon: CLONE_ICON, label: "Duplicate", onClick: () => actions.duplicate() },
31390
31610
  { icon: FRONT_ICON, label: "Bring to front", onClick: () => actions.reorder("front") },
31391
31611
  { icon: BACK_ICON, label: "Send to back", onClick: () => actions.reorder("back") },
31392
31612
  { icon: RESET_ICON, label: "Reset settings", onClick: () => actions.resetSettings() }
@@ -31543,26 +31763,30 @@ var DrawingSettingsPopup = class {
31543
31763
  this.tipEl?.remove();
31544
31764
  this.tipEl = null;
31545
31765
  }
31546
- toggle(tip, icon2, active, onChange) {
31766
+ /** An on/off button. A mixed state (the selected drawings disagree) reads as half-lit and
31767
+ * the first click turns it ON for all of them. `off` swaps in a distinct glyph while the
31768
+ * toggle is off (a lock reads as an open padlock until it is engaged); mixed shows `icon`. */
31769
+ toggle(tip, icon2, active, onChange, off) {
31547
31770
  const b = this.base(tip);
31548
- b.innerHTML = sized(icon2);
31549
31771
  const set = (on2) => {
31550
- b.dataset.active = on2 ? "1" : "0";
31772
+ b.dataset.active = on2 === MIXED ? "mixed" : on2 ? "1" : "0";
31773
+ b.innerHTML = sized(off && on2 === false ? off : icon2);
31551
31774
  };
31552
31775
  set(active);
31553
31776
  let on = active;
31554
31777
  b.addEventListener("click", () => {
31555
- on = !on;
31778
+ on = on === MIXED ? true : !on;
31556
31779
  set(on);
31557
31780
  onChange(on);
31558
31781
  });
31559
31782
  return b;
31560
31783
  }
31561
31784
  /** An inline numeric width field for tools whose stroke range outgrows the 1–4px
31562
- * ladder — the value is clamped to the schema's declared min/max on commit. */
31785
+ * ladder — the value is clamped to the schema's declared min/max on commit. A mixed
31786
+ * value shows an empty field with a dash until a number is typed for all. */
31563
31787
  widthInput(tip, value, min, max, step, onChange) {
31564
31788
  const ni = new NumberInput({
31565
- value,
31789
+ value: value === MIXED ? min : value,
31566
31790
  min,
31567
31791
  max,
31568
31792
  step,
@@ -31574,13 +31798,18 @@ var DrawingSettingsPopup = class {
31574
31798
  title: tip,
31575
31799
  onChange
31576
31800
  });
31801
+ if (value === MIXED) {
31802
+ ni.input.value = "";
31803
+ ni.input.placeholder = "\u2014";
31804
+ }
31577
31805
  ni.el.dataset.tip = tip;
31578
31806
  trapChartKeys(ni.el);
31579
31807
  return ni.el;
31580
31808
  }
31581
31809
  /** A pick-one dropdown: the trigger shows the current value's glyph (plus an optional inline
31582
31810
  * text label — e.g. `2px` — for controls that would otherwise look alike), and clicking it
31583
- * opens a floating list of every option so a value is one click away (no cycling through). */
31811
+ * opens a floating list of every option so a value is one click away (no cycling through).
31812
+ * A mixed value shows a dash in the trigger and highlights no row. */
31584
31813
  dropdown(tip, values, current, render, onChange, opts = {}) {
31585
31814
  const b = this.base(tip);
31586
31815
  b.style.width = "auto";
@@ -31590,14 +31819,20 @@ var DrawingSettingsPopup = class {
31590
31819
  let cur = current;
31591
31820
  const paint = (v) => {
31592
31821
  b.replaceChildren();
31593
- const glyph = render(v);
31822
+ if (v === MIXED) {
31823
+ const dash = document.createElement("span");
31824
+ dash.textContent = "\u2014";
31825
+ dash.style.cssText = "min-width:18px;text-align:center;opacity:0.85;";
31826
+ b.appendChild(dash);
31827
+ }
31828
+ const glyph = v === MIXED ? "" : render(v);
31594
31829
  if (glyph) {
31595
31830
  const ic = document.createElement("span");
31596
31831
  ic.style.cssText = "display:flex;";
31597
31832
  ic.innerHTML = sized(glyph);
31598
31833
  b.appendChild(ic);
31599
31834
  }
31600
- if (opts.label && opts.labelInTrigger) {
31835
+ if (opts.label && opts.labelInTrigger && v !== MIXED) {
31601
31836
  const tx = document.createElement("span");
31602
31837
  tx.textContent = opts.label(v);
31603
31838
  tx.style.cssText = "font-size:11px;opacity:0.85;font-variant-numeric:tabular-nums;";
@@ -31667,8 +31902,10 @@ var DrawingSettingsPopup = class {
31667
31902
  }
31668
31903
  /** A color control: an `icon` over a thin **colored underline** showing the current
31669
31904
  * value (the common idiom), with an invisible native picker overlaid to edit it.
31670
- * `iconSize` shrinks it for the floating text controls. */
31671
- colorButton(tip, icon2, color, onChange, iconSize = 17) {
31905
+ * `iconSize` shrinks it for the floating text controls. A mixed color paints the
31906
+ * underline striped with the colors `used` across the selection, and the picker then
31907
+ * leads with those colors so unifying onto one of them is a single click. */
31908
+ colorButton(tip, icon2, color, onChange, iconSize = 17, used = []) {
31672
31909
  const b = document.createElement("button");
31673
31910
  b.type = "button";
31674
31911
  b.dataset.tip = tip;
@@ -31678,13 +31915,14 @@ var DrawingSettingsPopup = class {
31678
31915
  ic.style.cssText = "display:flex;";
31679
31916
  ic.innerHTML = sized(icon2, iconSize);
31680
31917
  const bar = document.createElement("span");
31681
- bar.style.cssText = `display:block;height:3px;width:${Math.round(iconSize * 0.85)}px;border-radius:2px;background:${color};`;
31918
+ bar.style.cssText = `display:block;height:3px;width:${Math.round(iconSize * 0.85)}px;border-radius:2px;background:${color === MIXED ? stripes(used) : color};`;
31682
31919
  b.append(ic, bar);
31683
31920
  let cur = color;
31684
31921
  b.addEventListener("pointerdown", (e) => e.stopPropagation());
31685
31922
  b.addEventListener("click", (e) => {
31686
31923
  e.stopPropagation();
31687
- this.toggleColorPopover(b, cur, (v) => {
31924
+ const seed = cur === MIXED ? used[0] ?? DEFAULT_DRAWING_COLOR : cur;
31925
+ this.toggleColorPopover(b, seed, cur === MIXED ? used : [], (v) => {
31688
31926
  cur = v;
31689
31927
  bar.style.background = v;
31690
31928
  onChange(v);
@@ -31693,20 +31931,42 @@ var DrawingSettingsPopup = class {
31693
31931
  return b;
31694
31932
  }
31695
31933
  /** Open the color popover for `anchor`, or close it if it's already this anchor's (toggle). */
31696
- toggleColorPopover(anchor, color, onChange) {
31934
+ toggleColorPopover(anchor, color, used, onChange) {
31697
31935
  if (this.colorOwner === anchor) {
31698
31936
  this.closeColorPopover();
31699
31937
  return;
31700
31938
  }
31701
- this.openColorPopover(anchor, color, onChange);
31939
+ this.openColorPopover(anchor, color, used, onChange);
31702
31940
  }
31703
- /** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`. */
31704
- openColorPopover(anchor, color, onChange) {
31941
+ /** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`.
31942
+ * `used` (a mixed swatch's colors) leads the picker as one-click unify shortcuts. */
31943
+ openColorPopover(anchor, color, used, onChange) {
31705
31944
  const t = this.theme;
31706
31945
  this.colorPop = this.hostFloat(anchor, {
31707
31946
  zIndex: 25,
31708
31947
  padding: "10px",
31709
- fill: (el) => {
31948
+ fill: (el, pop) => {
31949
+ if (used.length > 1) {
31950
+ const row = document.createElement("div");
31951
+ row.style.cssText = "display:flex;align-items:center;gap:6px;padding-bottom:9px;border-bottom:1px solid var(--vela-border);";
31952
+ const lbl = document.createElement("span");
31953
+ lbl.textContent = "In use";
31954
+ lbl.style.cssText = "font:var(--vela-font-size-sm) inherit;opacity:0.7;margin-right:2px;";
31955
+ row.appendChild(lbl);
31956
+ for (const c of used) {
31957
+ const sw = document.createElement("button");
31958
+ sw.type = "button";
31959
+ sw.dataset.tip = c;
31960
+ sw.style.cssText = `width:18px;height:18px;border-radius:4px;border:1px solid var(--vela-border-strong);cursor:pointer;background:${c};padding:0;`;
31961
+ sw.addEventListener("click", (e) => {
31962
+ e.stopPropagation();
31963
+ pop.hide();
31964
+ onChange(c);
31965
+ });
31966
+ row.appendChild(sw);
31967
+ }
31968
+ el.appendChild(row);
31969
+ }
31710
31970
  el.appendChild(buildColorPicker(color, t, onChange));
31711
31971
  }
31712
31972
  });
@@ -31727,6 +31987,7 @@ var TYPE_ICON = icon("type");
31727
31987
  var PRICE_DELTA_ICON = icon("price-delta");
31728
31988
  var DATE_DELTA_ICON = icon("date-delta");
31729
31989
  var LOCK_ICON = icon("lock");
31990
+ var UNLOCK_ICON = icon("unlock");
31730
31991
  var FRONT_ICON = icon("bring-front");
31731
31992
  var BACK_ICON = icon("send-back");
31732
31993
  var TRASH_ICON = icon("trash");
@@ -31734,6 +31995,7 @@ var BOLD_ICON = icon("bold");
31734
31995
  var ITALIC_ICON = icon("italic");
31735
31996
  var GRIP_ICON = icon("grip");
31736
31997
  var KEBAB_ICON = icon("kebab");
31998
+ var CLONE_ICON = icon("clone");
31737
31999
  var RESET_ICON = icon("reset");
31738
32000
  var GEAR_ICON = icon("gear");
31739
32001
  var CHEVRON_ICON = icon("chevron-down");
@@ -31742,6 +32004,12 @@ var BANDS_ICON = icon("bands");
31742
32004
  var DEDEKIND_ICON2 = icon("dedekind");
31743
32005
  var SONIC_ICON2 = icon("sonic");
31744
32006
  var SUPERSONIC_ICON2 = icon("supersonic");
32007
+ function stripes(colors) {
32008
+ const bands = colors.length > 1 ? colors : ["var(--vela-fg-faint)", "var(--vela-fg-muted)"];
32009
+ const step = 100 / bands.length;
32010
+ const stops = bands.map((c, i) => `${c} ${i * step}% ${(i + 1) * step}%`).join(",");
32011
+ return `linear-gradient(90deg,${stops})`;
32012
+ }
31745
32013
  function lineIcon(width, style) {
31746
32014
  const dash = style === "dashed" ? "5,3" : style === "dotted" ? "1.5,3" : "";
31747
32015
  return svg24(`<line x1="3.5" y1="12" x2="20.5" y2="12" stroke-width="${Number(width) || 2}" stroke-dasharray="${dash}"/>`);
@@ -32025,6 +32293,20 @@ var UserDrawingController = class {
32025
32293
  // reported exactly when it actually changes).
32026
32294
  this.textEditor = null;
32027
32295
  this.painter = new DrawingPainter();
32296
+ /** The open popup is the multi-selection bar (vs one drawing's own) — decides what a
32297
+ * shrinking selection does with it. */
32298
+ this.popupMulti = false;
32299
+ /** Dismiss-on-outside-press also clears the selection — except when the press is ABOUT the
32300
+ * selection: it carries a multi-select modifier (Shift / Ctrl / Cmd — it will add to the
32301
+ * selection or sweep a marquee onto it), or it landed on a drawing that is already selected
32302
+ * (the canvas handler ran first and is holding it for a drag or a click), so the drawings
32303
+ * whose bar just closed must stay selected. */
32304
+ this.onPopupDismissed = (e) => {
32305
+ if (e && (e.shiftKey || e.ctrlKey || e.metaKey)) return;
32306
+ const pressed = this.interaction.pressedId();
32307
+ if (pressed && this.selectedIds.has(pressed)) return;
32308
+ this.clearSelection();
32309
+ };
32028
32310
  this.ctx = canvas.getContext("2d");
32029
32311
  this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), () => deps.chartBarMs());
32030
32312
  this.toolbar = new DrawingToolbar(
@@ -32201,6 +32483,12 @@ var UserDrawingController = class {
32201
32483
  }
32202
32484
  setSelection(ids) {
32203
32485
  this.selectedIds = new Set(ids);
32486
+ if (ids.length >= 2) this.openSettingsForSelection(ids);
32487
+ else if (this.popupMulti) {
32488
+ this.popupMulti = false;
32489
+ this.popup.close();
32490
+ if (ids.length === 1) this.openSettingsById(ids[0], 0, 0);
32491
+ }
32204
32492
  this.render();
32205
32493
  }
32206
32494
  /** Programmatic twin of clicking the drawing: highlight it + float its settings popup. */
@@ -32246,12 +32534,16 @@ var UserDrawingController = class {
32246
32534
  });
32247
32535
  }
32248
32536
  /** Delete the (unlocked) drawing under the cursor. True when one was removed.
32249
- * Shared by the eraser (click + drag) and the middle-click shortcut. */
32250
- deleteAt(x, y) {
32537
+ * Shared by the eraser (click + drag) and the middle-click shortcut; the latter passes
32538
+ * `withSelection` so a hit on a member of a multi-selection removes the selection's
32539
+ * unlocked members — whichever member was hit, locked or not. */
32540
+ deleteAt(x, y, withSelection = false) {
32251
32541
  const hit = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE);
32252
- if (!hit || hit.locked) return false;
32542
+ if (!hit) return false;
32543
+ const ids = deleteTargets(hit, this.selectedIds, this.drawings, withSelection);
32544
+ if (ids.length === 0) return false;
32253
32545
  this.popup.close();
32254
- this.emit({ kind: "delete", ids: [hit.id] });
32546
+ this.emit({ kind: "delete", ids });
32255
32547
  return true;
32256
32548
  }
32257
32549
  /** Shift+press on the empty plot: arm the measure ruler AND start it at (x, y) in one
@@ -32277,7 +32569,14 @@ var UserDrawingController = class {
32277
32569
  this.render();
32278
32570
  }
32279
32571
  }
32280
- pointerDown(x, y, snap = "off", shift2 = false) {
32572
+ /** Ctrl/Cmd+press on the empty plot: sweep a selection box (adds the drawings it touches to
32573
+ * the selection). Returns false when a mode/tool is active (the normal press path owns it). */
32574
+ beginMarqueeAt(x, y) {
32575
+ if (this.measureMode || this.eraserMode || !this.interaction.beginMarquee(x, y)) return false;
32576
+ this.render();
32577
+ return true;
32578
+ }
32579
+ pointerDown(x, y, snap = "off", shift2 = false, mod = false) {
32281
32580
  if (this.eraserMode) {
32282
32581
  this.erasing = true;
32283
32582
  this.deleteAt(x, y);
@@ -32297,9 +32596,9 @@ var UserDrawingController = class {
32297
32596
  return;
32298
32597
  }
32299
32598
  }
32300
- this.interaction.down(x, y, snap, shift2);
32599
+ this.interaction.down(x, y, snap, shift2, mod);
32301
32600
  }
32302
- pointerMove(x, y, snap = "off", shift2 = false) {
32601
+ pointerMove(x, y, snap = "off", shift2 = false, mod = false) {
32303
32602
  if (this.eraserMode) {
32304
32603
  if (this.erasing) this.deleteAt(x, y);
32305
32604
  return;
@@ -32311,12 +32610,14 @@ var UserDrawingController = class {
32311
32610
  return;
32312
32611
  }
32313
32612
  this.interaction.move(x, y, snap, shift2);
32314
- this.updateHover(x, y);
32613
+ this.updateHover(x, y, mod);
32315
32614
  }
32316
- /** Track which drawing is hovered so its handles appear on hover (and only then). */
32317
- updateHover(x, y) {
32615
+ /** Track which drawing is hovered so its handles appear on hover (and only then). While
32616
+ * Ctrl/Cmd is held the cursor is picking a selection — handles then mark only what IS
32617
+ * selected, so a hovered candidate doesn't already read as selected. */
32618
+ updateHover(x, y, mod = false) {
32318
32619
  let id = null;
32319
- if (this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
32620
+ if (!mod && this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
32320
32621
  id = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE)?.id ?? null;
32321
32622
  }
32322
32623
  if (id !== this.hoveredId) {
@@ -32336,7 +32637,12 @@ var UserDrawingController = class {
32336
32637
  this.render();
32337
32638
  return;
32338
32639
  }
32640
+ const dragged = this.interaction.isDragging() ? this.interaction.pressedId() : null;
32339
32641
  this.interaction.up(x, y);
32642
+ if (dragged && this.selectedIds.has(dragged) && !this.popup.isOpen()) {
32643
+ if (this.selectedIds.size >= 2) this.openSettingsForSelection(this.selectionIds());
32644
+ else this.openSettingsById(dragged, x, y);
32645
+ }
32340
32646
  }
32341
32647
  /** Toggle the transient ruler. Arming it clears any drawing tool + selection. */
32342
32648
  toggleMeasure() {
@@ -32601,7 +32907,7 @@ var UserDrawingController = class {
32601
32907
  this.emit({ kind: "duplicate", ids: this.selectionIds() });
32602
32908
  break;
32603
32909
  case "delete": {
32604
- const ids = this.selectedIds.size ? this.selectionIds() : this.hoveredId ? [this.hoveredId] : [];
32910
+ const ids = this.selectedIds.size ? this.deletableSelection() : this.hoveredId ? [this.hoveredId] : [];
32605
32911
  if (ids.length) {
32606
32912
  this.popup.close();
32607
32913
  this.emit({ kind: "delete", ids });
@@ -32617,6 +32923,9 @@ var UserDrawingController = class {
32617
32923
  selectionIds() {
32618
32924
  return [...this.selectedIds];
32619
32925
  }
32926
+ deletableSelection() {
32927
+ return deletableSelection(this.selectedIds, this.drawings);
32928
+ }
32620
32929
  /** Move every selected (unlocked) drawing by a pixel delta — one edit/edit-many → one undo step. */
32621
32930
  nudgeSelection(dx, dy) {
32622
32931
  const proj = this.deps.projector();
@@ -32725,6 +33034,8 @@ var UserDrawingController = class {
32725
33034
  this.painter.seriesLook = this.deps.seriesLook();
32726
33035
  this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
32727
33036
  this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
33037
+ const clones = this.interaction.dragClones();
33038
+ if (clones) this.painter.paintAll(ctx, clones, proj, this.deps.theme(), { selected: new Set(clones.map((c) => c.id)) });
32728
33039
  this.layoutTextEditor();
32729
33040
  const ghost = this.interaction.ghost();
32730
33041
  if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
@@ -32740,6 +33051,8 @@ var UserDrawingController = class {
32740
33051
  const my = m ? proj.yOf(m.point.price, m.paneId) : null;
32741
33052
  if (m && my != null) this.painter.paintSnapRing(ctx, proj.xOf(m.point.time), my, this.deps.theme());
32742
33053
  if (this.measure.isActive()) this.measure.paint(ctx, proj, this.deps.theme());
33054
+ const marquee = this.interaction.marqueeRect();
33055
+ if (marquee) this.painter.paintMarquee(ctx, marquee);
32743
33056
  }
32744
33057
  destroy() {
32745
33058
  this.closeTextEditor();
@@ -32757,15 +33070,80 @@ var UserDrawingController = class {
32757
33070
  if (this.selectedIds.size) this.emit({ kind: "select", ids: [] });
32758
33071
  this.render();
32759
33072
  }
33073
+ /** Report the live drawings' current state as one edit (one undo step, however many). */
33074
+ emitEdits(drawings) {
33075
+ const docs = drawings.map((d) => d.serialize());
33076
+ if (docs.length === 1) this.emit({ kind: "edit", doc: docs[0] });
33077
+ else if (docs.length > 1) this.emit({ kind: "edit-many", docs });
33078
+ }
33079
+ /** The one bar for a multi-selection: it shows the controls all of its drawings share and
33080
+ * every action applies to all of them. Floats clear of their combined bounds. */
33081
+ openSettingsForSelection(ids) {
33082
+ const live = () => ids.map((id) => this.drawings.find((d) => d.id === id)).filter((d) => d != null);
33083
+ const drawings = live();
33084
+ if (drawings.length < 2) return;
33085
+ const proj = this.deps.projector();
33086
+ let anchor = null;
33087
+ for (const d of drawings) {
33088
+ const b = d.bounds(proj);
33089
+ if (!b) continue;
33090
+ if (!anchor) anchor = { ...b };
33091
+ else {
33092
+ const right = Math.max(anchor.x + anchor.w, b.x + b.w);
33093
+ const bottom = Math.max(anchor.y + anchor.h, b.y + b.h);
33094
+ anchor.x = Math.min(anchor.x, b.x);
33095
+ anchor.y = Math.min(anchor.y, b.y);
33096
+ anchor.w = right - anchor.x;
33097
+ anchor.h = bottom - anchor.y;
33098
+ }
33099
+ }
33100
+ this.popupMulti = true;
33101
+ this.emit({ kind: "settings", id: drawings[0].id });
33102
+ this.popup.open(drawings, anchor, {
33103
+ resolve: () => live()[0] ?? null,
33104
+ patch: (p) => {
33105
+ const ds = live();
33106
+ for (const d of ds) d.applySettings(p);
33107
+ this.render();
33108
+ this.emitEdits(ds);
33109
+ },
33110
+ setLocked: (v) => {
33111
+ const ds = live();
33112
+ for (const d of ds) d.locked = v;
33113
+ this.emitEdits(ds);
33114
+ },
33115
+ reorder: (to) => {
33116
+ for (const d of live()) this.emit({ kind: "reorder", id: d.id, to });
33117
+ },
33118
+ duplicate: () => {
33119
+ this.popup.close();
33120
+ this.emit({ kind: "duplicate", ids: live().map((d) => d.id) });
33121
+ },
33122
+ resetSettings: () => {
33123
+ const ds = live();
33124
+ for (const d of ds) resetDrawingSettings(d);
33125
+ this.render();
33126
+ this.emitEdits(ds);
33127
+ this.openSettingsForSelection(ids);
33128
+ },
33129
+ remove: () => {
33130
+ const ids2 = live().filter((d) => !d.locked).map((d) => d.id);
33131
+ if (ids2.length === 0) return;
33132
+ this.popup.close();
33133
+ this.emit({ kind: "delete", ids: ids2 });
33134
+ }
33135
+ }, this.onPopupDismissed);
33136
+ }
32760
33137
  /** A click on a drawing opens its settings toolbar (text labels edit text here too). */
32761
33138
  openSettingsById(id, _x, _y) {
32762
33139
  const drawing = this.drawings.find((d) => d.id === id);
32763
33140
  if (!drawing) return;
32764
33141
  const live = () => this.drawings.find((d) => d.id === id);
33142
+ this.popupMulti = false;
32765
33143
  this.emit({ kind: "select", ids: [id] });
32766
33144
  this.emit({ kind: "settings", id });
32767
33145
  const anchor = drawing.bounds(this.deps.projector());
32768
- this.popup.open(drawing, anchor, {
33146
+ this.popup.open([drawing], anchor, {
32769
33147
  // Sync rebuilds instances, so a panel that reads values back after a patch (e.g. the
32770
33148
  // position tool's price fields, where one edit can flip another level) resolves fresh.
32771
33149
  resolve: () => live() ?? null,
@@ -32783,6 +33161,11 @@ var UserDrawingController = class {
32783
33161
  this.emit({ kind: "edit", doc: d.serialize() });
32784
33162
  },
32785
33163
  reorder: (to) => this.emit({ kind: "reorder", id, to }),
33164
+ duplicate: () => {
33165
+ this.finishTextEditor(true);
33166
+ this.popup.close();
33167
+ this.emit({ kind: "duplicate", ids: [id] });
33168
+ },
32786
33169
  resetSettings: () => {
32787
33170
  const d = live();
32788
33171
  if (!d) return;
@@ -32805,7 +33188,13 @@ var UserDrawingController = class {
32805
33188
  this.popup.close();
32806
33189
  this.emit({ kind: "delete", ids: [id] });
32807
33190
  }
32808
- }, () => this.clearSelection());
33191
+ }, this.onPopupDismissed);
33192
+ }
33193
+ /** A plain click on the empty plot: drop the selection. The popup's dismiss-on-outside-press
33194
+ * does the same, but a selection can exist with no bar open (a lone Ctrl-click pick, a
33195
+ * selection whose bar was dismissed by a modifier press) — this covers those. */
33196
+ deselect() {
33197
+ this.clearSelection();
32809
33198
  }
32810
33199
  /** Report placement progress upstream (`draft` intent) so the drawings sync can
32811
33200
  * mirror the ghost live on linked charts. Called from every render; only actual
@@ -32831,6 +33220,13 @@ var UserDrawingController = class {
32831
33220
  }, 0);
32832
33221
  return;
32833
33222
  }
33223
+ if (i.kind === "clone") {
33224
+ const before = new Set(this.drawings.map((d) => d.id));
33225
+ this.intentCb?.(i);
33226
+ const fresh = this.drawings.filter((d) => !before.has(d.id));
33227
+ if (fresh.length === 1) this.openSettingsById(fresh[0].id, 0, 0);
33228
+ return;
33229
+ }
32834
33230
  this.intentCb?.(i);
32835
33231
  }
32836
33232
  };
@@ -35006,7 +35402,10 @@ var NativeRenderer = class {
35006
35402
  zoomTo: (target, anchorLogical, anchorX) => this.zoomTo(target, anchorLogical, anchorX),
35007
35403
  fling: (v) => this.fling(v),
35008
35404
  onPointerMove: (x, y) => this.handlePointerMove(x, y),
35009
- onClick: (x) => this.handleClick(x),
35405
+ onClick: (x) => {
35406
+ this.userDrawings?.deselect();
35407
+ this.handleClick(x);
35408
+ },
35010
35409
  onAxisLongPress: (axis, x, y) => {
35011
35410
  for (const cb of this.axisLongPressCbs) cb({ axis, x, y });
35012
35411
  },
@@ -35024,11 +35423,13 @@ var NativeRenderer = class {
35024
35423
  // User drawings claim a gesture before pan when armed / over a drawing.
35025
35424
  drawingsClaim: (x, y) => this.userDrawings?.claim(x, y) ?? false,
35026
35425
  drawingsMeasureStart: (x, y, snap) => this.userDrawings?.beginMeasureAt(x, y, snap) ?? false,
35027
- drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y) ?? false,
35426
+ drawingsMarqueeStart: (x, y) => this.userDrawings?.beginMarqueeAt(x, y) ?? false,
35427
+ drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y, true) ?? false,
35428
+ // middle-click: a selected hit takes the whole selection
35028
35429
  drawingsCancelPlacement: () => this.userDrawings?.cancelPlacement() ?? false,
35029
35430
  drawingsSnapMode: () => this.snapMode,
35030
- drawingsPointerDown: (x, y, snap, shift2) => this.userDrawings?.pointerDown(x, y, snap, shift2),
35031
- drawingsPointerMove: (x, y, snap, shift2) => this.userDrawings?.pointerMove(x, y, snap, shift2),
35431
+ drawingsPointerDown: (x, y, snap, shift2, mod) => this.userDrawings?.pointerDown(x, y, snap, shift2, mod),
35432
+ drawingsPointerMove: (x, y, snap, shift2, mod) => this.userDrawings?.pointerMove(x, y, snap, shift2, mod),
35032
35433
  drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
35033
35434
  drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
35034
35435
  drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
@@ -37362,6 +37763,10 @@ function str3(inputs, key, fallback) {
37362
37763
  const v = inputs[key];
37363
37764
  return typeof v === "string" && v !== "" ? v : fallback;
37364
37765
  }
37766
+ function bool(inputs, key, fallback) {
37767
+ const v = inputs[key];
37768
+ return typeof v === "boolean" ? v : fallback;
37769
+ }
37365
37770
  function toPoints(bars, values, colors) {
37366
37771
  const pts = new Array(bars.length);
37367
37772
  for (let i = 0; i < bars.length; i++) {
@@ -38893,10 +39298,6 @@ var aroon = {
38893
39298
  plots: [
38894
39299
  { key: "up", title: "Aroon up", values: up, color: BULLISH, width: 2 },
38895
39300
  { key: "down", title: "Aroon down", values: down, color: BEARISH, width: 2 }
38896
- ],
38897
- levels: [
38898
- { key: "upper", price: 100, color: NEUTRAL, lineStyle: "dotted" },
38899
- { key: "lower", price: 0, color: NEUTRAL, lineStyle: "dotted" }
38900
39301
  ]
38901
39302
  };
38902
39303
  }
@@ -39394,8 +39795,10 @@ var volumeSpecs = [
39394
39795
  var DAY_MS2 = 864e5;
39395
39796
  function periodKey(anchor, time) {
39396
39797
  if (anchor === "Week") return Math.floor((Math.floor(time / DAY_MS2) + 3) / 7);
39397
- if (anchor === "Month") {
39798
+ if (anchor === "Month" || anchor === "Quarter" || anchor === "Year") {
39398
39799
  const d = new Date(time);
39800
+ if (anchor === "Year") return d.getUTCFullYear();
39801
+ if (anchor === "Quarter") return d.getUTCFullYear() * 4 + Math.floor(d.getUTCMonth() / 3);
39399
39802
  return d.getUTCFullYear() * 12 + d.getUTCMonth();
39400
39803
  }
39401
39804
  return Math.floor(time / DAY_MS2);
@@ -39447,40 +39850,86 @@ var parabolicSar = {
39447
39850
  return { plots: [{ key: "sar", title: "PSAR", values, kind: "circles", color: SERIES_LINE, colors }] };
39448
39851
  }
39449
39852
  };
39853
+ var VWAP_BANDS_GROUP = "Bands";
39854
+ var VWAP_STYLE = "Style";
39855
+ var VWAP_BANDS = [
39856
+ { on: "band1", mult: "band1Mult", color: "band1Color", fill: "band1Fill", fillColor: "band1FillColor", defMult: 1, defOn: true, defColor: BULLISH },
39857
+ { on: "band2", mult: "band2Mult", color: "band2Color", fill: "band2Fill", fillColor: "band2FillColor", defMult: 2, defOn: false, defColor: WARNING },
39858
+ { on: "band3", mult: "band3Mult", color: "band3Color", fill: "band3Fill", fillColor: "band3FillColor", defMult: 3, defOn: false, defColor: CATEGORICAL[4] }
39859
+ ];
39450
39860
  var vwap = {
39451
39861
  type: "vwap",
39452
39862
  title: "Volume Weighted Average Price",
39453
39863
  shortTitle: "VWAP",
39454
39864
  overlay: true,
39455
39865
  inputs: [
39456
- { key: "anchor", title: "Anchor", type: "string", defval: "Day", options: ["Day", "Week", "Month"], tooltip: "Where the accumulation resets (UTC periods)" },
39457
- { key: "source", title: "Source", type: "string", defval: "HLC3", options: ["HLC3", "OHLC4", "Close"] },
39458
- colorInput(INFO)
39866
+ { key: "anchor", title: "Period", type: "string", defval: "Day", options: ["Day", "Week", "Month", "Quarter", "Year"], tooltip: "Where the accumulation resets (UTC periods)" },
39867
+ sourceInput("HLC3"),
39868
+ // Each band is one row: its toggle leads, the multiplier follows unlabeled.
39869
+ ...VWAP_BANDS.flatMap((b, i) => [
39870
+ { key: b.on, title: `Band ${i + 1} multiplier`, type: "bool", defval: b.defOn, inline: b.on, group: VWAP_BANDS_GROUP },
39871
+ { key: b.mult, title: "", type: "float", defval: b.defMult, min: 0.1, max: 10, step: 0.1, inline: b.on, group: VWAP_BANDS_GROUP }
39872
+ ]),
39873
+ { ...colorInput(INFO, "VWAP color"), group: VWAP_STYLE },
39874
+ // One row per band: its ink, then the fill toggle with the fill's own (translucent) color.
39875
+ ...VWAP_BANDS.flatMap((b, i) => [
39876
+ { ...colorInput(b.defColor, `Band ${i + 1} color`, b.color), inline: b.color, group: VWAP_STYLE },
39877
+ { key: b.fill, title: "Fill", type: "bool", defval: true, inline: b.color, group: VWAP_STYLE },
39878
+ { ...colorInput(withAlpha2(b.defColor), "", b.fillColor), inline: b.color, group: VWAP_STYLE }
39879
+ ])
39459
39880
  ],
39460
39881
  compute: (bars, inputs) => {
39461
39882
  const anchor = str3(inputs, "anchor", "Day");
39462
- const source = str3(inputs, "source", "HLC3");
39463
- const values = new Array(bars.length).fill(Number.NaN);
39883
+ const src = sourceValues(bars, str3(inputs, "source", "HLC3"));
39884
+ const n = bars.length;
39885
+ const values = new Array(n).fill(Number.NaN);
39886
+ const bands = VWAP_BANDS.filter((b) => bool(inputs, b.on, b.defOn)).map((b) => ({
39887
+ mult: Math.max(0, num3(inputs, b.mult, b.defMult)),
39888
+ color: str3(inputs, b.color, b.defColor),
39889
+ fill: bool(inputs, b.fill, true),
39890
+ fillColor: str3(inputs, b.fillColor, withAlpha2(b.defColor)),
39891
+ up: new Array(n).fill(Number.NaN),
39892
+ down: new Array(n).fill(Number.NaN)
39893
+ }));
39464
39894
  let period = Number.NaN;
39465
39895
  let cumPV = 0;
39896
+ let cumPV2 = 0;
39466
39897
  let cumV = 0;
39467
- for (let i = 0; i < bars.length; i++) {
39898
+ for (let i = 0; i < n; i++) {
39468
39899
  const b = bars[i];
39469
39900
  const key = periodKey(anchor, b.time);
39470
39901
  if (key !== period) {
39471
39902
  period = key;
39472
39903
  cumPV = 0;
39904
+ cumPV2 = 0;
39473
39905
  cumV = 0;
39474
39906
  }
39475
39907
  const v = b.volume;
39476
39908
  if (v != null && Number.isFinite(v) && v > 0) {
39477
- const tp = source === "Close" ? b.close : source === "OHLC4" ? (b.open + b.high + b.low + b.close) / 4 : (b.high + b.low + b.close) / 3;
39909
+ const tp = src[i];
39478
39910
  cumPV += tp * v;
39911
+ cumPV2 += tp * tp * v;
39479
39912
  cumV += v;
39480
39913
  }
39481
- if (cumV > 0) values[i] = cumPV / cumV;
39482
- }
39483
- return { plots: [{ key: "vwap", title: "VWAP", values, color: str3(inputs, "color", INFO), width: 2 }] };
39914
+ if (cumV <= 0) continue;
39915
+ const mean = cumPV / cumV;
39916
+ const sd = Math.sqrt(Math.max(0, cumPV2 / cumV - mean * mean));
39917
+ values[i] = mean;
39918
+ for (const band of bands) {
39919
+ band.up[i] = mean + band.mult * sd;
39920
+ band.down[i] = mean - band.mult * sd;
39921
+ }
39922
+ }
39923
+ const plots = [{ key: "vwap", title: "VWAP", values, color: str3(inputs, "color", INFO), width: 2 }];
39924
+ const fills = [];
39925
+ bands.forEach((band, i) => {
39926
+ plots.push(
39927
+ { key: `up${i}`, title: `Upper ${band.mult}\u03C3`, values: band.up, color: band.color },
39928
+ { key: `down${i}`, title: `Lower ${band.mult}\u03C3`, values: band.down, color: band.color }
39929
+ );
39930
+ if (band.fill) fills.push({ key: `band${i}`, from: `up${i}`, to: `down${i}`, color: band.fillColor });
39931
+ });
39932
+ return { plots, bands: fills };
39484
39933
  }
39485
39934
  };
39486
39935
  var pivotPoints = {
@@ -39994,6 +40443,24 @@ function resolveElement(container) {
39994
40443
  function segmentVisibility(parts, chartHidden = false) {
39995
40444
  return { avatar: parts.logo, symbol: parts.name, meta: parts.name, market: parts.market, ohlc: parts.ohlc && !chartHidden, change: parts.change && !chartHidden, eye: chartHidden };
39996
40445
  }
40446
+ var STATUSLINE_LADDER = [
40447
+ { stacked: false, level: "full" },
40448
+ { stacked: true, level: "full" },
40449
+ { stacked: true, level: "compact" },
40450
+ { stacked: true, level: "minimal" }
40451
+ ];
40452
+ function readoutCells(level, readout) {
40453
+ if (readout === "value") return [{ key: "close", label: "" }];
40454
+ if (level === "full") {
40455
+ return [
40456
+ { key: "open", label: "O" },
40457
+ { key: "high", label: "H" },
40458
+ { key: "low", label: "L" },
40459
+ { key: "close", label: "C" }
40460
+ ];
40461
+ }
40462
+ return [{ key: "close", label: level === "compact" ? "C" : "" }];
40463
+ }
39997
40464
  function statuslineMenuItems(parts, chartVisible) {
39998
40465
  return [
39999
40466
  { id: "part:logo", label: "Symbol logo", checked: parts.logo },
@@ -40022,6 +40489,11 @@ function fmtChange(open2, close) {
40022
40489
  const sign = diff >= 0 ? "+" : "";
40023
40490
  return `${sign}${fmtPrice(diff, decimalsFor(close))} (${sign}${pct.toFixed(2)}%)`;
40024
40491
  }
40492
+ function fmtChangePct(open2, close) {
40493
+ if (open2 == null || close == null || !Number.isFinite(open2) || !Number.isFinite(close) || open2 === 0) return "";
40494
+ const pct = (close - open2) / open2 * 100;
40495
+ return `${pct >= 0 ? "+" : ""}${pct.toFixed(2)}%`;
40496
+ }
40025
40497
 
40026
40498
  // src/widget/statusline.ts
40027
40499
  var STYLE_ID24 = "vela-widget-statusline";
@@ -40038,6 +40510,11 @@ var CSS21 = `
40038
40510
  display: flex;
40039
40511
  align-items: baseline;
40040
40512
  gap: var(--vela-space-2);
40513
+ /* The chip never grows past the plot: fit() measures overflow against this width to
40514
+ * walk the layout ladder (stack, then shed values), so overflow:hidden only guards
40515
+ * the transient between a resize and the next measure. */
40516
+ max-width: calc(100% - var(--vela-toolbar-gutter, 0px) - var(--vela-scale-gutter, 0px) - 24px);
40517
+ overflow: hidden;
40041
40518
  color: var(--vela-fg);
40042
40519
  font-size: var(--vela-font-size-md);
40043
40520
  /* Same chip treatment as the indicator legend rows (InputsUI): a translucent wash of
@@ -40059,6 +40536,31 @@ var CSS21 = `
40059
40536
  /* Chart hidden (the price series' eye \u2014 renderer 'candleVisible'): the line dims to
40060
40537
  * the same 0.5 wash a hidden indicator's legend row wears. */
40061
40538
  .vela-statusline.vela-sl-chart-hidden { opacity: 0.5; }
40539
+ /* Two rows inside the chip \u2014 the identity (logo / symbol / meta / market badge) and the
40540
+ * value readout (O/H/L/C + change, or the show-chart eye). In the widest layout they sit
40541
+ * side by side on one line; the STACKED layouts (fit() decides, see the ladder in
40542
+ * statusline-model) turn the chip into a column so the values drop under the symbol. */
40543
+ .vela-statusline .vela-sl-row {
40544
+ display: flex;
40545
+ align-items: baseline;
40546
+ gap: var(--vela-space-2);
40547
+ white-space: nowrap;
40548
+ }
40549
+ .vela-statusline.vela-sl-stacked {
40550
+ flex-direction: column;
40551
+ align-items: flex-start;
40552
+ /* The same air between the two rows as between the values row and the legend row
40553
+ * that follows the chip (see the stacked legend shift below). */
40554
+ row-gap: var(--vela-space-1);
40555
+ }
40556
+ /* Stacked, the identity row may shrink (the meta carries the ellipsis) \u2014 the values row
40557
+ * never does, so its overflow is what fit() measures to keep descending the ladder. */
40558
+ .vela-statusline.vela-sl-stacked .vela-sl-identity { max-width: 100%; }
40559
+ .vela-statusline.vela-sl-stacked .vela-sl-meta {
40560
+ min-width: 0;
40561
+ overflow: hidden;
40562
+ text-overflow: ellipsis;
40563
+ }
40062
40564
  .vela-statusline .vela-sl-avatar {
40063
40565
  width: 18px;
40064
40566
  height: 18px;
@@ -40110,59 +40612,13 @@ var CSS21 = `
40110
40612
  * stylesheet is document-global, so a bare attribute selector here would shift every
40111
40613
  * chart on the page, including statusline-less ones. */
40112
40614
  .vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(26px); }
40113
- /* Mobile: two-line chip \u2014 logo / symbol / meta / market status on one aligned row, the
40114
- * bar change on the next. Full O/H/L/C stays hidden (too dense on a phone-width plot).
40115
- * GRID, not a wrapping flexbox: an absolutely positioned wrapping flex container sizes
40116
- * to the one-line sum of ALL segments (max-content), so its background used to stretch
40117
- * far past the market badge; a grid hugs the widest actual row. The meta column may
40118
- * shrink (it carries the ellipsis), the others wrap their content. */
40119
- [data-layout='mobile'] .vela-statusline {
40120
- display: grid;
40121
- grid-template-columns: auto auto minmax(0, auto) auto;
40122
- justify-content: start;
40123
- align-items: center;
40124
- row-gap: 1px;
40125
- max-width: calc(100% - var(--vela-toolbar-gutter, 0px) - var(--vela-scale-gutter, 0px) - 24px);
40126
- font-size: var(--vela-font-size-sm);
40127
- }
40128
- [data-layout='mobile'] .vela-statusline .vela-sl-symbol {
40129
- font-size: var(--vela-font-size-md);
40130
- line-height: 1;
40131
- }
40132
- [data-layout='mobile'] .vela-statusline .vela-sl-meta {
40133
- min-width: 0;
40134
- overflow: hidden;
40135
- text-overflow: ellipsis;
40136
- white-space: nowrap;
40137
- line-height: 1;
40138
- }
40139
- [data-layout='mobile'] .vela-statusline .vela-sl-avatar,
40140
- [data-layout='mobile'] .vela-statusline .vela-sl-market { align-self: center; }
40141
- [data-layout='mobile'] .vela-statusline .vela-sl-ohlc { display: none !important; }
40142
- [data-layout='mobile'] .vela-statusline .vela-sl-change {
40143
- /* Second row, under the text column \u2014 the avatar keeps the first column. */
40144
- grid-column: 2 / -1;
40145
- font-size: var(--vela-font-size-sm);
40146
- line-height: 1.2;
40147
- }
40148
- [data-layout='mobile'] .vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(40px); }
40149
- /* FIT mode (multi-chart cells \u2014 see setFitMode): the line never wraps; segments that
40150
- * don't fit are HIDDEN by fit() (change first, then meta, then the market badge), so
40151
- * overflow:hidden only guards the transient between a resize and the next measure.
40152
- * Placed after the mobile block on purpose: same specificity, later wins. */
40153
- .vela-statusline.vela-sl-fit {
40154
- display: flex; /* undo the mobile grid \u2014 fit mode is one flex row again */
40155
- flex-wrap: nowrap;
40156
- align-items: center;
40157
- max-width: calc(100% - var(--vela-toolbar-gutter, 0px) - var(--vela-scale-gutter, 0px) - 24px);
40158
- overflow: hidden;
40159
- }
40160
- .vela-statusline.vela-sl-fit .vela-sl-change {
40161
- flex-basis: auto;
40162
- padding-left: 0;
40163
- }
40164
- /* One row again \u2014 the mobile two-line shift doesn't apply in fit mode. */
40165
- [data-layout='mobile'] .vela-sl-fit-host.vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(26px); }
40615
+ /* A stacked chip is two lines tall \u2014 the legend shifts one more line. fit() flags the
40616
+ * host whenever the ladder lands on a stacked layout. */
40617
+ .vela-has-statusline.vela-sl-stacked-host [${LEGEND_AT_TOP_ATTR}] { transform: translateY(40px); }
40618
+ /* Mobile: the same ladder at the phone's type scale \u2014 the width alone decides how much
40619
+ * of the readout fits, exactly as on a narrow desktop chart. */
40620
+ [data-layout='mobile'] .vela-statusline { font-size: var(--vela-font-size-sm); }
40621
+ [data-layout='mobile'] .vela-statusline .vela-sl-symbol { font-size: var(--vela-font-size-md); }
40166
40622
  `;
40167
40623
  function baseOfTicker(ticker) {
40168
40624
  return ticker.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || ticker;
@@ -40233,6 +40689,8 @@ var Statusline = class {
40233
40689
  /** Fit mode (multi-chart cells): one row, overflowing segments hidden — see {@link setFitMode}. */
40234
40690
  this.fitMode = false;
40235
40691
  this.fitRO = null;
40692
+ /** The ladder rung currently applied — see {@link fit}. */
40693
+ this.layout = { stacked: false, level: "full" };
40236
40694
  this.onContextMenu = (e) => {
40237
40695
  if (!this.menu || !this.menuHooks) return;
40238
40696
  e.preventDefault();
@@ -40279,8 +40737,18 @@ var Statusline = class {
40279
40737
  this.menuHooks?.setChartVisible(true);
40280
40738
  this.setChartHidden(false);
40281
40739
  });
40282
- this.el.append(this.avatarEl, this.symbolEl, this.metaEl, this.marketEl, this.ohlcEl, this.changeEl, this.eyeEl);
40740
+ this.identityRow = doc.createElement("span");
40741
+ this.identityRow.className = "vela-sl-row vela-sl-identity";
40742
+ this.identityRow.append(this.avatarEl, this.symbolEl, this.metaEl, this.marketEl);
40743
+ this.valuesRow = doc.createElement("span");
40744
+ this.valuesRow.className = "vela-sl-row vela-sl-values";
40745
+ this.valuesRow.append(this.ohlcEl, this.changeEl, this.eyeEl);
40746
+ this.el.append(this.identityRow, this.valuesRow);
40283
40747
  host.appendChild(this.el);
40748
+ if (typeof ResizeObserver !== "undefined") {
40749
+ this.fitRO = new ResizeObserver(() => this.fit());
40750
+ this.fitRO.observe(host);
40751
+ }
40284
40752
  this.marketTip = new Tooltip(this.marketEl, { content: MARKET_LABELS.open, placement: "bottom" });
40285
40753
  this.eyeTip = new Tooltip(this.eyeEl, { content: "Show chart", placement: "bottom" });
40286
40754
  this.setMarketStatus("open");
@@ -40296,27 +40764,15 @@ var Statusline = class {
40296
40764
  this.fit();
40297
40765
  }
40298
40766
  /**
40299
- * Multi-chart cells: keep the line on ONE row whatever the cell width — never wrap.
40300
- * Segments that don't fit are hidden outright rather than clipped mid-glyph, least
40301
- * important first: OHLC, then the bar change, the venue/timeframe meta, and the
40302
- * market badge; the logo + ticker always stay. Re-fits live on host resizes.
40767
+ * Multi-chart cells: the same width ladder as a single chart, plus a last resort for
40768
+ * cells too narrow for even its bottom rung — whatever still overflows is hidden
40769
+ * outright rather than clipped mid-glyph, least important first: the values, the
40770
+ * venue/timeframe meta, then the market badge; the logo + ticker always stay.
40303
40771
  */
40304
40772
  setFitMode(on) {
40305
40773
  if (on === this.fitMode) return;
40306
40774
  this.fitMode = on;
40307
- this.el.classList.toggle("vela-sl-fit", on);
40308
- this.host.classList.toggle("vela-sl-fit-host", on);
40309
- if (on) {
40310
- if (typeof ResizeObserver !== "undefined" && !this.fitRO) {
40311
- this.fitRO = new ResizeObserver(() => this.fit());
40312
- this.fitRO.observe(this.host);
40313
- }
40314
- this.fit();
40315
- } else {
40316
- this.fitRO?.disconnect();
40317
- this.fitRO = null;
40318
- this.syncParts();
40319
- }
40775
+ this.fit();
40320
40776
  }
40321
40777
  /** Project the parts config onto the segments (the baseline fit() prunes from). */
40322
40778
  syncParts() {
@@ -40328,22 +40784,42 @@ var Statusline = class {
40328
40784
  this.ohlcEl.style.display = seg.ohlc ? "" : "none";
40329
40785
  this.changeEl.style.display = seg.change ? "" : "none";
40330
40786
  this.eyeEl.style.display = seg.eye ? "inline-flex" : "none";
40787
+ this.valuesRow.style.display = seg.ohlc || seg.change || seg.eye ? "" : "none";
40788
+ }
40789
+ /** Apply one ladder rung: the stacked/inline shape and the readout's level. */
40790
+ applyLayout(layout) {
40791
+ this.layout = layout;
40792
+ this.el.classList.toggle("vela-sl-stacked", layout.stacked);
40793
+ this.host.classList.toggle("vela-sl-stacked-host", layout.stacked);
40794
+ this.renderValues();
40331
40795
  }
40332
- /** Hide overflowing segments until the row fits its max-width (fit mode only). */
40796
+ /**
40797
+ * Walk the width ladder (see {@link STATUSLINE_LADDER}) until the chip fits its
40798
+ * max-width: the full one-line readout, then the values stacked under the symbol
40799
+ * line, then O/H/L shed, then the absolute change. In fit mode, when the last rung
40800
+ * still overflows, whole segments hide, least important first. Without layout
40801
+ * (detached host, node tests) nothing overflows, so the widest rung stays.
40802
+ */
40333
40803
  fit() {
40334
- if (!this.fitMode) return;
40335
40804
  this.syncParts();
40336
40805
  const seg = segmentVisibility(this.parts, this.chartHidden);
40806
+ const overflows = () => this.el.scrollWidth > this.el.clientWidth;
40807
+ const hasValues = seg.ohlc || seg.change;
40808
+ for (const rung of STATUSLINE_LADDER) {
40809
+ this.applyLayout(rung);
40810
+ if (!hasValues || !overflows()) break;
40811
+ }
40812
+ if (!this.fitMode) return;
40337
40813
  const order = [
40338
- [this.ohlcEl, seg.ohlc],
40339
- [this.changeEl, seg.change],
40814
+ [this.valuesRow, hasValues],
40340
40815
  [this.metaEl, seg.meta],
40341
40816
  [this.marketEl, seg.market]
40342
40817
  ];
40343
40818
  for (const [el, shown] of order) {
40344
- if (this.el.scrollWidth <= this.el.clientWidth) break;
40819
+ if (!overflows()) break;
40345
40820
  if (shown) el.style.display = "none";
40346
40821
  }
40822
+ if (this.valuesRow.style.display === "none") this.applyLayout({ ...this.layout, stacked: false });
40347
40823
  }
40348
40824
  /** Shape + color the value readout after the active price style: its own up/down
40349
40825
  * colors (candle bodies, bar ticks, the line color, …), the direction rule that
@@ -40456,7 +40932,7 @@ var Statusline = class {
40456
40932
  this.marketTip.destroy();
40457
40933
  this.eyeTip.destroy();
40458
40934
  this.marketBubble.destroy();
40459
- this.host.classList.remove("vela-has-statusline", "vela-sl-fit-host");
40935
+ this.host.classList.remove("vela-has-statusline", "vela-sl-stacked-host");
40460
40936
  this.el.remove();
40461
40937
  }
40462
40938
  detach() {
@@ -40465,6 +40941,10 @@ var Statusline = class {
40465
40941
  }
40466
40942
  render() {
40467
40943
  if (this.menuHooks) this.setChartHidden(!this.menuHooks.chartVisible());
40944
+ this.fit();
40945
+ }
40946
+ /** Write the value readout for the current bar at the current ladder level. */
40947
+ renderValues() {
40468
40948
  const bar = this.hoverBar ?? this.lastBar;
40469
40949
  if (!bar) {
40470
40950
  this.ohlcEl.replaceChildren();
@@ -40484,12 +40964,11 @@ var Statusline = class {
40484
40964
  s.appendChild(b);
40485
40965
  return s;
40486
40966
  };
40487
- if (this.readout === "value") this.ohlcEl.replaceChildren(cell("", bar.close));
40488
- else this.ohlcEl.replaceChildren(cell("O", bar.open), cell("H", bar.high), cell("L", bar.low), cell("C", bar.close));
40489
- this.changeEl.textContent = fmtChange(bar.open, bar.close);
40967
+ const { level } = this.layout;
40968
+ this.ohlcEl.replaceChildren(...readoutCells(level, this.readout).map((c) => cell(c.label, bar[c.key])));
40969
+ this.changeEl.textContent = level === "minimal" ? fmtChangePct(bar.open, bar.close) : fmtChange(bar.open, bar.close);
40490
40970
  this.changeEl.dataset.dir = up ? "up" : "down";
40491
40971
  this.changeEl.style.color = ink;
40492
- this.fit();
40493
40972
  }
40494
40973
  };
40495
40974
 
@@ -42629,6 +43108,17 @@ function nextAutoCellId(taken) {
42629
43108
  if (!taken.has(`c${i}`)) return `c${i}`;
42630
43109
  }
42631
43110
  }
43111
+ function resolveTyping(key, open2) {
43112
+ if (open2.dialogs > 0) {
43113
+ if (key.length !== 1) return null;
43114
+ if (open2.symbolSearch) return { target: "symbol", text: key.toUpperCase() };
43115
+ if (open2.timeframeEntry) return { target: "timeframe", text: key };
43116
+ return null;
43117
+ }
43118
+ if (/^[a-zA-Z]$/.test(key)) return { target: "symbol", text: key.toUpperCase() };
43119
+ if (/^[0-9]$/.test(key)) return { target: "timeframe", text: key };
43120
+ return null;
43121
+ }
42632
43122
  var VelaWorkspace = class {
42633
43123
  constructor(container, opts = {}) {
42634
43124
  this.events = new TypedEventBus();
@@ -44262,17 +44752,14 @@ var VelaWorkspace = class {
44262
44752
  }
44263
44753
  /** Bare-typing router: letters → symbol search (seeded), digits → timeframe entry. */
44264
44754
  routeTyping(ev) {
44265
- if (this.destroyed || this.openDialogs > 0) return;
44755
+ if (this.destroyed) return;
44266
44756
  if (ev.ctrlKey || ev.metaKey || ev.altKey) return;
44267
44757
  if (isEditableTarget(ev)) return;
44268
- const key = ev.key;
44269
- if (/^[a-zA-Z]$/.test(key)) {
44270
- ev.preventDefault();
44271
- this.symbolPicker.open(key.toUpperCase());
44272
- } else if (/^[0-9]$/.test(key)) {
44273
- ev.preventDefault();
44274
- this.tfQuick.open(key);
44275
- }
44758
+ const hit = resolveTyping(ev.key, { dialogs: this.openDialogs, symbolSearch: this.symbolPicker.isOpen, timeframeEntry: this.tfQuick.isOpen });
44759
+ if (!hit) return;
44760
+ ev.preventDefault();
44761
+ if (hit.target === "symbol") this.symbolPicker.type(hit.text);
44762
+ else this.tfQuick.type(hit.text);
44276
44763
  }
44277
44764
  trackDialog(open2) {
44278
44765
  this.openDialogs = Math.max(0, this.openDialogs + (open2 ? 1 : -1));