@luxalgo/vela 0.6.17 → 0.6.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/{DataProvider-B-Jz59Rf.d.ts → DataProvider-Cb-2lC5O.d.ts} +1 -1
  2. package/dist/{DataProvider-OfR3fXg9.d.cts → DataProvider-UtWw2CgJ.d.cts} +1 -1
  3. package/dist/{chunk-YCD72KGK.js → chunk-22QJKGXX.js} +565 -151
  4. package/dist/{chunk-OSZDSH7F.js → chunk-627A5HEV.js} +252 -120
  5. package/dist/{contributions-C3b0Sx5P.d.cts → contributions-5nz3PLRe.d.ts} +9 -4
  6. package/dist/{contributions-Swzz10fG.d.ts → contributions-DEPRtkmG.d.cts} +9 -4
  7. package/dist/index.cjs +563 -149
  8. package/dist/index.d.cts +6 -6
  9. package/dist/index.d.ts +6 -6
  10. package/dist/index.js +1 -1
  11. package/dist/{options-CX1lSYWA.d.cts → options-BCRmYALw.d.cts} +12 -3
  12. package/dist/{options-CX1lSYWA.d.ts → options-BCRmYALw.d.ts} +12 -3
  13. package/dist/{plugin-mvddudqd.d.ts → plugin-CUtfLeqz.d.ts} +3 -3
  14. package/dist/{plugin-BjnEE1zA.d.cts → plugin-DVUaROPo.d.cts} +3 -3
  15. package/dist/plugin.d.cts +4 -4
  16. package/dist/plugin.d.ts +4 -4
  17. package/dist/providers/binance.d.cts +2 -2
  18. package/dist/providers/binance.d.ts +2 -2
  19. package/dist/providers/coinbase.d.cts +2 -2
  20. package/dist/providers/coinbase.d.ts +2 -2
  21. package/dist/providers/hyperliquid.d.cts +2 -2
  22. package/dist/providers/hyperliquid.d.ts +2 -2
  23. package/dist/{statusline-model-IURCya0H.d.ts → statusline-model-hY9cK4BS.d.cts} +17 -7
  24. package/dist/{statusline-model-D91cU2fF.d.cts → statusline-model-wW3m4in1.d.ts} +17 -7
  25. package/dist/ui.d.cts +1 -1
  26. package/dist/ui.d.ts +1 -1
  27. package/dist/vela.global.js +563 -149
  28. package/dist/vela.global.min.js +53 -51
  29. package/dist/widget.cjs +814 -268
  30. package/dist/widget.d.cts +39 -12
  31. package/dist/widget.d.ts +39 -12
  32. package/dist/widget.js +3 -3
  33. package/dist/workspace.cjs +814 -268
  34. package/dist/workspace.d.cts +14 -5
  35. package/dist/workspace.d.ts +14 -5
  36. package/dist/workspace.js +2 -2
  37. package/package.json +1 -1
@@ -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;
@@ -17063,9 +17104,10 @@ async function resolveIndicators(config, fetchImpl = fetch) {
17063
17104
  return out;
17064
17105
  }
17065
17106
  var ledgerEntryName = (e) => typeof e === "string" ? e : e.name;
17107
+ var ledgerNativeType = (e) => typeof e === "string" ? e : e.type;
17066
17108
  function indicatorLedger(i) {
17067
17109
  const natives = [...i.present];
17068
- if (i.volumePending && !natives.includes("volume")) natives.push("volume");
17110
+ if (i.volumePending && !natives.some((e) => ledgerNativeType(e) === "volume")) natives.push("volume");
17069
17111
  return {
17070
17112
  manifest: i.manifestSettled ? [...i.instanceEntries] : [...i.pendingManifest ?? i.instanceEntries],
17071
17113
  natives
@@ -18020,18 +18062,28 @@ function sanitizeCell(raw) {
18020
18062
  if (c.drawings != null && typeof c.drawings === "object") out.drawings = c.drawings;
18021
18063
  const ind = c.indicators;
18022
18064
  if (ind != null && typeof ind === "object") {
18065
+ const bag = (v) => v != null && typeof v === "object" && !Array.isArray(v) ? v : void 0;
18023
18066
  const manifest = Array.isArray(ind.manifest) ? ind.manifest.flatMap((n) => {
18024
18067
  if (typeof n === "string") return [n];
18025
18068
  if (n != null && typeof n === "object" && typeof n.name === "string") {
18026
18069
  const e = n;
18027
- const bag = (v) => v != null && typeof v === "object" && !Array.isArray(v) ? v : void 0;
18028
18070
  const inputs = bag(e.inputs);
18029
18071
  const props = bag(e.props);
18030
- return [inputs || props ? { name: e.name, ...inputs ? { inputs } : {}, ...props ? { props } : {} } : e.name];
18072
+ const hidden = e.hidden === true;
18073
+ return [inputs || props || hidden ? { name: e.name, ...inputs ? { inputs } : {}, ...props ? { props } : {}, ...hidden ? { hidden } : {} } : e.name];
18074
+ }
18075
+ return [];
18076
+ }) : [];
18077
+ const natives = Array.isArray(ind.natives) ? ind.natives.flatMap((n) => {
18078
+ if (typeof n === "string") return [n];
18079
+ if (n != null && typeof n === "object" && typeof n.type === "string") {
18080
+ const e = n;
18081
+ const inputs = bag(e.inputs);
18082
+ const hidden = e.hidden === true;
18083
+ return [inputs || hidden ? { type: e.type, ...inputs ? { inputs } : {}, ...hidden ? { hidden } : {} } : e.type];
18031
18084
  }
18032
18085
  return [];
18033
18086
  }) : [];
18034
- const natives = Array.isArray(ind.natives) ? ind.natives.filter((n) => typeof n === "string") : [];
18035
18087
  out.indicators = { manifest, natives };
18036
18088
  }
18037
18089
  const ext = sanitizeExt(c.ext);
@@ -19844,7 +19896,12 @@ var EngineOrchestrator = class _EngineOrchestrator {
19844
19896
  if (!model) continue;
19845
19897
  const paneId = model.paneId ?? "price";
19846
19898
  if (!byPane.has(paneId)) byPane.set(paneId, []);
19847
- byPane.get(paneId).push({ id: r.id, title: model.title || r.title, ownScale: model.ownScale === true });
19899
+ byPane.get(paneId).push({
19900
+ id: r.id,
19901
+ title: model.title || r.title,
19902
+ ...model.shorttitle ? { shorttitle: model.shorttitle } : {},
19903
+ ownScale: model.ownScale === true
19904
+ });
19848
19905
  }
19849
19906
  const panes = [];
19850
19907
  for (const paneId of this.paneOrder) {
@@ -24477,7 +24534,7 @@ var InputController = class {
24477
24534
  if (e.repeat || e.key !== "Shift" && e.key !== "Control" && e.key !== "Meta") return;
24478
24535
  if (Number.isNaN(this.cursorX)) return;
24479
24536
  if (this.dragging && this.region !== "drawing") return;
24480
- this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey);
24537
+ this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
24481
24538
  };
24482
24539
  this.onDown = (e) => {
24483
24540
  if (e.button === 1) {
@@ -24509,7 +24566,7 @@ var InputController = class {
24509
24566
  this.startY = y;
24510
24567
  if (this.deps.drawingsClaim?.(x, y)) {
24511
24568
  this.region = "drawing";
24512
- this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey);
24569
+ this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
24513
24570
  this.capture(e.pointerId);
24514
24571
  return;
24515
24572
  }
@@ -24518,6 +24575,11 @@ var InputController = class {
24518
24575
  this.capture(e.pointerId);
24519
24576
  return;
24520
24577
  }
24578
+ if ((e.ctrlKey || e.metaKey) && this.regionAt(x, y) === "data" && this.deps.drawingsMarqueeStart?.(x, y)) {
24579
+ this.region = "drawing";
24580
+ this.capture(e.pointerId);
24581
+ return;
24582
+ }
24521
24583
  const vp = this.deps.getCoords().getViewport();
24522
24584
  this.deps.apply(vp);
24523
24585
  this.startRightOffset = vp.rightOffset;
@@ -24566,7 +24628,7 @@ var InputController = class {
24566
24628
  this.deps.apply({ barSpacing, rightOffset: this.startRightOffset });
24567
24629
  } else if (this.region === "drawing") {
24568
24630
  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);
24631
+ this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
24570
24632
  } else {
24571
24633
  const coords = this.deps.getCoords();
24572
24634
  const vp = coords.getViewport();
@@ -24589,7 +24651,7 @@ var InputController = class {
24589
24651
  const drawCursor = this.deps.drawingsCursor?.(x, y);
24590
24652
  const r = this.regionAt(x, y);
24591
24653
  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);
24654
+ this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
24593
24655
  }
24594
24656
  if (e.pointerType !== "touch") this.deps.onPointerMove(x, y);
24595
24657
  };
@@ -24930,12 +24992,12 @@ function defaultTradeMarkersState() {
24930
24992
  function mergeTradeMarkersState(base, patch) {
24931
24993
  const p = patch && typeof patch === "object" ? patch : {};
24932
24994
  const c = p.colors && typeof p.colors === "object" ? p.colors : {};
24933
- const bool = (v, fb) => typeof v === "boolean" ? v : fb;
24995
+ const bool2 = (v, fb) => typeof v === "boolean" ? v : fb;
24934
24996
  const color = (v, fb) => typeof v === "string" && v.trim().length > 0 ? v : fb;
24935
24997
  return {
24936
- visible: bool(p.visible, base.visible),
24937
- labels: bool(p.labels, base.labels),
24938
- qty: bool(p.qty, base.qty),
24998
+ visible: bool2(p.visible, base.visible),
24999
+ labels: bool2(p.labels, base.labels),
25000
+ qty: bool2(p.qty, base.qty),
24939
25001
  colors: {
24940
25002
  long: color(c.long, base.colors.long),
24941
25003
  short: color(c.short, base.colors.short),
@@ -28795,6 +28857,18 @@ var DrawingPainter = class {
28795
28857
  }
28796
28858
  this.paintHandles(ctx, pts);
28797
28859
  }
28860
+ /** The selection box a Ctrl/Cmd-drag on the empty plot sweeps: a faint fill in the handle
28861
+ * accent under a thin dashed outline. */
28862
+ paintMarquee(ctx, rect) {
28863
+ ctx.save();
28864
+ ctx.fillStyle = withAlpha(HANDLE_BORDER, 0.08);
28865
+ ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
28866
+ ctx.strokeStyle = HANDLE_BORDER;
28867
+ ctx.lineWidth = 1;
28868
+ ctx.setLineDash([4, 3]);
28869
+ ctx.strokeRect(rect.x + 0.5, rect.y + 0.5, rect.w, rect.h);
28870
+ ctx.restore();
28871
+ }
28798
28872
  /** A hollow ring marking the candle point the magnet (Ctrl) will snap the next anchor to. */
28799
28873
  paintSnapRing(ctx, x, y, theme) {
28800
28874
  ctx.setLineDash([]);
@@ -30263,6 +30337,15 @@ function topDrawingAt(drawings, x, y, proj, tol = HIT_TOLERANCE) {
30263
30337
  }
30264
30338
  return null;
30265
30339
  }
30340
+ function deletableSelection(selected, drawings) {
30341
+ const ids = [...selected];
30342
+ if (ids.length < 2) return ids;
30343
+ return ids.filter((id) => !drawings.find((d) => d.id === id)?.locked);
30344
+ }
30345
+ function deleteTargets(hit, selected, drawings, withSelection) {
30346
+ if (withSelection && selected.size >= 2 && selected.has(hit.id)) return deletableSelection(selected, drawings);
30347
+ return hit.locked ? [] : [hit.id];
30348
+ }
30266
30349
 
30267
30350
  // src/renderers/native/drawings/DrawingInteraction.ts
30268
30351
  var DRAG_SLOP3 = 3;
@@ -30366,10 +30449,32 @@ var DrawingInteraction = class {
30366
30449
  activeDragId() {
30367
30450
  return this.state.kind === "pressed" && this.state.moved ? this.state.id : null;
30368
30451
  }
30452
+ /** The drawing under a press that has not been released yet (drag or click undecided), or null. */
30453
+ pressedId() {
30454
+ return this.state.kind === "pressed" ? this.state.id : null;
30455
+ }
30456
+ /** The copies a Ctrl-drag is moving (transient — not yet in the store), or null. */
30457
+ dragClones() {
30458
+ return this.state.kind === "pressed" && this.state.moved && this.state.clones ? this.state.clones.map((c) => c.d) : null;
30459
+ }
30460
+ /** The marquee box being swept, normalized (or null when none is in progress). */
30461
+ marqueeRect() {
30462
+ if (this.state.kind !== "marquee") return null;
30463
+ const { x0, y0, x1, y1 } = this.state;
30464
+ return { x: Math.min(x0, x1), y: Math.min(y0, y1), w: Math.abs(x1 - x0), h: Math.abs(y1 - y0) };
30465
+ }
30466
+ /** Ctrl/Cmd-press on the empty plot: start sweeping a selection box. False when a tool is
30467
+ * armed or a gesture is in flight (that path owns the press). */
30468
+ beginMarquee(x, y) {
30469
+ if (this.state.kind !== "idle" || this.deps.activeTool() != null) return false;
30470
+ this.state = { kind: "marquee", x0: x, y0: y, x1: x, y1: y };
30471
+ return true;
30472
+ }
30369
30473
  /** A press: place the next anchor, or grab a drawing/handle (resolved as click or drag on release).
30370
30474
  * `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) {
30475
+ * while placing a line tool it locks the segment angle to 45° steps. `mod` (Ctrl/Cmd) over a
30476
+ * drawing also toggles on a click, but a body drag then moves a COPY. */
30477
+ down(x, y, mode = "off", shift2 = false, mod = false) {
30373
30478
  const proj = this.deps.projector();
30374
30479
  if (this.state.kind === "placing") {
30375
30480
  if (this.state.draft.placementMode() !== "click") return;
@@ -30397,7 +30502,50 @@ var DrawingInteraction = class {
30397
30502
  return;
30398
30503
  }
30399
30504
  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 };
30505
+ const riders = [];
30506
+ if (handle < 0 && this.deps.selectedIds().has(hit.id)) {
30507
+ for (const id of this.deps.selectedIds()) {
30508
+ const d = id === hit.id ? null : this.byId(id);
30509
+ if (d && !d.locked && d.visible) riders.push({ id, orig: cloneAnchors(d) });
30510
+ }
30511
+ }
30512
+ this.state = { kind: "pressed", id: hit.id, handle, orig: cloneAnchors(hit), px: x, py: y, moved: false, riders, mod, clones: null };
30513
+ }
30514
+ }
30515
+ /** The data-space delta a body drag from the press to (x, y) means on `paneId` — resolved per
30516
+ * pane so drawings riding along on another pane move by the same PIXELS, not the same price. */
30517
+ bodyDelta(paneId, x, y) {
30518
+ if (this.state.kind !== "pressed") return { dt: 0, dp: 0 };
30519
+ const proj = this.deps.projector();
30520
+ const from = proj.pxToPoint(this.state.px, this.state.py, paneId);
30521
+ const to = proj.pxToPoint(x, y, paneId);
30522
+ return { dt: to.time - from.time, dp: to.price - from.price };
30523
+ }
30524
+ /** The pressed drawing and its riders, each with its anchors at the press. */
30525
+ dragSources(d) {
30526
+ if (this.state.kind !== "pressed") return [];
30527
+ const out = [{ d, orig: this.state.orig }];
30528
+ for (const r of this.state.riders) {
30529
+ const rd = this.byId(r.id);
30530
+ if (rd) out.push({ d: rd, orig: r.orig });
30531
+ }
30532
+ return out;
30533
+ }
30534
+ /** Move the pressed drawing's body (and its riders) — or, on a Ctrl-drag, their copies — to the
30535
+ * cursor. The copies are built on the first move (so a Ctrl-click never creates anything), from
30536
+ * sources that have not moved yet, so their own anchors are the origin to translate from. */
30537
+ dragBody(d, x, y) {
30538
+ if (this.state.kind !== "pressed") return;
30539
+ this.snapAt = null;
30540
+ if (this.state.mod && !this.state.clones) {
30541
+ this.state.clones = this.dragSources(d).flatMap((s) => {
30542
+ const c = deserializeDrawing(s.d.serialize());
30543
+ return c ? [{ d: c, orig: cloneAnchors(c) }] : [];
30544
+ });
30545
+ }
30546
+ for (const t of this.state.clones ?? this.dragSources(d)) {
30547
+ const { dt, dp } = this.bodyDelta(t.d.paneId, x, y);
30548
+ t.d.anchors = t.d.translateBody(dt, dp, t.orig);
30401
30549
  }
30402
30550
  }
30403
30551
  /** Pointer move: advance the placing ghost, or live-preview a drag once past the slop.
@@ -30421,12 +30569,20 @@ var DrawingInteraction = class {
30421
30569
  this.deps.changed();
30422
30570
  return;
30423
30571
  }
30572
+ if (this.state.kind === "marquee") {
30573
+ this.state.x1 = x;
30574
+ this.state.y1 = y;
30575
+ this.deps.changed();
30576
+ return;
30577
+ }
30424
30578
  if (this.state.kind !== "pressed") return;
30425
30579
  const d = this.byId(this.state.id);
30426
30580
  if (!d || d.locked) return;
30427
30581
  if (!this.state.moved && Math.abs(x - this.state.px) <= DRAG_SLOP3 && Math.abs(y - this.state.py) <= DRAG_SLOP3) return;
30428
30582
  this.state.moved = true;
30429
- if (this.state.handle >= 0) {
30583
+ if (this.state.handle < 0) {
30584
+ this.dragBody(d, x, y);
30585
+ } else {
30430
30586
  const a = d.anchors[this.state.handle];
30431
30587
  if (a) {
30432
30588
  const pivot = shift2 && ANGLE_SNAP_TYPES.has(d.type) && d.anchors.length === 2 ? d.anchors[1 - this.state.handle] : void 0;
@@ -30434,12 +30590,6 @@ var DrawingInteraction = class {
30434
30590
  applyFree(a, point, d.anchorSchema().slots[this.state.handle]?.free ?? "both");
30435
30591
  }
30436
30592
  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
30593
  }
30444
30594
  this.deps.changed();
30445
30595
  }
@@ -30459,16 +30609,46 @@ var DrawingInteraction = class {
30459
30609
  }
30460
30610
  return;
30461
30611
  }
30612
+ if (this.state.kind === "marquee") {
30613
+ const rect = this.marqueeRect();
30614
+ this.state = { kind: "idle" };
30615
+ if (rect.w > DRAG_SLOP3 || rect.h > DRAG_SLOP3) this.selectWithin(rect);
30616
+ this.deps.changed();
30617
+ return;
30618
+ }
30462
30619
  if (this.state.kind !== "pressed") return;
30463
- const { id, moved } = this.state;
30620
+ const { id, moved, riders, mod, clones } = this.state;
30464
30621
  this.state = { kind: "idle" };
30465
30622
  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);
30623
+ if (!moved) {
30624
+ if (mod) this.deps.emit({ kind: "select", ids: [id], additive: true });
30625
+ else this.deps.openSettings(id, x, y);
30626
+ return;
30627
+ }
30628
+ if (clones) {
30629
+ this.deps.emit({ kind: "clone", docs: clones.map((c) => c.d.serialize()) });
30630
+ } else if (d) {
30631
+ const docs = [d.serialize()];
30632
+ for (const r of riders) {
30633
+ const rd = this.byId(r.id);
30634
+ if (rd) docs.push(rd.serialize());
30635
+ }
30636
+ if (docs.length > 1) this.deps.emit({ kind: "edit-many", docs });
30637
+ else this.deps.emit({ kind: "edit", doc: docs[0] });
30471
30638
  }
30639
+ this.deps.changed();
30640
+ }
30641
+ /** Add every visible drawing whose on-screen bounds touch `rect` to the selection (a union,
30642
+ * so successive sweeps accumulate). No change when the box touches nothing new. */
30643
+ selectWithin(rect) {
30644
+ const proj = this.deps.projector();
30645
+ const ids = [...this.deps.selectedIds()];
30646
+ for (const d of this.deps.drawings()) {
30647
+ if (!d.visible || ids.includes(d.id)) continue;
30648
+ const b = d.bounds(proj);
30649
+ if (b && rectsIntersect(rect, b)) ids.push(d.id);
30650
+ }
30651
+ if (ids.length > this.deps.selectedIds().size) this.deps.emit({ kind: "select", ids });
30472
30652
  }
30473
30653
  /** Cancel an in-progress placement or drag (Escape). Returns whether anything was cancelled. */
30474
30654
  cancel() {
@@ -30481,8 +30661,15 @@ var DrawingInteraction = class {
30481
30661
  return true;
30482
30662
  }
30483
30663
  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 }));
30664
+ if (this.state.moved && !this.state.clones) {
30665
+ const d = this.byId(this.state.id);
30666
+ if (d) for (const s of this.dragSources(d)) s.d.anchors = s.orig.map((o) => ({ time: o.time, price: o.price }));
30667
+ }
30668
+ this.state = { kind: "idle" };
30669
+ this.deps.changed();
30670
+ return true;
30671
+ }
30672
+ if (this.state.kind === "marquee") {
30486
30673
  this.state = { kind: "idle" };
30487
30674
  this.deps.changed();
30488
30675
  return true;
@@ -30597,6 +30784,9 @@ var DrawingInteraction = class {
30597
30784
  function cloneAnchors(d) {
30598
30785
  return d.anchors.map((a) => ({ time: a.time, price: a.price }));
30599
30786
  }
30787
+ function rectsIntersect(a, b) {
30788
+ 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;
30789
+ }
30600
30790
  function applyFree(anchor, pt, free) {
30601
30791
  if (free === "both" || free === "x") anchor.time = pt.time;
30602
30792
  if (free === "both" || free === "y") anchor.price = pt.price;
@@ -30982,10 +31172,27 @@ function btn(label, primary, onClick) {
30982
31172
  }
30983
31173
 
30984
31174
  // src/renderers/native/drawings/DrawingSettingsPopup.ts
31175
+ var MIXED = /* @__PURE__ */ Symbol("mixed");
31176
+ function sharedPaths(drawings) {
31177
+ const [first, ...rest] = drawings;
31178
+ const paths = new Set(first?.schema().fields.map((f) => f.path) ?? []);
31179
+ for (const d of rest) {
31180
+ const own = new Set(d.schema().fields.map((f) => f.path));
31181
+ for (const p of paths) if (!own.has(p)) paths.delete(p);
31182
+ }
31183
+ return paths;
31184
+ }
31185
+ function commonValue(drawings, read) {
31186
+ const first = read(drawings[0]);
31187
+ return drawings.every((d) => read(d) === first) ? first : MIXED;
31188
+ }
31189
+ function distinctValues(drawings, read) {
31190
+ return [...new Set(drawings.map(read))];
31191
+ }
30985
31192
  var BTN = 30;
30986
31193
  var ICON = 21;
30987
31194
  var STYLE_ID23 = "vela-drawing-popup-styles";
30988
- var STYLE_REV = "3";
31195
+ var STYLE_REV = "4";
30989
31196
  function ensureStyles5() {
30990
31197
  if (typeof document === "undefined") return;
30991
31198
  const existing = document.getElementById(STYLE_ID23);
@@ -30997,6 +31204,8 @@ function ensureStyles5() {
30997
31204
  .vela-dpop-btn{background:transparent;color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
30998
31205
  .vela-dpop-btn:hover{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
30999
31206
  .vela-dpop-btn[data-active='1']{background:var(--vela-active);color:var(--vela-fg-bright);}
31207
+ /* Mixed (a multi-selection disagrees): the active fill at half strength behind a dashed ring. */
31208
+ .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
31209
  .vela-dpop-item{background:transparent;transition:background var(--vela-dur-fast) ease;}
31001
31210
  .vela-dpop-item:hover{background:var(--vela-hover-strong);}
31002
31211
  .vela-dpop-item[data-active='1']{background:var(--vela-active);}
@@ -31028,7 +31237,7 @@ var DrawingSettingsPopup = class {
31028
31237
  if (this.isOwnChrome(node)) return;
31029
31238
  const cb = this.onClose;
31030
31239
  this.close();
31031
- cb?.();
31240
+ cb?.(e);
31032
31241
  };
31033
31242
  this.theme = theme;
31034
31243
  this.settingsDialog = new DrawingSettingsDialog(host, theme);
@@ -31053,16 +31262,27 @@ var DrawingSettingsPopup = class {
31053
31262
  contains(node) {
31054
31263
  return node != null && (this.isOwnChrome(node) || this.settingsDialog.contains(node));
31055
31264
  }
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) {
31265
+ /** Open the quick toolbar for `drawings` (one, or a whole multi-selection), floating clear of
31266
+ * their `anchor` box. With several drawings the bar shows only the controls they ALL support,
31267
+ * a control whose values differ reads as mixed, and every edit applies to all of them —
31268
+ * per-drawing panels (levels, position sizing, the text field) stay single-drawing only.
31269
+ * `onClose` fires on a dismissal (an outside press, or Escape in the text field — not a
31270
+ * programmatic close), with the dismissing press when there is one, so the caller can tell a
31271
+ * modifier press (multi-select) from a plain one. */
31272
+ open(drawings, anchor, actions, onClose) {
31059
31273
  this.close();
31274
+ const drawing = drawings[0];
31275
+ if (!drawing) return;
31060
31276
  ensureStyles5();
31061
31277
  this.onClose = onClose ?? null;
31062
31278
  const t = this.theme;
31279
+ const multi = drawings.length > 1;
31063
31280
  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");
31281
+ const paths = sharedPaths(drawings);
31282
+ const common = (read) => commonValue(drawings, read);
31283
+ const every = (pred) => drawings.every(pred);
31284
+ const swatch = (tip, glyph, read, path) => this.colorButton(tip, glyph, common(read), (v) => actions.patch({ [path]: v }), void 0, distinctValues(drawings, read));
31285
+ const textOnBar = multi || schema.textIsContent === true || !paths.has("text.value");
31066
31286
  const el = document.createElement("div");
31067
31287
  el.className = "vela-dpop";
31068
31288
  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 +31296,84 @@ var DrawingSettingsPopup = class {
31076
31296
  bar.style.cssText = "display:flex;align-items:center;gap:2px;padding:4px;";
31077
31297
  bar.appendChild(this.dragHandle());
31078
31298
  if (paths.has("glyph")) {
31079
- const cur = drawing.glyph ?? GLYPH_OPTIONS[0];
31299
+ const cur = common((d) => d.glyph ?? GLYPH_OPTIONS[0]);
31080
31300
  bar.appendChild(this.dropdown("Icon", GLYPH_OPTIONS, cur, (g) => glyphIcon(g), (v) => actions.patch({ glyph: v })));
31081
31301
  }
31082
31302
  if (paths.has("size")) {
31083
- const sz = drawing.size ?? "normal";
31303
+ const sz = common((d) => d.size ?? "normal");
31084
31304
  bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
31085
31305
  }
31086
- if (paths.has("magnifier.timeframe") && drawing instanceof Magnifier) {
31306
+ if (paths.has("magnifier.timeframe") && every((d) => d instanceof Magnifier)) {
31307
+ const mag = (d) => d;
31087
31308
  const options = this.lowerTimeframeOptions();
31088
31309
  if (options.length > 0) {
31089
31310
  bar.appendChild(
31090
- this.dropdown("Lower timeframe", options.map((o) => o.value), drawing.magnifier.timeframe, () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
31311
+ this.dropdown("Lower timeframe", options.map((o) => o.value), common((d) => mag(d).magnifier.timeframe), () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
31091
31312
  label: (v) => magnifierTimeframeLabel(String(v)),
31092
31313
  labelInTrigger: true
31093
31314
  })
31094
31315
  );
31095
31316
  }
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 })));
31317
+ bar.appendChild(swatch("Up candles", BUCKET_ICON, (d) => mag(d).magnifier.upColor || t.upColor, "magnifier.upColor"));
31318
+ bar.appendChild(swatch("Down candles", BUCKET_ICON, (d) => mag(d).magnifier.downColor || t.downColor, "magnifier.downColor"));
31098
31319
  }
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 })));
31320
+ 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
31321
  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 })));
31322
+ const widthField2 = (d) => d.schema().fields.find((f) => f.path === "style.lineWidth");
31323
+ const wf = widthField2(drawing);
31324
+ const width = common((d) => d.style.lineWidth);
31325
+ const markers = every((d) => {
31326
+ const f = widthField2(d);
31327
+ return f?.kind === "number" && (f.min ?? 1) > 1;
31328
+ });
31329
+ if (markers && wf?.kind === "number") {
31330
+ bar.appendChild(this.widthInput("Line width", width, wf.min ?? 1, wf.max ?? 60, wf.step ?? 1, (v) => actions.patch({ "style.lineWidth": v })));
31104
31331
  } 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 }));
31332
+ 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
31333
  }
31107
31334
  }
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 })));
31335
+ 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 }));
31336
+ 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
31337
  const isFrvp = paths.has("frvp.rows") && drawing instanceof FixedRangeVolumeProfile;
31111
- const isPosition = paths.has("riskPercent") && drawing instanceof PositionTool;
31338
+ const isPosition = paths.has("riskPercent") && every((d) => d instanceof PositionTool);
31112
31339
  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 })));
31340
+ const pos = (d) => d;
31341
+ bar.appendChild(swatch("Profit zone", BUCKET_ICON, (d) => pos(d).profitColor, "profitColor"));
31342
+ bar.appendChild(swatch("Loss zone", BUCKET_ICON, (d) => pos(d).lossColor, "lossColor"));
31116
31343
  }
31117
- const reg = drawing.reg;
31118
- if (paths.has("reg.midColor") && reg) {
31344
+ const regOf = (d) => d.reg;
31345
+ if (paths.has("reg.midColor") && every((d) => regOf(d) != null)) {
31346
+ const reg = (d) => regOf(d);
31119
31347
  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) {
31348
+ bar.appendChild(swatch("Midline color", BRUSH_ICON, (d) => reg(d).midColor, "reg.midColor"));
31349
+ bar.appendChild(this.dropdown("Midline style", styles, common((d) => reg(d).midStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "reg.midStyle": v }), { label: styleLabel2 }));
31350
+ bar.appendChild(swatch("Upper line color", BRUSH_ICON, (d) => reg(d).upperColor, "reg.upperColor"));
31351
+ 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 }));
31352
+ bar.appendChild(swatch("Lower line color", BRUSH_ICON, (d) => reg(d).lowerColor, "reg.lowerColor"));
31353
+ 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 }));
31354
+ bar.appendChild(swatch("Upper fill", BUCKET_ICON, (d) => reg(d).upperFill, "reg.upperFill"));
31355
+ bar.appendChild(swatch("Lower fill", BUCKET_ICON, (d) => reg(d).lowerFill, "reg.lowerFill"));
31356
+ bar.appendChild(this.toggle("Show R\xB2", R2_ICON, common((d) => reg(d).showR2), (v) => actions.patch({ "reg.showR2": v })));
31357
+ }
31358
+ const vwapOf = (d) => d.vwap;
31359
+ if (paths.has("vwap.midColor") && every((d) => vwapOf(d) != null)) {
31360
+ const vwap2 = (d) => vwapOf(d);
31132
31361
  const styles = LINE_STYLE_OPTIONS.map((o) => o.value);
31133
31362
  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 }));
31363
+ bar.appendChild(swatch("VWAP color", BRUSH_ICON, (d) => vwap2(d).midColor, "vwap.midColor"));
31364
+ 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
31365
  bar.appendChild(
31137
- this.dropdown("Band multiplier", MULTS, vwap2.multiplier, () => BANDS_ICON, (v) => actions.patch({ "vwap.multiplier": v }), {
31366
+ this.dropdown("Band multiplier", MULTS, common((d) => vwap2(d).multiplier), () => BANDS_ICON, (v) => actions.patch({ "vwap.multiplier": v }), {
31138
31367
  label: (v) => `${v}\u03C3`,
31139
31368
  labelInTrigger: true
31140
31369
  })
31141
31370
  );
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 })));
31371
+ bar.appendChild(swatch("Upper band color", BRUSH_ICON, (d) => vwap2(d).upperColor, "vwap.upperColor"));
31372
+ bar.appendChild(swatch("Lower band color", BRUSH_ICON, (d) => vwap2(d).lowerColor, "vwap.lowerColor"));
31373
+ bar.appendChild(swatch("Band fill", BUCKET_ICON, (d) => vwap2(d).bandFill, "vwap.bandFill"));
31145
31374
  }
31146
31375
  if (paths.has("maxCurvature")) {
31147
- const cur = drawing.maxCurvature ?? 24;
31376
+ const cur = common((d) => d.maxCurvature ?? 24);
31148
31377
  bar.appendChild(
31149
31378
  this.dropdown("Max curvature", DEDEKIND_CURVATURE_OPTIONS, cur, () => DEDEKIND_ICON2, (v) => actions.patch({ maxCurvature: v }), {
31150
31379
  label: (v) => `n=${v}`,
@@ -31153,7 +31382,7 @@ var DrawingSettingsPopup = class {
31153
31382
  );
31154
31383
  }
31155
31384
  if (paths.has("mach")) {
31156
- const cur = drawing.mach ?? 2;
31385
+ const cur = common((d) => d.mach ?? 2);
31157
31386
  bar.appendChild(
31158
31387
  this.dropdown("Mach number", MACH_NUMBER_OPTIONS, cur, () => SUPERSONIC_ICON2, (v) => actions.patch({ mach: v }), {
31159
31388
  label: (v) => `M=${v}`,
@@ -31162,7 +31391,7 @@ var DrawingSettingsPopup = class {
31162
31391
  );
31163
31392
  }
31164
31393
  if (paths.has("waveCount")) {
31165
- const cur = drawing.waveCount ?? 6;
31394
+ const cur = common((d) => d.waveCount ?? 6);
31166
31395
  bar.appendChild(
31167
31396
  this.dropdown("Waves", MACH_WAVE_COUNT_OPTIONS, cur, () => SONIC_ICON2, (v) => actions.patch({ waveCount: v }), {
31168
31397
  label: (v) => `${v}`,
@@ -31170,26 +31399,27 @@ var DrawingSettingsPopup = class {
31170
31399
  })
31171
31400
  );
31172
31401
  }
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)));
31402
+ const flags = (d) => d;
31403
+ if (paths.has("showPrice")) bar.appendChild(this.toggle("Show price", PRICE_DELTA_ICON, common((d) => flags(d).showPrice !== false), (v) => actions.patch({ showPrice: v })));
31404
+ if (paths.has("showDate")) bar.appendChild(this.toggle("Show date", DATE_DELTA_ICON, common((d) => flags(d).showDate !== false), (v) => actions.patch({ showDate: v })));
31405
+ if (paths.has("text.color") && textOnBar) bar.appendChild(swatch("Text color", TYPE_ICON, (d) => d.text?.color || t.textColor, "text.color"));
31406
+ 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 }));
31407
+ const textField = !multi && paths.has("text.value");
31408
+ if (paths.has("text.bold") && !textField) bar.appendChild(this.toggle("Bold", BOLD_ICON, common((d) => !!d.text?.bold), (v) => actions.patch({ "text.bold": v })));
31409
+ if (paths.has("text.italic") && !textField) bar.appendChild(this.toggle("Italic", ITALIC_ICON, common((d) => !!d.text?.italic), (v) => actions.patch({ "text.italic": v })));
31410
+ if (textField) bar.appendChild(this.iconBtn("Text", TYPE_ICON, () => this.toggleTextPanel(drawing, actions, !textOnBar)));
31181
31411
  const editableLevels = drawing.editableLevels();
31182
- if (editableLevels && !(drawing instanceof MachFigure)) {
31183
- const fib = drawing;
31412
+ if (editableLevels && every((d) => d.editableLevels() != null && !(d instanceof MachFigure))) {
31413
+ const fib = (d) => d;
31184
31414
  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 }));
31415
+ bar.appendChild(this.dropdown("Numbers size", sizes, common((d) => fib(d).numbersSize ?? "small"), (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel }));
31416
+ bar.appendChild(this.dropdown("Label size", sizes, common((d) => fib(d).labelsSize ?? "normal"), (s) => labelSizeIcon(s), (v) => actions.patch({ labelsSize: v }), { label: sizeLabel }));
31187
31417
  }
31188
31418
  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)));
31419
+ if (!multi && isFrvp) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "frvp")));
31420
+ if (!multi && isPosition) bar.appendChild(this.iconBtn("Position size", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "position")));
31421
+ if (!multi && editableLevels) bar.appendChild(this.iconBtn("Levels", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "levels")));
31422
+ bar.appendChild(this.toggle("Lock", LOCK_ICON, common((d) => d.locked), (v) => actions.setLocked(v), UNLOCK_ICON));
31193
31423
  const del = this.iconBtn("Delete", TRASH_ICON, () => actions.remove());
31194
31424
  del.style.color = "var(--vela-danger)";
31195
31425
  bar.appendChild(del);
@@ -31375,7 +31605,7 @@ var DrawingSettingsPopup = class {
31375
31605
  });
31376
31606
  return b;
31377
31607
  }
31378
- /** The trailing kebab overflow — z-order actions + reset settings. */
31608
+ /** The trailing kebab overflow — duplicate, z-order actions, reset settings. */
31379
31609
  kebabButton(actions) {
31380
31610
  const b = this.base("More");
31381
31611
  b.innerHTML = sized(KEBAB_ICON);
@@ -31387,6 +31617,7 @@ var DrawingSettingsPopup = class {
31387
31617
  return;
31388
31618
  }
31389
31619
  this.openActionMenu(b, [
31620
+ { icon: CLONE_ICON, label: "Duplicate", onClick: () => actions.duplicate() },
31390
31621
  { icon: FRONT_ICON, label: "Bring to front", onClick: () => actions.reorder("front") },
31391
31622
  { icon: BACK_ICON, label: "Send to back", onClick: () => actions.reorder("back") },
31392
31623
  { icon: RESET_ICON, label: "Reset settings", onClick: () => actions.resetSettings() }
@@ -31543,26 +31774,30 @@ var DrawingSettingsPopup = class {
31543
31774
  this.tipEl?.remove();
31544
31775
  this.tipEl = null;
31545
31776
  }
31546
- toggle(tip, icon2, active, onChange) {
31777
+ /** An on/off button. A mixed state (the selected drawings disagree) reads as half-lit and
31778
+ * the first click turns it ON for all of them. `off` swaps in a distinct glyph while the
31779
+ * toggle is off (a lock reads as an open padlock until it is engaged); mixed shows `icon`. */
31780
+ toggle(tip, icon2, active, onChange, off) {
31547
31781
  const b = this.base(tip);
31548
- b.innerHTML = sized(icon2);
31549
31782
  const set = (on2) => {
31550
- b.dataset.active = on2 ? "1" : "0";
31783
+ b.dataset.active = on2 === MIXED ? "mixed" : on2 ? "1" : "0";
31784
+ b.innerHTML = sized(off && on2 === false ? off : icon2);
31551
31785
  };
31552
31786
  set(active);
31553
31787
  let on = active;
31554
31788
  b.addEventListener("click", () => {
31555
- on = !on;
31789
+ on = on === MIXED ? true : !on;
31556
31790
  set(on);
31557
31791
  onChange(on);
31558
31792
  });
31559
31793
  return b;
31560
31794
  }
31561
31795
  /** 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. */
31796
+ * ladder — the value is clamped to the schema's declared min/max on commit. A mixed
31797
+ * value shows an empty field with a dash until a number is typed for all. */
31563
31798
  widthInput(tip, value, min, max, step, onChange) {
31564
31799
  const ni = new NumberInput({
31565
- value,
31800
+ value: value === MIXED ? min : value,
31566
31801
  min,
31567
31802
  max,
31568
31803
  step,
@@ -31574,13 +31809,18 @@ var DrawingSettingsPopup = class {
31574
31809
  title: tip,
31575
31810
  onChange
31576
31811
  });
31812
+ if (value === MIXED) {
31813
+ ni.input.value = "";
31814
+ ni.input.placeholder = "\u2014";
31815
+ }
31577
31816
  ni.el.dataset.tip = tip;
31578
31817
  trapChartKeys(ni.el);
31579
31818
  return ni.el;
31580
31819
  }
31581
31820
  /** A pick-one dropdown: the trigger shows the current value's glyph (plus an optional inline
31582
31821
  * 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). */
31822
+ * opens a floating list of every option so a value is one click away (no cycling through).
31823
+ * A mixed value shows a dash in the trigger and highlights no row. */
31584
31824
  dropdown(tip, values, current, render, onChange, opts = {}) {
31585
31825
  const b = this.base(tip);
31586
31826
  b.style.width = "auto";
@@ -31590,14 +31830,20 @@ var DrawingSettingsPopup = class {
31590
31830
  let cur = current;
31591
31831
  const paint = (v) => {
31592
31832
  b.replaceChildren();
31593
- const glyph = render(v);
31833
+ if (v === MIXED) {
31834
+ const dash = document.createElement("span");
31835
+ dash.textContent = "\u2014";
31836
+ dash.style.cssText = "min-width:18px;text-align:center;opacity:0.85;";
31837
+ b.appendChild(dash);
31838
+ }
31839
+ const glyph = v === MIXED ? "" : render(v);
31594
31840
  if (glyph) {
31595
31841
  const ic = document.createElement("span");
31596
31842
  ic.style.cssText = "display:flex;";
31597
31843
  ic.innerHTML = sized(glyph);
31598
31844
  b.appendChild(ic);
31599
31845
  }
31600
- if (opts.label && opts.labelInTrigger) {
31846
+ if (opts.label && opts.labelInTrigger && v !== MIXED) {
31601
31847
  const tx = document.createElement("span");
31602
31848
  tx.textContent = opts.label(v);
31603
31849
  tx.style.cssText = "font-size:11px;opacity:0.85;font-variant-numeric:tabular-nums;";
@@ -31667,8 +31913,10 @@ var DrawingSettingsPopup = class {
31667
31913
  }
31668
31914
  /** A color control: an `icon` over a thin **colored underline** showing the current
31669
31915
  * 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) {
31916
+ * `iconSize` shrinks it for the floating text controls. A mixed color paints the
31917
+ * underline striped with the colors `used` across the selection, and the picker then
31918
+ * leads with those colors so unifying onto one of them is a single click. */
31919
+ colorButton(tip, icon2, color, onChange, iconSize = 17, used = []) {
31672
31920
  const b = document.createElement("button");
31673
31921
  b.type = "button";
31674
31922
  b.dataset.tip = tip;
@@ -31678,13 +31926,14 @@ var DrawingSettingsPopup = class {
31678
31926
  ic.style.cssText = "display:flex;";
31679
31927
  ic.innerHTML = sized(icon2, iconSize);
31680
31928
  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};`;
31929
+ bar.style.cssText = `display:block;height:3px;width:${Math.round(iconSize * 0.85)}px;border-radius:2px;background:${color === MIXED ? stripes(used) : color};`;
31682
31930
  b.append(ic, bar);
31683
31931
  let cur = color;
31684
31932
  b.addEventListener("pointerdown", (e) => e.stopPropagation());
31685
31933
  b.addEventListener("click", (e) => {
31686
31934
  e.stopPropagation();
31687
- this.toggleColorPopover(b, cur, (v) => {
31935
+ const seed = cur === MIXED ? used[0] ?? DEFAULT_DRAWING_COLOR : cur;
31936
+ this.toggleColorPopover(b, seed, cur === MIXED ? used : [], (v) => {
31688
31937
  cur = v;
31689
31938
  bar.style.background = v;
31690
31939
  onChange(v);
@@ -31693,20 +31942,42 @@ var DrawingSettingsPopup = class {
31693
31942
  return b;
31694
31943
  }
31695
31944
  /** Open the color popover for `anchor`, or close it if it's already this anchor's (toggle). */
31696
- toggleColorPopover(anchor, color, onChange) {
31945
+ toggleColorPopover(anchor, color, used, onChange) {
31697
31946
  if (this.colorOwner === anchor) {
31698
31947
  this.closeColorPopover();
31699
31948
  return;
31700
31949
  }
31701
- this.openColorPopover(anchor, color, onChange);
31950
+ this.openColorPopover(anchor, color, used, onChange);
31702
31951
  }
31703
- /** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`. */
31704
- openColorPopover(anchor, color, onChange) {
31952
+ /** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`.
31953
+ * `used` (a mixed swatch's colors) leads the picker as one-click unify shortcuts. */
31954
+ openColorPopover(anchor, color, used, onChange) {
31705
31955
  const t = this.theme;
31706
31956
  this.colorPop = this.hostFloat(anchor, {
31707
31957
  zIndex: 25,
31708
31958
  padding: "10px",
31709
- fill: (el) => {
31959
+ fill: (el, pop) => {
31960
+ if (used.length > 1) {
31961
+ const row = document.createElement("div");
31962
+ row.style.cssText = "display:flex;align-items:center;gap:6px;padding-bottom:9px;border-bottom:1px solid var(--vela-border);";
31963
+ const lbl = document.createElement("span");
31964
+ lbl.textContent = "In use";
31965
+ lbl.style.cssText = "font:var(--vela-font-size-sm) inherit;opacity:0.7;margin-right:2px;";
31966
+ row.appendChild(lbl);
31967
+ for (const c of used) {
31968
+ const sw = document.createElement("button");
31969
+ sw.type = "button";
31970
+ sw.dataset.tip = c;
31971
+ sw.style.cssText = `width:18px;height:18px;border-radius:4px;border:1px solid var(--vela-border-strong);cursor:pointer;background:${c};padding:0;`;
31972
+ sw.addEventListener("click", (e) => {
31973
+ e.stopPropagation();
31974
+ pop.hide();
31975
+ onChange(c);
31976
+ });
31977
+ row.appendChild(sw);
31978
+ }
31979
+ el.appendChild(row);
31980
+ }
31710
31981
  el.appendChild(buildColorPicker(color, t, onChange));
31711
31982
  }
31712
31983
  });
@@ -31727,6 +31998,7 @@ var TYPE_ICON = icon("type");
31727
31998
  var PRICE_DELTA_ICON = icon("price-delta");
31728
31999
  var DATE_DELTA_ICON = icon("date-delta");
31729
32000
  var LOCK_ICON = icon("lock");
32001
+ var UNLOCK_ICON = icon("unlock");
31730
32002
  var FRONT_ICON = icon("bring-front");
31731
32003
  var BACK_ICON = icon("send-back");
31732
32004
  var TRASH_ICON = icon("trash");
@@ -31734,6 +32006,7 @@ var BOLD_ICON = icon("bold");
31734
32006
  var ITALIC_ICON = icon("italic");
31735
32007
  var GRIP_ICON = icon("grip");
31736
32008
  var KEBAB_ICON = icon("kebab");
32009
+ var CLONE_ICON = icon("clone");
31737
32010
  var RESET_ICON = icon("reset");
31738
32011
  var GEAR_ICON = icon("gear");
31739
32012
  var CHEVRON_ICON = icon("chevron-down");
@@ -31742,6 +32015,12 @@ var BANDS_ICON = icon("bands");
31742
32015
  var DEDEKIND_ICON2 = icon("dedekind");
31743
32016
  var SONIC_ICON2 = icon("sonic");
31744
32017
  var SUPERSONIC_ICON2 = icon("supersonic");
32018
+ function stripes(colors) {
32019
+ const bands = colors.length > 1 ? colors : ["var(--vela-fg-faint)", "var(--vela-fg-muted)"];
32020
+ const step = 100 / bands.length;
32021
+ const stops = bands.map((c, i) => `${c} ${i * step}% ${(i + 1) * step}%`).join(",");
32022
+ return `linear-gradient(90deg,${stops})`;
32023
+ }
31745
32024
  function lineIcon(width, style) {
31746
32025
  const dash = style === "dashed" ? "5,3" : style === "dotted" ? "1.5,3" : "";
31747
32026
  return svg24(`<line x1="3.5" y1="12" x2="20.5" y2="12" stroke-width="${Number(width) || 2}" stroke-dasharray="${dash}"/>`);
@@ -32025,6 +32304,20 @@ var UserDrawingController = class {
32025
32304
  // reported exactly when it actually changes).
32026
32305
  this.textEditor = null;
32027
32306
  this.painter = new DrawingPainter();
32307
+ /** The open popup is the multi-selection bar (vs one drawing's own) — decides what a
32308
+ * shrinking selection does with it. */
32309
+ this.popupMulti = false;
32310
+ /** Dismiss-on-outside-press also clears the selection — except when the press is ABOUT the
32311
+ * selection: it carries a multi-select modifier (Shift / Ctrl / Cmd — it will add to the
32312
+ * selection or sweep a marquee onto it), or it landed on a drawing that is already selected
32313
+ * (the canvas handler ran first and is holding it for a drag or a click), so the drawings
32314
+ * whose bar just closed must stay selected. */
32315
+ this.onPopupDismissed = (e) => {
32316
+ if (e && (e.shiftKey || e.ctrlKey || e.metaKey)) return;
32317
+ const pressed = this.interaction.pressedId();
32318
+ if (pressed && this.selectedIds.has(pressed)) return;
32319
+ this.clearSelection();
32320
+ };
32028
32321
  this.ctx = canvas.getContext("2d");
32029
32322
  this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), () => deps.chartBarMs());
32030
32323
  this.toolbar = new DrawingToolbar(
@@ -32201,6 +32494,12 @@ var UserDrawingController = class {
32201
32494
  }
32202
32495
  setSelection(ids) {
32203
32496
  this.selectedIds = new Set(ids);
32497
+ if (ids.length >= 2) this.openSettingsForSelection(ids);
32498
+ else if (this.popupMulti) {
32499
+ this.popupMulti = false;
32500
+ this.popup.close();
32501
+ if (ids.length === 1) this.openSettingsById(ids[0], 0, 0);
32502
+ }
32204
32503
  this.render();
32205
32504
  }
32206
32505
  /** Programmatic twin of clicking the drawing: highlight it + float its settings popup. */
@@ -32246,12 +32545,16 @@ var UserDrawingController = class {
32246
32545
  });
32247
32546
  }
32248
32547
  /** 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) {
32548
+ * Shared by the eraser (click + drag) and the middle-click shortcut; the latter passes
32549
+ * `withSelection` so a hit on a member of a multi-selection removes the selection's
32550
+ * unlocked members — whichever member was hit, locked or not. */
32551
+ deleteAt(x, y, withSelection = false) {
32251
32552
  const hit = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE);
32252
- if (!hit || hit.locked) return false;
32553
+ if (!hit) return false;
32554
+ const ids = deleteTargets(hit, this.selectedIds, this.drawings, withSelection);
32555
+ if (ids.length === 0) return false;
32253
32556
  this.popup.close();
32254
- this.emit({ kind: "delete", ids: [hit.id] });
32557
+ this.emit({ kind: "delete", ids });
32255
32558
  return true;
32256
32559
  }
32257
32560
  /** Shift+press on the empty plot: arm the measure ruler AND start it at (x, y) in one
@@ -32277,7 +32580,14 @@ var UserDrawingController = class {
32277
32580
  this.render();
32278
32581
  }
32279
32582
  }
32280
- pointerDown(x, y, snap = "off", shift2 = false) {
32583
+ /** Ctrl/Cmd+press on the empty plot: sweep a selection box (adds the drawings it touches to
32584
+ * the selection). Returns false when a mode/tool is active (the normal press path owns it). */
32585
+ beginMarqueeAt(x, y) {
32586
+ if (this.measureMode || this.eraserMode || !this.interaction.beginMarquee(x, y)) return false;
32587
+ this.render();
32588
+ return true;
32589
+ }
32590
+ pointerDown(x, y, snap = "off", shift2 = false, mod = false) {
32281
32591
  if (this.eraserMode) {
32282
32592
  this.erasing = true;
32283
32593
  this.deleteAt(x, y);
@@ -32297,9 +32607,9 @@ var UserDrawingController = class {
32297
32607
  return;
32298
32608
  }
32299
32609
  }
32300
- this.interaction.down(x, y, snap, shift2);
32610
+ this.interaction.down(x, y, snap, shift2, mod);
32301
32611
  }
32302
- pointerMove(x, y, snap = "off", shift2 = false) {
32612
+ pointerMove(x, y, snap = "off", shift2 = false, mod = false) {
32303
32613
  if (this.eraserMode) {
32304
32614
  if (this.erasing) this.deleteAt(x, y);
32305
32615
  return;
@@ -32311,12 +32621,14 @@ var UserDrawingController = class {
32311
32621
  return;
32312
32622
  }
32313
32623
  this.interaction.move(x, y, snap, shift2);
32314
- this.updateHover(x, y);
32624
+ this.updateHover(x, y, mod);
32315
32625
  }
32316
- /** Track which drawing is hovered so its handles appear on hover (and only then). */
32317
- updateHover(x, y) {
32626
+ /** Track which drawing is hovered so its handles appear on hover (and only then). While
32627
+ * Ctrl/Cmd is held the cursor is picking a selection — handles then mark only what IS
32628
+ * selected, so a hovered candidate doesn't already read as selected. */
32629
+ updateHover(x, y, mod = false) {
32318
32630
  let id = null;
32319
- if (this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
32631
+ if (!mod && this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
32320
32632
  id = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE)?.id ?? null;
32321
32633
  }
32322
32634
  if (id !== this.hoveredId) {
@@ -32336,7 +32648,12 @@ var UserDrawingController = class {
32336
32648
  this.render();
32337
32649
  return;
32338
32650
  }
32651
+ const dragged = this.interaction.isDragging() ? this.interaction.pressedId() : null;
32339
32652
  this.interaction.up(x, y);
32653
+ if (dragged && this.selectedIds.has(dragged) && !this.popup.isOpen()) {
32654
+ if (this.selectedIds.size >= 2) this.openSettingsForSelection(this.selectionIds());
32655
+ else this.openSettingsById(dragged, x, y);
32656
+ }
32340
32657
  }
32341
32658
  /** Toggle the transient ruler. Arming it clears any drawing tool + selection. */
32342
32659
  toggleMeasure() {
@@ -32601,7 +32918,7 @@ var UserDrawingController = class {
32601
32918
  this.emit({ kind: "duplicate", ids: this.selectionIds() });
32602
32919
  break;
32603
32920
  case "delete": {
32604
- const ids = this.selectedIds.size ? this.selectionIds() : this.hoveredId ? [this.hoveredId] : [];
32921
+ const ids = this.selectedIds.size ? this.deletableSelection() : this.hoveredId ? [this.hoveredId] : [];
32605
32922
  if (ids.length) {
32606
32923
  this.popup.close();
32607
32924
  this.emit({ kind: "delete", ids });
@@ -32617,6 +32934,9 @@ var UserDrawingController = class {
32617
32934
  selectionIds() {
32618
32935
  return [...this.selectedIds];
32619
32936
  }
32937
+ deletableSelection() {
32938
+ return deletableSelection(this.selectedIds, this.drawings);
32939
+ }
32620
32940
  /** Move every selected (unlocked) drawing by a pixel delta — one edit/edit-many → one undo step. */
32621
32941
  nudgeSelection(dx, dy) {
32622
32942
  const proj = this.deps.projector();
@@ -32725,6 +33045,8 @@ var UserDrawingController = class {
32725
33045
  this.painter.seriesLook = this.deps.seriesLook();
32726
33046
  this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
32727
33047
  this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
33048
+ const clones = this.interaction.dragClones();
33049
+ if (clones) this.painter.paintAll(ctx, clones, proj, this.deps.theme(), { selected: new Set(clones.map((c) => c.id)) });
32728
33050
  this.layoutTextEditor();
32729
33051
  const ghost = this.interaction.ghost();
32730
33052
  if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
@@ -32740,6 +33062,8 @@ var UserDrawingController = class {
32740
33062
  const my = m ? proj.yOf(m.point.price, m.paneId) : null;
32741
33063
  if (m && my != null) this.painter.paintSnapRing(ctx, proj.xOf(m.point.time), my, this.deps.theme());
32742
33064
  if (this.measure.isActive()) this.measure.paint(ctx, proj, this.deps.theme());
33065
+ const marquee = this.interaction.marqueeRect();
33066
+ if (marquee) this.painter.paintMarquee(ctx, marquee);
32743
33067
  }
32744
33068
  destroy() {
32745
33069
  this.closeTextEditor();
@@ -32757,15 +33081,80 @@ var UserDrawingController = class {
32757
33081
  if (this.selectedIds.size) this.emit({ kind: "select", ids: [] });
32758
33082
  this.render();
32759
33083
  }
33084
+ /** Report the live drawings' current state as one edit (one undo step, however many). */
33085
+ emitEdits(drawings) {
33086
+ const docs = drawings.map((d) => d.serialize());
33087
+ if (docs.length === 1) this.emit({ kind: "edit", doc: docs[0] });
33088
+ else if (docs.length > 1) this.emit({ kind: "edit-many", docs });
33089
+ }
33090
+ /** The one bar for a multi-selection: it shows the controls all of its drawings share and
33091
+ * every action applies to all of them. Floats clear of their combined bounds. */
33092
+ openSettingsForSelection(ids) {
33093
+ const live = () => ids.map((id) => this.drawings.find((d) => d.id === id)).filter((d) => d != null);
33094
+ const drawings = live();
33095
+ if (drawings.length < 2) return;
33096
+ const proj = this.deps.projector();
33097
+ let anchor = null;
33098
+ for (const d of drawings) {
33099
+ const b = d.bounds(proj);
33100
+ if (!b) continue;
33101
+ if (!anchor) anchor = { ...b };
33102
+ else {
33103
+ const right = Math.max(anchor.x + anchor.w, b.x + b.w);
33104
+ const bottom = Math.max(anchor.y + anchor.h, b.y + b.h);
33105
+ anchor.x = Math.min(anchor.x, b.x);
33106
+ anchor.y = Math.min(anchor.y, b.y);
33107
+ anchor.w = right - anchor.x;
33108
+ anchor.h = bottom - anchor.y;
33109
+ }
33110
+ }
33111
+ this.popupMulti = true;
33112
+ this.emit({ kind: "settings", id: drawings[0].id });
33113
+ this.popup.open(drawings, anchor, {
33114
+ resolve: () => live()[0] ?? null,
33115
+ patch: (p) => {
33116
+ const ds = live();
33117
+ for (const d of ds) d.applySettings(p);
33118
+ this.render();
33119
+ this.emitEdits(ds);
33120
+ },
33121
+ setLocked: (v) => {
33122
+ const ds = live();
33123
+ for (const d of ds) d.locked = v;
33124
+ this.emitEdits(ds);
33125
+ },
33126
+ reorder: (to) => {
33127
+ for (const d of live()) this.emit({ kind: "reorder", id: d.id, to });
33128
+ },
33129
+ duplicate: () => {
33130
+ this.popup.close();
33131
+ this.emit({ kind: "duplicate", ids: live().map((d) => d.id) });
33132
+ },
33133
+ resetSettings: () => {
33134
+ const ds = live();
33135
+ for (const d of ds) resetDrawingSettings(d);
33136
+ this.render();
33137
+ this.emitEdits(ds);
33138
+ this.openSettingsForSelection(ids);
33139
+ },
33140
+ remove: () => {
33141
+ const ids2 = live().filter((d) => !d.locked).map((d) => d.id);
33142
+ if (ids2.length === 0) return;
33143
+ this.popup.close();
33144
+ this.emit({ kind: "delete", ids: ids2 });
33145
+ }
33146
+ }, this.onPopupDismissed);
33147
+ }
32760
33148
  /** A click on a drawing opens its settings toolbar (text labels edit text here too). */
32761
33149
  openSettingsById(id, _x, _y) {
32762
33150
  const drawing = this.drawings.find((d) => d.id === id);
32763
33151
  if (!drawing) return;
32764
33152
  const live = () => this.drawings.find((d) => d.id === id);
33153
+ this.popupMulti = false;
32765
33154
  this.emit({ kind: "select", ids: [id] });
32766
33155
  this.emit({ kind: "settings", id });
32767
33156
  const anchor = drawing.bounds(this.deps.projector());
32768
- this.popup.open(drawing, anchor, {
33157
+ this.popup.open([drawing], anchor, {
32769
33158
  // Sync rebuilds instances, so a panel that reads values back after a patch (e.g. the
32770
33159
  // position tool's price fields, where one edit can flip another level) resolves fresh.
32771
33160
  resolve: () => live() ?? null,
@@ -32783,6 +33172,11 @@ var UserDrawingController = class {
32783
33172
  this.emit({ kind: "edit", doc: d.serialize() });
32784
33173
  },
32785
33174
  reorder: (to) => this.emit({ kind: "reorder", id, to }),
33175
+ duplicate: () => {
33176
+ this.finishTextEditor(true);
33177
+ this.popup.close();
33178
+ this.emit({ kind: "duplicate", ids: [id] });
33179
+ },
32786
33180
  resetSettings: () => {
32787
33181
  const d = live();
32788
33182
  if (!d) return;
@@ -32805,7 +33199,13 @@ var UserDrawingController = class {
32805
33199
  this.popup.close();
32806
33200
  this.emit({ kind: "delete", ids: [id] });
32807
33201
  }
32808
- }, () => this.clearSelection());
33202
+ }, this.onPopupDismissed);
33203
+ }
33204
+ /** A plain click on the empty plot: drop the selection. The popup's dismiss-on-outside-press
33205
+ * does the same, but a selection can exist with no bar open (a lone Ctrl-click pick, a
33206
+ * selection whose bar was dismissed by a modifier press) — this covers those. */
33207
+ deselect() {
33208
+ this.clearSelection();
32809
33209
  }
32810
33210
  /** Report placement progress upstream (`draft` intent) so the drawings sync can
32811
33211
  * mirror the ghost live on linked charts. Called from every render; only actual
@@ -32831,6 +33231,13 @@ var UserDrawingController = class {
32831
33231
  }, 0);
32832
33232
  return;
32833
33233
  }
33234
+ if (i.kind === "clone") {
33235
+ const before = new Set(this.drawings.map((d) => d.id));
33236
+ this.intentCb?.(i);
33237
+ const fresh = this.drawings.filter((d) => !before.has(d.id));
33238
+ if (fresh.length === 1) this.openSettingsById(fresh[0].id, 0, 0);
33239
+ return;
33240
+ }
32834
33241
  this.intentCb?.(i);
32835
33242
  }
32836
33243
  };
@@ -35006,7 +35413,10 @@ var NativeRenderer = class {
35006
35413
  zoomTo: (target, anchorLogical, anchorX) => this.zoomTo(target, anchorLogical, anchorX),
35007
35414
  fling: (v) => this.fling(v),
35008
35415
  onPointerMove: (x, y) => this.handlePointerMove(x, y),
35009
- onClick: (x) => this.handleClick(x),
35416
+ onClick: (x) => {
35417
+ this.userDrawings?.deselect();
35418
+ this.handleClick(x);
35419
+ },
35010
35420
  onAxisLongPress: (axis, x, y) => {
35011
35421
  for (const cb of this.axisLongPressCbs) cb({ axis, x, y });
35012
35422
  },
@@ -35024,11 +35434,13 @@ var NativeRenderer = class {
35024
35434
  // User drawings claim a gesture before pan when armed / over a drawing.
35025
35435
  drawingsClaim: (x, y) => this.userDrawings?.claim(x, y) ?? false,
35026
35436
  drawingsMeasureStart: (x, y, snap) => this.userDrawings?.beginMeasureAt(x, y, snap) ?? false,
35027
- drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y) ?? false,
35437
+ drawingsMarqueeStart: (x, y) => this.userDrawings?.beginMarqueeAt(x, y) ?? false,
35438
+ drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y, true) ?? false,
35439
+ // middle-click: a selected hit takes the whole selection
35028
35440
  drawingsCancelPlacement: () => this.userDrawings?.cancelPlacement() ?? false,
35029
35441
  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),
35442
+ drawingsPointerDown: (x, y, snap, shift2, mod) => this.userDrawings?.pointerDown(x, y, snap, shift2, mod),
35443
+ drawingsPointerMove: (x, y, snap, shift2, mod) => this.userDrawings?.pointerMove(x, y, snap, shift2, mod),
35032
35444
  drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
35033
35445
  drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
35034
35446
  drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
@@ -37362,6 +37774,10 @@ function str3(inputs, key, fallback) {
37362
37774
  const v = inputs[key];
37363
37775
  return typeof v === "string" && v !== "" ? v : fallback;
37364
37776
  }
37777
+ function bool(inputs, key, fallback) {
37778
+ const v = inputs[key];
37779
+ return typeof v === "boolean" ? v : fallback;
37780
+ }
37365
37781
  function toPoints(bars, values, colors) {
37366
37782
  const pts = new Array(bars.length);
37367
37783
  for (let i = 0; i < bars.length; i++) {
@@ -38893,10 +39309,6 @@ var aroon = {
38893
39309
  plots: [
38894
39310
  { key: "up", title: "Aroon up", values: up, color: BULLISH, width: 2 },
38895
39311
  { 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
39312
  ]
38901
39313
  };
38902
39314
  }
@@ -39394,8 +39806,10 @@ var volumeSpecs = [
39394
39806
  var DAY_MS2 = 864e5;
39395
39807
  function periodKey(anchor, time) {
39396
39808
  if (anchor === "Week") return Math.floor((Math.floor(time / DAY_MS2) + 3) / 7);
39397
- if (anchor === "Month") {
39809
+ if (anchor === "Month" || anchor === "Quarter" || anchor === "Year") {
39398
39810
  const d = new Date(time);
39811
+ if (anchor === "Year") return d.getUTCFullYear();
39812
+ if (anchor === "Quarter") return d.getUTCFullYear() * 4 + Math.floor(d.getUTCMonth() / 3);
39399
39813
  return d.getUTCFullYear() * 12 + d.getUTCMonth();
39400
39814
  }
39401
39815
  return Math.floor(time / DAY_MS2);
@@ -39447,40 +39861,86 @@ var parabolicSar = {
39447
39861
  return { plots: [{ key: "sar", title: "PSAR", values, kind: "circles", color: SERIES_LINE, colors }] };
39448
39862
  }
39449
39863
  };
39864
+ var VWAP_BANDS_GROUP = "Bands";
39865
+ var VWAP_STYLE = "Style";
39866
+ var VWAP_BANDS = [
39867
+ { on: "band1", mult: "band1Mult", color: "band1Color", fill: "band1Fill", fillColor: "band1FillColor", defMult: 1, defOn: true, defColor: BULLISH },
39868
+ { on: "band2", mult: "band2Mult", color: "band2Color", fill: "band2Fill", fillColor: "band2FillColor", defMult: 2, defOn: false, defColor: WARNING },
39869
+ { on: "band3", mult: "band3Mult", color: "band3Color", fill: "band3Fill", fillColor: "band3FillColor", defMult: 3, defOn: false, defColor: CATEGORICAL[4] }
39870
+ ];
39450
39871
  var vwap = {
39451
39872
  type: "vwap",
39452
39873
  title: "Volume Weighted Average Price",
39453
39874
  shortTitle: "VWAP",
39454
39875
  overlay: true,
39455
39876
  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)
39877
+ { key: "anchor", title: "Period", type: "string", defval: "Day", options: ["Day", "Week", "Month", "Quarter", "Year"], tooltip: "Where the accumulation resets (UTC periods)" },
39878
+ sourceInput("HLC3"),
39879
+ // Each band is one row: its toggle leads, the multiplier follows unlabeled.
39880
+ ...VWAP_BANDS.flatMap((b, i) => [
39881
+ { key: b.on, title: `Band ${i + 1} multiplier`, type: "bool", defval: b.defOn, inline: b.on, group: VWAP_BANDS_GROUP },
39882
+ { key: b.mult, title: "", type: "float", defval: b.defMult, min: 0.1, max: 10, step: 0.1, inline: b.on, group: VWAP_BANDS_GROUP }
39883
+ ]),
39884
+ { ...colorInput(INFO, "VWAP color"), group: VWAP_STYLE },
39885
+ // One row per band: its ink, then the fill toggle with the fill's own (translucent) color.
39886
+ ...VWAP_BANDS.flatMap((b, i) => [
39887
+ { ...colorInput(b.defColor, `Band ${i + 1} color`, b.color), inline: b.color, group: VWAP_STYLE },
39888
+ { key: b.fill, title: "Fill", type: "bool", defval: true, inline: b.color, group: VWAP_STYLE },
39889
+ { ...colorInput(withAlpha2(b.defColor), "", b.fillColor), inline: b.color, group: VWAP_STYLE }
39890
+ ])
39459
39891
  ],
39460
39892
  compute: (bars, inputs) => {
39461
39893
  const anchor = str3(inputs, "anchor", "Day");
39462
- const source = str3(inputs, "source", "HLC3");
39463
- const values = new Array(bars.length).fill(Number.NaN);
39894
+ const src = sourceValues(bars, str3(inputs, "source", "HLC3"));
39895
+ const n = bars.length;
39896
+ const values = new Array(n).fill(Number.NaN);
39897
+ const bands = VWAP_BANDS.filter((b) => bool(inputs, b.on, b.defOn)).map((b) => ({
39898
+ mult: Math.max(0, num3(inputs, b.mult, b.defMult)),
39899
+ color: str3(inputs, b.color, b.defColor),
39900
+ fill: bool(inputs, b.fill, true),
39901
+ fillColor: str3(inputs, b.fillColor, withAlpha2(b.defColor)),
39902
+ up: new Array(n).fill(Number.NaN),
39903
+ down: new Array(n).fill(Number.NaN)
39904
+ }));
39464
39905
  let period = Number.NaN;
39465
39906
  let cumPV = 0;
39907
+ let cumPV2 = 0;
39466
39908
  let cumV = 0;
39467
- for (let i = 0; i < bars.length; i++) {
39909
+ for (let i = 0; i < n; i++) {
39468
39910
  const b = bars[i];
39469
39911
  const key = periodKey(anchor, b.time);
39470
39912
  if (key !== period) {
39471
39913
  period = key;
39472
39914
  cumPV = 0;
39915
+ cumPV2 = 0;
39473
39916
  cumV = 0;
39474
39917
  }
39475
39918
  const v = b.volume;
39476
39919
  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;
39920
+ const tp = src[i];
39478
39921
  cumPV += tp * v;
39922
+ cumPV2 += tp * tp * v;
39479
39923
  cumV += v;
39480
39924
  }
39481
- if (cumV > 0) values[i] = cumPV / cumV;
39482
- }
39483
- return { plots: [{ key: "vwap", title: "VWAP", values, color: str3(inputs, "color", INFO), width: 2 }] };
39925
+ if (cumV <= 0) continue;
39926
+ const mean = cumPV / cumV;
39927
+ const sd = Math.sqrt(Math.max(0, cumPV2 / cumV - mean * mean));
39928
+ values[i] = mean;
39929
+ for (const band of bands) {
39930
+ band.up[i] = mean + band.mult * sd;
39931
+ band.down[i] = mean - band.mult * sd;
39932
+ }
39933
+ }
39934
+ const plots = [{ key: "vwap", title: "VWAP", values, color: str3(inputs, "color", INFO), width: 2 }];
39935
+ const fills = [];
39936
+ bands.forEach((band, i) => {
39937
+ plots.push(
39938
+ { key: `up${i}`, title: `Upper ${band.mult}\u03C3`, values: band.up, color: band.color },
39939
+ { key: `down${i}`, title: `Lower ${band.mult}\u03C3`, values: band.down, color: band.color }
39940
+ );
39941
+ if (band.fill) fills.push({ key: `band${i}`, from: `up${i}`, to: `down${i}`, color: band.fillColor });
39942
+ });
39943
+ return { plots, bands: fills };
39484
39944
  }
39485
39945
  };
39486
39946
  var pivotPoints = {
@@ -39994,6 +40454,24 @@ function resolveElement(container) {
39994
40454
  function segmentVisibility(parts, chartHidden = false) {
39995
40455
  return { avatar: parts.logo, symbol: parts.name, meta: parts.name, market: parts.market, ohlc: parts.ohlc && !chartHidden, change: parts.change && !chartHidden, eye: chartHidden };
39996
40456
  }
40457
+ var STATUSLINE_LADDER = [
40458
+ { stacked: false, level: "full" },
40459
+ { stacked: true, level: "full" },
40460
+ { stacked: true, level: "compact" },
40461
+ { stacked: true, level: "minimal" }
40462
+ ];
40463
+ function readoutCells(level, readout) {
40464
+ if (readout === "value") return [{ key: "close", label: "" }];
40465
+ if (level === "full") {
40466
+ return [
40467
+ { key: "open", label: "O" },
40468
+ { key: "high", label: "H" },
40469
+ { key: "low", label: "L" },
40470
+ { key: "close", label: "C" }
40471
+ ];
40472
+ }
40473
+ return [{ key: "close", label: level === "compact" ? "C" : "" }];
40474
+ }
39997
40475
  function statuslineMenuItems(parts, chartVisible) {
39998
40476
  return [
39999
40477
  { id: "part:logo", label: "Symbol logo", checked: parts.logo },
@@ -40022,6 +40500,11 @@ function fmtChange(open2, close) {
40022
40500
  const sign = diff >= 0 ? "+" : "";
40023
40501
  return `${sign}${fmtPrice(diff, decimalsFor(close))} (${sign}${pct.toFixed(2)}%)`;
40024
40502
  }
40503
+ function fmtChangePct(open2, close) {
40504
+ if (open2 == null || close == null || !Number.isFinite(open2) || !Number.isFinite(close) || open2 === 0) return "";
40505
+ const pct = (close - open2) / open2 * 100;
40506
+ return `${pct >= 0 ? "+" : ""}${pct.toFixed(2)}%`;
40507
+ }
40025
40508
 
40026
40509
  // src/widget/statusline.ts
40027
40510
  var STYLE_ID24 = "vela-widget-statusline";
@@ -40038,6 +40521,11 @@ var CSS21 = `
40038
40521
  display: flex;
40039
40522
  align-items: baseline;
40040
40523
  gap: var(--vela-space-2);
40524
+ /* The chip never grows past the plot: fit() measures overflow against this width to
40525
+ * walk the layout ladder (stack, then shed values), so overflow:hidden only guards
40526
+ * the transient between a resize and the next measure. */
40527
+ max-width: calc(100% - var(--vela-toolbar-gutter, 0px) - var(--vela-scale-gutter, 0px) - 24px);
40528
+ overflow: hidden;
40041
40529
  color: var(--vela-fg);
40042
40530
  font-size: var(--vela-font-size-md);
40043
40531
  /* Same chip treatment as the indicator legend rows (InputsUI): a translucent wash of
@@ -40059,6 +40547,31 @@ var CSS21 = `
40059
40547
  /* Chart hidden (the price series' eye \u2014 renderer 'candleVisible'): the line dims to
40060
40548
  * the same 0.5 wash a hidden indicator's legend row wears. */
40061
40549
  .vela-statusline.vela-sl-chart-hidden { opacity: 0.5; }
40550
+ /* Two rows inside the chip \u2014 the identity (logo / symbol / meta / market badge) and the
40551
+ * value readout (O/H/L/C + change, or the show-chart eye). In the widest layout they sit
40552
+ * side by side on one line; the STACKED layouts (fit() decides, see the ladder in
40553
+ * statusline-model) turn the chip into a column so the values drop under the symbol. */
40554
+ .vela-statusline .vela-sl-row {
40555
+ display: flex;
40556
+ align-items: baseline;
40557
+ gap: var(--vela-space-2);
40558
+ white-space: nowrap;
40559
+ }
40560
+ .vela-statusline.vela-sl-stacked {
40561
+ flex-direction: column;
40562
+ align-items: flex-start;
40563
+ /* The same air between the two rows as between the values row and the legend row
40564
+ * that follows the chip (see the stacked legend shift below). */
40565
+ row-gap: var(--vela-space-1);
40566
+ }
40567
+ /* Stacked, the identity row may shrink (the meta carries the ellipsis) \u2014 the values row
40568
+ * never does, so its overflow is what fit() measures to keep descending the ladder. */
40569
+ .vela-statusline.vela-sl-stacked .vela-sl-identity { max-width: 100%; }
40570
+ .vela-statusline.vela-sl-stacked .vela-sl-meta {
40571
+ min-width: 0;
40572
+ overflow: hidden;
40573
+ text-overflow: ellipsis;
40574
+ }
40062
40575
  .vela-statusline .vela-sl-avatar {
40063
40576
  width: 18px;
40064
40577
  height: 18px;
@@ -40110,59 +40623,13 @@ var CSS21 = `
40110
40623
  * stylesheet is document-global, so a bare attribute selector here would shift every
40111
40624
  * chart on the page, including statusline-less ones. */
40112
40625
  .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); }
40626
+ /* A stacked chip is two lines tall \u2014 the legend shifts one more line. fit() flags the
40627
+ * host whenever the ladder lands on a stacked layout. */
40628
+ .vela-has-statusline.vela-sl-stacked-host [${LEGEND_AT_TOP_ATTR}] { transform: translateY(40px); }
40629
+ /* Mobile: the same ladder at the phone's type scale \u2014 the width alone decides how much
40630
+ * of the readout fits, exactly as on a narrow desktop chart. */
40631
+ [data-layout='mobile'] .vela-statusline { font-size: var(--vela-font-size-sm); }
40632
+ [data-layout='mobile'] .vela-statusline .vela-sl-symbol { font-size: var(--vela-font-size-md); }
40166
40633
  `;
40167
40634
  function baseOfTicker(ticker) {
40168
40635
  return ticker.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || ticker;
@@ -40233,6 +40700,8 @@ var Statusline = class {
40233
40700
  /** Fit mode (multi-chart cells): one row, overflowing segments hidden — see {@link setFitMode}. */
40234
40701
  this.fitMode = false;
40235
40702
  this.fitRO = null;
40703
+ /** The ladder rung currently applied — see {@link fit}. */
40704
+ this.layout = { stacked: false, level: "full" };
40236
40705
  this.onContextMenu = (e) => {
40237
40706
  if (!this.menu || !this.menuHooks) return;
40238
40707
  e.preventDefault();
@@ -40279,8 +40748,18 @@ var Statusline = class {
40279
40748
  this.menuHooks?.setChartVisible(true);
40280
40749
  this.setChartHidden(false);
40281
40750
  });
40282
- this.el.append(this.avatarEl, this.symbolEl, this.metaEl, this.marketEl, this.ohlcEl, this.changeEl, this.eyeEl);
40751
+ this.identityRow = doc.createElement("span");
40752
+ this.identityRow.className = "vela-sl-row vela-sl-identity";
40753
+ this.identityRow.append(this.avatarEl, this.symbolEl, this.metaEl, this.marketEl);
40754
+ this.valuesRow = doc.createElement("span");
40755
+ this.valuesRow.className = "vela-sl-row vela-sl-values";
40756
+ this.valuesRow.append(this.ohlcEl, this.changeEl, this.eyeEl);
40757
+ this.el.append(this.identityRow, this.valuesRow);
40283
40758
  host.appendChild(this.el);
40759
+ if (typeof ResizeObserver !== "undefined") {
40760
+ this.fitRO = new ResizeObserver(() => this.fit());
40761
+ this.fitRO.observe(host);
40762
+ }
40284
40763
  this.marketTip = new Tooltip(this.marketEl, { content: MARKET_LABELS.open, placement: "bottom" });
40285
40764
  this.eyeTip = new Tooltip(this.eyeEl, { content: "Show chart", placement: "bottom" });
40286
40765
  this.setMarketStatus("open");
@@ -40296,27 +40775,15 @@ var Statusline = class {
40296
40775
  this.fit();
40297
40776
  }
40298
40777
  /**
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.
40778
+ * Multi-chart cells: the same width ladder as a single chart, plus a last resort for
40779
+ * cells too narrow for even its bottom rung — whatever still overflows is hidden
40780
+ * outright rather than clipped mid-glyph, least important first: the values, the
40781
+ * venue/timeframe meta, then the market badge; the logo + ticker always stay.
40303
40782
  */
40304
40783
  setFitMode(on) {
40305
40784
  if (on === this.fitMode) return;
40306
40785
  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
- }
40786
+ this.fit();
40320
40787
  }
40321
40788
  /** Project the parts config onto the segments (the baseline fit() prunes from). */
40322
40789
  syncParts() {
@@ -40328,22 +40795,42 @@ var Statusline = class {
40328
40795
  this.ohlcEl.style.display = seg.ohlc ? "" : "none";
40329
40796
  this.changeEl.style.display = seg.change ? "" : "none";
40330
40797
  this.eyeEl.style.display = seg.eye ? "inline-flex" : "none";
40798
+ this.valuesRow.style.display = seg.ohlc || seg.change || seg.eye ? "" : "none";
40799
+ }
40800
+ /** Apply one ladder rung: the stacked/inline shape and the readout's level. */
40801
+ applyLayout(layout) {
40802
+ this.layout = layout;
40803
+ this.el.classList.toggle("vela-sl-stacked", layout.stacked);
40804
+ this.host.classList.toggle("vela-sl-stacked-host", layout.stacked);
40805
+ this.renderValues();
40331
40806
  }
40332
- /** Hide overflowing segments until the row fits its max-width (fit mode only). */
40807
+ /**
40808
+ * Walk the width ladder (see {@link STATUSLINE_LADDER}) until the chip fits its
40809
+ * max-width: the full one-line readout, then the values stacked under the symbol
40810
+ * line, then O/H/L shed, then the absolute change. In fit mode, when the last rung
40811
+ * still overflows, whole segments hide, least important first. Without layout
40812
+ * (detached host, node tests) nothing overflows, so the widest rung stays.
40813
+ */
40333
40814
  fit() {
40334
- if (!this.fitMode) return;
40335
40815
  this.syncParts();
40336
40816
  const seg = segmentVisibility(this.parts, this.chartHidden);
40817
+ const overflows = () => this.el.scrollWidth > this.el.clientWidth;
40818
+ const hasValues = seg.ohlc || seg.change;
40819
+ for (const rung of STATUSLINE_LADDER) {
40820
+ this.applyLayout(rung);
40821
+ if (!hasValues || !overflows()) break;
40822
+ }
40823
+ if (!this.fitMode) return;
40337
40824
  const order = [
40338
- [this.ohlcEl, seg.ohlc],
40339
- [this.changeEl, seg.change],
40825
+ [this.valuesRow, hasValues],
40340
40826
  [this.metaEl, seg.meta],
40341
40827
  [this.marketEl, seg.market]
40342
40828
  ];
40343
40829
  for (const [el, shown] of order) {
40344
- if (this.el.scrollWidth <= this.el.clientWidth) break;
40830
+ if (!overflows()) break;
40345
40831
  if (shown) el.style.display = "none";
40346
40832
  }
40833
+ if (this.valuesRow.style.display === "none") this.applyLayout({ ...this.layout, stacked: false });
40347
40834
  }
40348
40835
  /** Shape + color the value readout after the active price style: its own up/down
40349
40836
  * colors (candle bodies, bar ticks, the line color, …), the direction rule that
@@ -40456,7 +40943,7 @@ var Statusline = class {
40456
40943
  this.marketTip.destroy();
40457
40944
  this.eyeTip.destroy();
40458
40945
  this.marketBubble.destroy();
40459
- this.host.classList.remove("vela-has-statusline", "vela-sl-fit-host");
40946
+ this.host.classList.remove("vela-has-statusline", "vela-sl-stacked-host");
40460
40947
  this.el.remove();
40461
40948
  }
40462
40949
  detach() {
@@ -40465,6 +40952,10 @@ var Statusline = class {
40465
40952
  }
40466
40953
  render() {
40467
40954
  if (this.menuHooks) this.setChartHidden(!this.menuHooks.chartVisible());
40955
+ this.fit();
40956
+ }
40957
+ /** Write the value readout for the current bar at the current ladder level. */
40958
+ renderValues() {
40468
40959
  const bar = this.hoverBar ?? this.lastBar;
40469
40960
  if (!bar) {
40470
40961
  this.ohlcEl.replaceChildren();
@@ -40484,12 +40975,11 @@ var Statusline = class {
40484
40975
  s.appendChild(b);
40485
40976
  return s;
40486
40977
  };
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);
40978
+ const { level } = this.layout;
40979
+ this.ohlcEl.replaceChildren(...readoutCells(level, this.readout).map((c) => cell(c.label, bar[c.key])));
40980
+ this.changeEl.textContent = level === "minimal" ? fmtChangePct(bar.open, bar.close) : fmtChange(bar.open, bar.close);
40490
40981
  this.changeEl.dataset.dir = up ? "up" : "down";
40491
40982
  this.changeEl.style.color = ink;
40492
- this.fit();
40493
40983
  }
40494
40984
  };
40495
40985
 
@@ -41257,6 +41747,11 @@ function cellDrawings(opt) {
41257
41747
  if (opt === true || opt == null) return { toolbar: false };
41258
41748
  return { ...opt, toolbar: false };
41259
41749
  }
41750
+ function nativeLedgerEntry(handle) {
41751
+ const inputs = inputDeltas(handle.inputs, handle.inputValues());
41752
+ const hidden = !handle.visible;
41753
+ return inputs || hidden ? { type: handle.nativeType, ...inputs ? { inputs } : {}, ...hidden ? { hidden: true } : {} } : handle.nativeType;
41754
+ }
41260
41755
  function instanceDeltas(handle) {
41261
41756
  if (!handle) return void 0;
41262
41757
  const inputs = inputDeltas(handle.inputs, handle.inputValues());
@@ -41379,10 +41874,13 @@ var ChartCell = class {
41379
41874
  if (seed.rendererConfig != null) this.inner.renderer.applyConfig(seed.rendererConfig);
41380
41875
  if (seed.drawings != null) this.inner.drawings.fromJSON(seed.drawings);
41381
41876
  if (seed.indicators) {
41382
- for (const type of seed.indicators.natives) this.inner.addNativeIndicator(type);
41877
+ for (const entry of seed.indicators.natives) {
41878
+ const handle = this.inner.addNativeIndicator(ledgerNativeType(entry));
41879
+ this.applyNativeLedgerEntry(handle, entry);
41880
+ }
41383
41881
  this.pendingManifestNames = [...seed.indicators.manifest];
41384
41882
  }
41385
- this.volumeIntent = seed.indicators ? seed.indicators.natives.includes("volume") : deps.volume;
41883
+ this.volumeIntent = seed.indicators ? seed.indicators.natives.some((e) => ledgerNativeType(e) === "volume") : deps.volume;
41386
41884
  this.extState = { ...seed.ext ?? {} };
41387
41885
  this.inner.on("load:end", () => {
41388
41886
  this.volumeMayBePending = false;
@@ -41454,6 +41952,7 @@ var ChartCell = class {
41454
41952
  this.refreshNativeCatalog();
41455
41953
  });
41456
41954
  this.inner.on("indicator:inputs", () => this.deps.onStateDirty());
41955
+ this.inner.on("indicator:visibility", () => this.deps.onStateDirty());
41457
41956
  this.inner.on("indicator:removed", ({ id: id2 }) => {
41458
41957
  if (this.destroyed) return;
41459
41958
  const idx = this.instances.findIndex((it) => it.handle?.id === id2);
@@ -41898,7 +42397,7 @@ var ChartCell = class {
41898
42397
  if (list.length === 0) return;
41899
42398
  for (const led of this.pendingManifestNames) {
41900
42399
  const entry = list.find((e) => e.name === ledgerEntryName(led));
41901
- if (entry) this.addManifestInstance(entry, { record: false, ...typeof led === "object" ? { inputs: led.inputs, props: led.props } : {} });
42400
+ if (entry) this.addManifestInstance(entry, { record: false, ...typeof led === "object" ? { inputs: led.inputs, props: led.props, hidden: led.hidden } : {} });
41902
42401
  }
41903
42402
  this.pendingManifestNames = null;
41904
42403
  return;
@@ -41917,23 +42416,35 @@ var ChartCell = class {
41917
42416
  applyIndicatorLedger(led) {
41918
42417
  const chart = this.inner;
41919
42418
  if (!chart) return;
41920
- this.volumeIntent = led.natives.includes("volume");
42419
+ this.volumeIntent = led.natives.some((e) => ledgerNativeType(e) === "volume");
41921
42420
  this.history.silently(() => {
41922
42421
  const owed = /* @__PURE__ */ new Map();
41923
- for (const type of led.natives) owed.set(type, (owed.get(type) ?? 0) + 1);
42422
+ for (const e of led.natives) {
42423
+ const type = ledgerNativeType(e);
42424
+ const queue = owed.get(type);
42425
+ if (queue) queue.push(e);
42426
+ else owed.set(type, [e]);
42427
+ }
41924
42428
  for (const h of this.nativeHandles()) {
41925
- const type = h.nativeType;
41926
- const n = owed.get(type) ?? 0;
41927
- if (n > 0) owed.set(type, n - 1);
42429
+ const entry = owed.get(h.nativeType)?.shift();
42430
+ if (entry !== void 0) this.applyNativeLedgerEntry(h, entry);
41928
42431
  else h.remove();
41929
42432
  }
41930
- for (const [type, n] of owed) for (let i = 0; i < n; i++) chart.addNativeIndicator(type);
42433
+ for (const queue of owed.values()) {
42434
+ for (const e of queue) {
42435
+ const h = chart.addNativeIndicator(ledgerNativeType(e));
42436
+ this.applyNativeLedgerEntry(h, e);
42437
+ }
42438
+ }
41931
42439
  for (const it of [...this.instances]) this.dropInstance(it);
41932
42440
  if (this.manifest.length > 0) {
41933
42441
  for (const item of led.manifest) {
41934
42442
  const entry = this.manifest.find((e) => e.name === ledgerEntryName(item));
41935
42443
  if (entry)
41936
- this.addManifestInstance(entry, { record: false, ...typeof item === "object" ? { inputs: item.inputs, props: item.props } : {} });
42444
+ this.addManifestInstance(entry, {
42445
+ record: false,
42446
+ ...typeof item === "object" ? { inputs: item.inputs, props: item.props, hidden: item.hidden } : {}
42447
+ });
41937
42448
  }
41938
42449
  this.pendingManifestNames = null;
41939
42450
  } else if (!this.deps.manifestSettled()) {
@@ -42000,6 +42511,7 @@ var ChartCell = class {
42000
42511
  if (this.destroyed) return;
42001
42512
  const values = opts.inputs || opts.props ? { inputs: opts.inputs, props: opts.props } : void 0;
42002
42513
  const it = { entry, handle: this.addToChart(entry, values), ...opts.external ? { external: true } : {}, ...values ? { values } : {} };
42514
+ if (opts.hidden) it.handle?.setVisible(false);
42003
42515
  this.instances.push(it);
42004
42516
  this.deps.onIndicatorsChanged(this.id);
42005
42517
  if (opts.record === false) return;
@@ -42043,6 +42555,29 @@ var ChartCell = class {
42043
42555
  /** Add a native indicator. A multi-instance type gets a fresh instance every time; a
42044
42556
  * single-instance type already on the chart hands back its existing one — nothing
42045
42557
  * changed, so nothing enters the undo timeline. */
42558
+ /**
42559
+ * Converge one native handle to a restored ledger entry: every schema key gets the
42560
+ * entry's value or its declaration default (the DOCUMENT is the truth — a live
42561
+ * tweak must not survive a restore that says otherwise), and visibility follows
42562
+ * the `hidden` flag. Skips the input write when nothing differs — `setInputs`
42563
+ * re-runs the indicator, and a no-op restore must not flicker every native.
42564
+ */
42565
+ applyNativeLedgerEntry(handle, entry) {
42566
+ const wanted = typeof entry === "string" ? void 0 : entry.inputs;
42567
+ if (handle.inputs.length > 0) {
42568
+ const current = handle.inputValues();
42569
+ const next = {};
42570
+ for (const schema of handle.inputs) {
42571
+ const target = wanted?.[schema.key] ?? schema.defval;
42572
+ if (JSON.stringify(current[schema.key]) !== JSON.stringify(target)) next[schema.key] = target;
42573
+ }
42574
+ if (Object.keys(next).length > 0) handle.setInputs(next);
42575
+ } else if (wanted) {
42576
+ handle.setInputs(wanted);
42577
+ }
42578
+ const visible = !(typeof entry === "object" && entry.hidden);
42579
+ if (handle.visible !== visible) handle.setVisible(visible);
42580
+ }
42046
42581
  addNative(type) {
42047
42582
  const chart = this.inner;
42048
42583
  if (!chart) return;
@@ -42206,10 +42741,13 @@ var ChartCell = class {
42206
42741
  // (`ctx.addIndicator`) stay out: their names would never resolve against the
42207
42742
  // manifest — their plugin persists them via the `ext` seam instead.
42208
42743
  indicators: indicatorLedger({
42209
- present: this.inner ? this.inner.presentNativeIndicators() : [],
42744
+ // Live handles carry what the registry read cannot: the input DELTAS and
42745
+ // the hidden flag, so native settings/visibility survive the round-trip.
42746
+ present: this.inner ? this.nativeHandles().map((h) => nativeLedgerEntry(h)) : [],
42210
42747
  instanceEntries: this.instances.filter((it) => !it.external).map((it) => {
42211
42748
  const d = it.handle ? instanceDeltas(it.handle) : it.values;
42212
- return d ? { name: it.entry.name, ...d } : it.entry.name;
42749
+ const hidden = it.handle ? !it.handle.visible : false;
42750
+ return d || hidden ? { name: it.entry.name, ...d ?? {}, ...hidden ? { hidden: true } : {} } : it.entry.name;
42213
42751
  }),
42214
42752
  pendingManifest: this.pendingManifestNames,
42215
42753
  manifestSettled: this.deps.manifestSettled(),
@@ -42629,6 +43167,17 @@ function nextAutoCellId(taken) {
42629
43167
  if (!taken.has(`c${i}`)) return `c${i}`;
42630
43168
  }
42631
43169
  }
43170
+ function resolveTyping(key, open2) {
43171
+ if (open2.dialogs > 0) {
43172
+ if (key.length !== 1) return null;
43173
+ if (open2.symbolSearch) return { target: "symbol", text: key.toUpperCase() };
43174
+ if (open2.timeframeEntry) return { target: "timeframe", text: key };
43175
+ return null;
43176
+ }
43177
+ if (/^[a-zA-Z]$/.test(key)) return { target: "symbol", text: key.toUpperCase() };
43178
+ if (/^[0-9]$/.test(key)) return { target: "timeframe", text: key };
43179
+ return null;
43180
+ }
42632
43181
  var VelaWorkspace = class {
42633
43182
  constructor(container, opts = {}) {
42634
43183
  this.events = new TypedEventBus();
@@ -44262,17 +44811,14 @@ var VelaWorkspace = class {
44262
44811
  }
44263
44812
  /** Bare-typing router: letters → symbol search (seeded), digits → timeframe entry. */
44264
44813
  routeTyping(ev) {
44265
- if (this.destroyed || this.openDialogs > 0) return;
44814
+ if (this.destroyed) return;
44266
44815
  if (ev.ctrlKey || ev.metaKey || ev.altKey) return;
44267
44816
  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
- }
44817
+ const hit = resolveTyping(ev.key, { dialogs: this.openDialogs, symbolSearch: this.symbolPicker.isOpen, timeframeEntry: this.tfQuick.isOpen });
44818
+ if (!hit) return;
44819
+ ev.preventDefault();
44820
+ if (hit.target === "symbol") this.symbolPicker.type(hit.text);
44821
+ else this.tfQuick.type(hit.text);
44276
44822
  }
44277
44823
  trackDialog(open2) {
44278
44824
  this.openDialogs = Math.max(0, this.openDialogs + (open2 ? 1 : -1));