@luxalgo/vela 0.5.0 → 0.5.1

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 (45) hide show
  1. package/dist/{DataProvider-DKNDHpNv.d.cts → DataProvider-BBQ3Hr21.d.cts} +1 -1
  2. package/dist/{DataProvider-Dzd-Erlk.d.ts → DataProvider-DGesjOJT.d.ts} +1 -1
  3. package/dist/{chunk-OVQKKXLZ.js → chunk-5SE4PL3H.js} +402 -60
  4. package/dist/{chunk-RHIDOUFL.js → chunk-AIW3EJUE.js} +2 -2
  5. package/dist/{chunk-GYD2THPV.js → chunk-EP6NDJYK.js} +5 -0
  6. package/dist/{chunk-KCCZNKH7.js → chunk-FNOMKHEZ.js} +668 -78
  7. package/dist/{chunk-7UFX5ZIG.js → chunk-S6F6SQ2L.js} +7 -2
  8. package/dist/{chunk-KM6LHB3Y.js → chunk-ULIX6AE2.js} +6 -1
  9. package/dist/{contributions-hX3EUyjG.d.ts → contributions-6N0rbrPC.d.ts} +46 -31
  10. package/dist/{contributions-o1GRKPI_.d.cts → contributions-BlV3kb0O.d.cts} +46 -31
  11. package/dist/{history-Dzxz-MQj.d.ts → history-BTd52YMA.d.ts} +10 -5
  12. package/dist/{history-LJkz4-sS.d.cts → history-DX3Zne3a.d.cts} +10 -5
  13. package/dist/index.cjs +680 -75
  14. package/dist/index.d.cts +37 -12
  15. package/dist/index.d.ts +37 -12
  16. package/dist/index.js +4 -4
  17. package/dist/{options-Q-576hIi.d.cts → options-DbQholFV.d.cts} +14 -0
  18. package/dist/{options-Q-576hIi.d.ts → options-DbQholFV.d.ts} +14 -0
  19. package/dist/{plugin-D94muTV-.d.cts → plugin-O34MbXf-.d.cts} +157 -7
  20. package/dist/{plugin-aGUD1epn.d.ts → plugin-TN3n-9YN.d.ts} +157 -7
  21. package/dist/plugin.cjs +5 -0
  22. package/dist/plugin.d.cts +4 -4
  23. package/dist/plugin.d.ts +4 -4
  24. package/dist/plugin.js +2 -2
  25. package/dist/providers/binance.d.cts +2 -2
  26. package/dist/providers/binance.d.ts +2 -2
  27. package/dist/providers/coinbase.d.cts +2 -2
  28. package/dist/providers/coinbase.d.ts +2 -2
  29. package/dist/providers/hyperliquid.d.cts +2 -2
  30. package/dist/providers/hyperliquid.d.ts +2 -2
  31. package/dist/ui.cjs +10 -0
  32. package/dist/ui.d.cts +1 -1
  33. package/dist/ui.d.ts +1 -1
  34. package/dist/ui.js +3 -3
  35. package/dist/vela.global.js +680 -75
  36. package/dist/vela.global.min.js +59 -28
  37. package/dist/widget.cjs +1081 -134
  38. package/dist/widget.d.cts +30 -16
  39. package/dist/widget.d.ts +30 -16
  40. package/dist/widget.js +9 -9
  41. package/dist/workspace.cjs +1372 -183
  42. package/dist/workspace.d.cts +66 -8
  43. package/dist/workspace.d.ts +66 -8
  44. package/dist/workspace.js +275 -37
  45. package/package.json +1 -1
@@ -532,6 +532,11 @@ var Vela = (function (exports) {
532
532
  "pen-lock",
533
533
  svg24('<path d="M13.8 4.2a2.1 2.1 0 0 1 3 3L8.5 15.5l-3.5 1 1-3.5Z"/><rect x="13" y="14.5" width="8" height="6" rx="1.2"/><path d="M15 14.5v-1.6a2 2 0 0 1 4 0v1.6"/>')
534
534
  );
535
+ registerIcon(
536
+ "pen-sync",
537
+ // Pen + two stacked panes — drawing onto every linked chart at once.
538
+ svg24('<path d="M13.8 4.2a2.1 2.1 0 0 1 3 3L8.5 15.5l-3.5 1 1-3.5Z"/><rect x="12.5" y="13.5" width="6" height="4.8" rx="1"/><path d="M15.5 18.3v1a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-2.8a1 1 0 0 0-1-1h-1"/>')
539
+ );
535
540
  registerIcon("brush", svg24('<path d="M9.5 12 17 4.5a2.12 2.12 0 0 1 3 3L12.5 15"/><path d="M7 14a3 3 0 0 0-3 3c0 1.3-1.2 1.5-1.5 2 .8.9 2 1.5 3.5 1.5a3.5 3.5 0 0 0 3.5-3.5 3 3 0 0 0-2.5-3Z"/>'));
536
541
  registerIcon(
537
542
  "bucket",
@@ -6535,6 +6540,11 @@ var Vela = (function (exports) {
6535
6540
  setToolbar(option) {
6536
6541
  this.port?.setToolbar(buildToolbar(option).definition);
6537
6542
  }
6543
+ /** Display (or clear) another chart's in-progress placement as a ghost — silently
6544
+ * inert on a renderer without the optional `setExternalGhost` seam. */
6545
+ setExternalGhost(doc) {
6546
+ this.port?.setExternalGhost?.(doc);
6547
+ }
6538
6548
  /** Push per-tool shortcut hints (pre-formatted display strings) to the toolbar flyouts. */
6539
6549
  setToolShortcuts(map) {
6540
6550
  this.port?.setToolShortcuts?.(map);
@@ -6765,6 +6775,9 @@ var Vela = (function (exports) {
6765
6775
  case "arm":
6766
6776
  this.setTool(i.type);
6767
6777
  break;
6778
+ case "draft":
6779
+ this.events.emit("drawing:draft", { doc: i.doc });
6780
+ break;
6768
6781
  case "create": {
6769
6782
  const before = this.store.serialize();
6770
6783
  const last = this.lastStyle.get(i.doc.type);
@@ -7040,6 +7053,11 @@ var Vela = (function (exports) {
7040
7053
  };
7041
7054
 
7042
7055
  // src/chart-types/registry.ts
7056
+ function settingsRowVisible(when, bag) {
7057
+ if (!when) return true;
7058
+ const conds = Array.isArray(when) ? when : [when];
7059
+ return conds.every((c) => c.anyOf ? c.anyOf.some((x) => x === bag[c.key]) : bag[c.key] === c.equals);
7060
+ }
7043
7061
  var registry2 = /* @__PURE__ */ new Map();
7044
7062
  function registerChartType(def) {
7045
7063
  registry2.set(def.id, def);
@@ -9384,6 +9402,14 @@ var Vela = (function (exports) {
9384
9402
  if (this.ok("setToolShortcuts")) this.ctrl.setToolShortcuts(map);
9385
9403
  return this;
9386
9404
  }
9405
+ /** Display another chart's in-progress placement as a GHOST at reduced opacity
9406
+ * (`null` clears it) — how a multi-chart host mirrors `drawing:draft` onto linked
9407
+ * charts. Never a real drawing: no store entry, no selection, no persistence.
9408
+ * Silently inert on a renderer without the optional seam. */
9409
+ setExternalGhost(doc) {
9410
+ this.ctrl.setExternalGhost(doc);
9411
+ return this;
9412
+ }
9387
9413
  /** Create a drawing programmatically (no clicking). Returns it, or null if unsupported. */
9388
9414
  add(type, init = {}) {
9389
9415
  if (!this.ok("add")) return null;
@@ -9622,6 +9648,11 @@ var Vela = (function (exports) {
9622
9648
  "--vela-hover-strong": wash(0.16),
9623
9649
  "--vela-focus": withAlpha(t.textColor, 0.5),
9624
9650
  "--vela-focus-soft": withAlpha(t.textColor, 0.12),
9651
+ // Separator hover — the SAME wash the chart's pane separators paint on hover
9652
+ // (soft full-thickness band + solid 2px center line), so DOM-drawn dividers
9653
+ // (the workspace grid) and canvas-drawn ones read as one family.
9654
+ "--vela-separator-hover-band": withAlpha(t.textColor, 0.1),
9655
+ "--vela-separator-hover-line": withAlpha(t.textColor, 0.55),
9625
9656
  "--vela-scroll": withAlpha(t.textColor, 0.3),
9626
9657
  "--vela-accent": ACCENT,
9627
9658
  "--vela-accent-bright": ACCENT_BRIGHT,
@@ -11465,6 +11496,44 @@ var Vela = (function (exports) {
11465
11496
  for (const t of chartTypes()) if (t.id === style) return t.basePainting ?? "candles";
11466
11497
  return "candles";
11467
11498
  }
11499
+ function hasOwnCandlePaint(style) {
11500
+ if (BUILTIN_PRICE_STYLES.includes(style)) return false;
11501
+ for (const t of chartTypes()) if (t.id === style) return (t.basePainting ?? "candles") === "candles";
11502
+ return false;
11503
+ }
11504
+ function candleOverrideFor(style, bags) {
11505
+ if (!hasOwnCandlePaint(style)) return null;
11506
+ const bag = bags[style] ?? {};
11507
+ const color = (v) => isColor(v) ? v : null;
11508
+ const bool = (v) => isBool(v) ? v : null;
11509
+ return {
11510
+ upColor: color(bag.candleUpColor),
11511
+ downColor: color(bag.candleDownColor),
11512
+ bodyVisible: bool(bag.candleBodyVisible),
11513
+ borderVisible: bool(bag.candleBorderVisible),
11514
+ borderUpColor: color(bag.candleBorderUpColor),
11515
+ borderDownColor: color(bag.candleBorderDownColor),
11516
+ wickVisible: bool(bag.candleWickVisible),
11517
+ wickUpColor: color(bag.candleWickUpColor),
11518
+ wickDownColor: color(bag.candleWickDownColor)
11519
+ };
11520
+ }
11521
+ function effectiveCandlePaint(base, override, themeUp, themeDown) {
11522
+ if (!override) return { up: themeUp, down: themeDown, candle: base };
11523
+ return {
11524
+ up: override.upColor ?? themeUp,
11525
+ down: override.downColor ?? themeDown,
11526
+ candle: {
11527
+ bodyVisible: override.bodyVisible ?? base.bodyVisible,
11528
+ borderVisible: override.borderVisible ?? base.borderVisible,
11529
+ borderUpColor: override.borderUpColor ?? base.borderUpColor,
11530
+ borderDownColor: override.borderDownColor ?? base.borderDownColor,
11531
+ wickVisible: override.wickVisible ?? base.wickVisible,
11532
+ wickUpColor: override.wickUpColor ?? base.wickUpColor,
11533
+ wickDownColor: override.wickDownColor ?? base.wickDownColor
11534
+ }
11535
+ };
11536
+ }
11468
11537
  function priceStyleIds() {
11469
11538
  const out = [...BUILTIN_PRICE_STYLES];
11470
11539
  for (const t of chartTypes()) if (!out.includes(t.id)) out.push(t.id);
@@ -11503,6 +11572,40 @@ var Vela = (function (exports) {
11503
11572
  function clampSpacing(v) {
11504
11573
  return v < 0.1 ? 0.1 : v > 10 ? 10 : v;
11505
11574
  }
11575
+ function factoryResetConfig(factory) {
11576
+ const bag = {};
11577
+ for (const t of chartTypes()) {
11578
+ const section = t.settings;
11579
+ if (!section) continue;
11580
+ const defaults2 = {};
11581
+ const addRows = (rows) => {
11582
+ for (const r of rows ?? []) {
11583
+ if (r.kind === "heading" || r.kind === "header") continue;
11584
+ if (r.kind === "range") {
11585
+ defaults2[r.minKey] = r.defval;
11586
+ defaults2[r.maxKey] = r.defval;
11587
+ continue;
11588
+ }
11589
+ defaults2[r.key] = r.defval;
11590
+ if (r.kind === "toggle") for (const c of r.colors ?? []) defaults2[c.key] = c.defval;
11591
+ }
11592
+ };
11593
+ for (const inst of section.instances ?? []) {
11594
+ if (inst.enableKey) defaults2[inst.enableKey] = false;
11595
+ addRows(inst.rows);
11596
+ }
11597
+ for (const sub of section.subsections ?? []) {
11598
+ if (sub.enableKey) defaults2[sub.enableKey] = false;
11599
+ addRows(sub.rows);
11600
+ }
11601
+ if (!section.instances) addRows(section.rows);
11602
+ bag[t.id] = defaults2;
11603
+ }
11604
+ for (const [typeId, vals] of Object.entries(factory.chartTypes)) {
11605
+ bag[typeId] = { ...bag[typeId] ?? {}, ...vals };
11606
+ }
11607
+ return { ...factory, chartTypes: bag };
11608
+ }
11506
11609
  function mergeConfig(base, patch) {
11507
11610
  const p = asObject(patch);
11508
11611
  const ctPatch = asObject(p.chartTypes);
@@ -11817,6 +11920,9 @@ var Vela = (function (exports) {
11817
11920
  this.priceStyle = "candles";
11818
11921
  /** Price-series base painting for the ACTIVE style (see ChartTypeDefinition.basePainting). */
11819
11922
  this.basePainting = "candles";
11923
+ /** The ACTIVE style's own candle cosmetics (`chartTypes.<id>.candle*`) when it is a
11924
+ * candle-based plugin type; null ⇒ paint with the shared `style.candle` block. */
11925
+ this.candleOverride = null;
11820
11926
  /** Explicit baseline reference price for `priceStyle:'baseline'`; when null the
11821
11927
  * baseline follows `style.baseline.baselineLevel` as a percent of the visible pane
11822
11928
  * range (resolved per frame via `baselinePriceFor`). */
@@ -13046,14 +13152,15 @@ void main() {
13046
13152
  emitCandles(b, scene, bars, i0, i1, coords, pane, theme, barColors) {
13047
13153
  const spacing = coords.bodySpacing();
13048
13154
  const tier = candleTier(spacing);
13155
+ const paint = effectiveCandlePaint(scene.style.candle, scene.candleOverride, theme.upColor, theme.downColor);
13049
13156
  if (tier === "aggregate") {
13050
- this.emitCandlesAggregated(b, bars, i0, i1, coords, pane, theme.upColor, theme.downColor, barColors);
13157
+ this.emitCandlesAggregated(b, bars, i0, i1, coords, pane, paint.up, paint.down, barColors);
13051
13158
  return;
13052
13159
  }
13053
13160
  const drawBody = tier === "full";
13054
- const up = theme.upColor;
13055
- const down = theme.downColor;
13056
- const cs = scene.style.candle;
13161
+ const up = paint.up;
13162
+ const down = paint.down;
13163
+ const cs = paint.candle;
13057
13164
  const fading = this.candleStructureAlpha > this.candleBodyAlpha + 1e-3;
13058
13165
  for (let i = i0; i <= i1; i += 1) {
13059
13166
  const bar = bars[i];
@@ -13645,7 +13752,6 @@ void main() {
13645
13752
  return momentaryOverride ? "strong" : sticky;
13646
13753
  }
13647
13754
  var InputController = class {
13648
- // the last middle press deleted a drawing (suppress autoscroll/paste)
13649
13755
  constructor(deps) {
13650
13756
  this.deps = deps;
13651
13757
  /** When true (the default), wheel-zoom pins the right edge instead of the bar
@@ -13673,9 +13779,23 @@ void main() {
13673
13779
  this.vx = 0;
13674
13780
  // smoothed pointer velocity, px/ms
13675
13781
  this.middleDeleted = false;
13782
+ // the last middle press deleted a drawing (suppress autoscroll/paste)
13783
+ // Last cursor position over the element (NaN once it leaves) — lets a modifier
13784
+ // press/release re-shape the drawings preview with the pointer stationary.
13785
+ this.cursorX = NaN;
13786
+ this.cursorY = NaN;
13676
13787
  this.onMiddleGuard = (e) => {
13677
13788
  if (e.button === 1 && this.middleDeleted) e.preventDefault();
13678
13789
  };
13790
+ /** A modifier press/release with the pointer stationary: re-issue the last cursor
13791
+ * position so the placing ghost / drag preview reflects the new state immediately
13792
+ * (Shift's 45° angle lock, Ctrl/Cmd's momentary strong magnet). */
13793
+ this.onModifier = (e) => {
13794
+ if (e.repeat || e.key !== "Shift" && e.key !== "Control" && e.key !== "Meta") return;
13795
+ if (Number.isNaN(this.cursorX)) return;
13796
+ if (this.dragging && this.region !== "drawing") return;
13797
+ this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey);
13798
+ };
13679
13799
  this.onDown = (e) => {
13680
13800
  if (e.button === 1) {
13681
13801
  const { x: x2, y: y2 } = this.local(e);
@@ -13717,6 +13837,8 @@ void main() {
13717
13837
  };
13718
13838
  this.onMove = (e) => {
13719
13839
  const { x, y } = this.local(e);
13840
+ this.cursorX = x;
13841
+ this.cursorY = y;
13720
13842
  if (this.dragging) {
13721
13843
  if (this.region === "price") {
13722
13844
  if (Math.abs(y - this.startY) > DRAG_SLOP) this.moved = true;
@@ -13730,7 +13852,7 @@ void main() {
13730
13852
  this.deps.apply({ barSpacing, rightOffset: this.startRightOffset });
13731
13853
  } else if (this.region === "drawing") {
13732
13854
  if (Math.abs(x - this.startX) > DRAG_SLOP || Math.abs(y - this.startY) > DRAG_SLOP) this.moved = true;
13733
- this.deps.drawingsPointerMove?.(x, y, this.snapMode(e));
13855
+ this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
13734
13856
  } else {
13735
13857
  const coords = this.deps.getCoords();
13736
13858
  const vp = coords.getViewport();
@@ -13753,7 +13875,7 @@ void main() {
13753
13875
  const drawCursor = this.deps.drawingsCursor?.(x, y);
13754
13876
  const r = this.regionAt(x, y);
13755
13877
  this.el.style.cursor = drawCursor ?? (r === "price" ? "ns-resize" : r === "time" ? "ew-resize" : r === "separator" ? "row-resize" : "");
13756
- this.deps.drawingsPointerMove?.(x, y, this.snapMode(e));
13878
+ this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
13757
13879
  }
13758
13880
  this.deps.onPointerMove(x, y);
13759
13881
  };
@@ -13778,6 +13900,8 @@ void main() {
13778
13900
  }
13779
13901
  };
13780
13902
  this.onLeave = () => {
13903
+ this.cursorX = NaN;
13904
+ this.cursorY = NaN;
13781
13905
  this.deps.onPointerMove(null, null);
13782
13906
  };
13783
13907
  this.onDblClick = (e) => {
@@ -13816,10 +13940,16 @@ void main() {
13816
13940
  el.addEventListener("wheel", this.onWheel, { passive: false });
13817
13941
  el.addEventListener("mousedown", this.onMiddleGuard);
13818
13942
  el.addEventListener("auxclick", this.onMiddleGuard);
13943
+ const win = el.ownerDocument?.defaultView;
13944
+ win?.addEventListener("keydown", this.onModifier);
13945
+ win?.addEventListener("keyup", this.onModifier);
13819
13946
  }
13820
13947
  detach() {
13821
13948
  const el = this.el;
13822
13949
  if (!el) return;
13950
+ const win = el.ownerDocument?.defaultView;
13951
+ win?.removeEventListener("keydown", this.onModifier);
13952
+ win?.removeEventListener("keyup", this.onModifier);
13823
13953
  el.removeEventListener("pointerdown", this.onDown);
13824
13954
  el.removeEventListener("pointermove", this.onMove);
13825
13955
  el.removeEventListener("pointerup", this.onUp);
@@ -14237,12 +14367,13 @@ void main() {
14237
14367
  drawCandles(ctx, scene, bars, i0, i1, coords, pane, theme, barColors) {
14238
14368
  const spacing = coords.bodySpacing();
14239
14369
  const tier = candleTier(spacing);
14370
+ const paint = effectiveCandlePaint(scene.style.candle, scene.candleOverride, theme.upColor, theme.downColor);
14240
14371
  if (tier === "aggregate") {
14241
- this.drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, theme.upColor, theme.downColor, barColors);
14372
+ this.drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, paint.up, paint.down, barColors);
14242
14373
  return;
14243
14374
  }
14244
14375
  const drawBody = tier === "full";
14245
- const cs = scene.style.candle;
14376
+ const cs = paint.candle;
14246
14377
  const fading = this.candleStructureAlpha > this.candleBodyAlpha + 1e-3;
14247
14378
  for (let i = i0; i <= i1; i += 1) {
14248
14379
  const b = bars[i];
@@ -14250,7 +14381,7 @@ void main() {
14250
14381
  const g = candleGeometry(coords.logicalToX(i), spacing, coords.dpr, this.candleBodyScale);
14251
14382
  const x = g.center;
14252
14383
  const up = b.close >= b.open;
14253
- const dir = up ? theme.upColor : theme.downColor;
14384
+ const dir = up ? paint.up : paint.down;
14254
14385
  const bc = barColors.get(b.time);
14255
14386
  const color = bc ?? dir;
14256
14387
  let top = 0;
@@ -15361,7 +15492,7 @@ void main() {
15361
15492
  constructor() {
15362
15493
  this.canvas = null;
15363
15494
  this.ctx = null;
15364
- // The color for axis tick labels — the stable chrome surface text, set each frame in render().
15495
+ // The color for axis tick labels — the host-passed surface text, set each frame in render().
15365
15496
  this.axisTextColor = DARK_THEME.textColor;
15366
15497
  // Shared Pine-drawing renderer (line/box/label/polyline/linefill); widthCache persists.
15367
15498
  this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
@@ -15393,8 +15524,8 @@ void main() {
15393
15524
  return dr;
15394
15525
  }
15395
15526
  /** Clear the chrome canvas and draw drawings + axes + current-price line.
15396
- * `surface` (background + text) paints the axis-scale gutters so they keep the app
15397
- * chrome color and stay readable, rather than tracking the user-editable plot cosmetics.
15527
+ * `surface` (background + text) paints the axis-scale gutters the host passes the live
15528
+ * chart background so the scales read as part of the plot, with contrast-corrected text.
15398
15529
  * Falls back to the theme's own colors when no surface is supplied. */
15399
15530
  render(scene, coords, theme, surface) {
15400
15531
  const ctx = this.ctx;
@@ -16251,6 +16382,37 @@ void main() {
16251
16382
  .vela-sd-btn:hover{background:var(--vela-hover);border-color:var(--vela-border-strong);color:var(--vela-fg-bright);}
16252
16383
  .vela-sd-close{cursor:pointer;display:inline-flex;align-items:center;justify-content:center;background:transparent;border:none;color:var(--vela-fg-muted);line-height:0;width:30px;height:30px;border-radius:var(--vela-radius-sm);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
16253
16384
  .vela-sd-close:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
16385
+ /* Rows/blocks gated away by chart-type conditions, TOC filters, or the instance strip.
16386
+ !important: the pane grid rewrites inline display ('contents') AFTER the initial
16387
+ visibility pass, so a class must win. */
16388
+ .vela-sd-hide{display:none !important;}
16389
+ /* Indented rail sub-entry (a chart-type section's subsection tab). */
16390
+ .vela-sd-tab-sub{padding-left:28px;font-weight:600;font-size:12px;}
16391
+ /* Instance strip: a tab per present instance (label, \u2715 on the active removable one)
16392
+ and a dashed + that turns on the next absent instance. The rule under it separates
16393
+ the strip from the instance's TOC + rows area. */
16394
+ .vela-sd-itabs{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin:14px 0 0;padding-bottom:12px;border-bottom:1px solid var(--vela-border);}
16395
+ .vela-sd-itab{display:inline-flex;align-items:center;gap:7px;height:30px;padding:0 11px;background:transparent;border:1px solid var(--vela-border);border-radius:var(--vela-radius-md);color:var(--vela-fg-muted);font-family:inherit;font-size:var(--vela-font-size-md);font-weight:600;cursor:pointer;transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease,border-color var(--vela-dur-fast) ease;}
16396
+ .vela-sd-itab:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
16397
+ .vela-sd-itab.on{background:var(--vela-active);color:var(--vela-fg-bright);border-color:var(--vela-border-strong);}
16398
+ .vela-sd-ix{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;margin-right:-4px;border-radius:var(--vela-radius-sm);color:var(--vela-fg-muted);font-size:10px;line-height:1;}
16399
+ .vela-sd-ix:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
16400
+ .vela-sd-itab-add{border-style:dashed;min-width:30px;justify-content:center;padding:0;}
16401
+ /* Structured pane: group TOC column + rows column, TOP-ALIGNED (the shared padding-top
16402
+ lives on the wrap, never on one column). The TOC sticks while the pane scrolls; the
16403
+ vertical rule sits on the rows column so it spans the full content height. When every
16404
+ group gates out the TOC hides and .no-toc drops the rule with it. */
16405
+ .vela-sd-struct{display:flex;align-items:flex-start;padding-top:14px;}
16406
+ .vela-sd-toc{position:sticky;top:0;flex:0 0 auto;min-width:104px;display:flex;flex-direction:column;gap:2px;padding:2px 14px 0 0;}
16407
+ .vela-sd-struct>[data-sd-rows-host]{border-left:1px solid var(--vela-border);padding-left:18px;}
16408
+ .vela-sd-struct.no-toc>[data-sd-rows-host]{border-left:none;padding-left:0;}
16409
+ .vela-sd-toc-btn{text-align:left;padding:6px 10px;background:transparent;border:none;border-radius:var(--vela-radius-sm);color:var(--vela-fg-muted);font-family:inherit;font-size:12px;font-weight:600;cursor:pointer;transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
16410
+ .vela-sd-toc-btn:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
16411
+ .vela-sd-toc-btn.on{background:var(--vela-active);color:var(--vela-fg-bright);}
16412
+ /* Soft-disable: a subsection's enableKey is off \u2014 rows stay visible (browseable) but
16413
+ muted and non-interactive. Applied to each row's children so it survives display:contents;
16414
+ !important beats the inline opacity on labels. */
16415
+ .vela-sd-soft>*{opacity:0.4 !important;pointer-events:none !important;}
16254
16416
  `;
16255
16417
  document.head.appendChild(st);
16256
16418
  }
@@ -16271,6 +16433,10 @@ void main() {
16271
16433
  this.themeControl = null;
16272
16434
  /** The built tabs, by title — how `showSection` reaches a pane while the dialog is open. */
16273
16435
  this.tabs = [];
16436
+ /** Active instance-strip tab per chart type — remembered across dialog rebuilds. */
16437
+ this.typeActiveInstance = /* @__PURE__ */ new Map();
16438
+ /** Active TOC group per structured pane (`<typeId>/<pane>` → group label). */
16439
+ this.typeActiveGroup = /* @__PURE__ */ new Map();
16274
16440
  /** The tab currently shown, so a theme change (which rebuilds) lands back on it. */
16275
16441
  this.activeSection = null;
16276
16442
  if (getComputedStyle(container).position === "static") container.style.position = "relative";
@@ -16432,6 +16598,29 @@ void main() {
16432
16598
  baseline.append(this.numberRow("Width", config.baseline.width, 1, 10, 1, (v) => this.emit({ baseline: { width: v } })));
16433
16599
  groups.baseline = baseline;
16434
16600
  body.append(baseline);
16601
+ for (const def of chartTypes()) {
16602
+ if (!hasOwnCandlePaint(def.id)) continue;
16603
+ const bag = config.chartTypes[def.id] ?? {};
16604
+ const colorOf2 = (key, shared) => typeof bag[key] === "string" && bag[key] !== "" ? bag[key] : shared;
16605
+ const boolOf = (key, shared) => typeof bag[key] === "boolean" ? bag[key] : shared;
16606
+ const g = this.group();
16607
+ g.append(this.sectionTitle("Candles"));
16608
+ g.append(this.toggleRow("Body", boolOf("candleBodyVisible", config.candles.bodyVisible), (v) => this.emitType(def.id, "candleBodyVisible", v), [
16609
+ this.swatch(colorOf2("candleUpColor", config.candles.upColor), (v) => this.emitType(def.id, "candleUpColor", v)),
16610
+ this.swatch(colorOf2("candleDownColor", config.candles.downColor), (v) => this.emitType(def.id, "candleDownColor", v))
16611
+ ]));
16612
+ g.append(this.toggleRow("Borders", boolOf("candleBorderVisible", config.candles.borderVisible), (v) => this.emitType(def.id, "candleBorderVisible", v), [
16613
+ this.swatch(colorOf2("candleBorderUpColor", config.candles.borderUpColor), (v) => this.emitType(def.id, "candleBorderUpColor", v)),
16614
+ this.swatch(colorOf2("candleBorderDownColor", config.candles.borderDownColor), (v) => this.emitType(def.id, "candleBorderDownColor", v))
16615
+ ]));
16616
+ g.append(this.toggleRow("Wick", boolOf("candleWickVisible", config.candles.wickVisible), (v) => this.emitType(def.id, "candleWickVisible", v), [
16617
+ this.swatch(colorOf2("candleWickUpColor", config.candles.wickUpColor), (v) => this.emitType(def.id, "candleWickUpColor", v)),
16618
+ this.swatch(colorOf2("candleWickDownColor", config.candles.wickDownColor), (v) => this.emitType(def.id, "candleWickDownColor", v))
16619
+ ]));
16620
+ g.append(this.numberRow("Spacing", config.series.spacing, 0.1, 10, 0.1, (v) => this.emit({ series: { spacing: v } })));
16621
+ groups[def.id] = g;
16622
+ body.append(g);
16623
+ }
16435
16624
  showActive(config.series.style);
16436
16625
  body.append(this.sectionTitle("Time zone"));
16437
16626
  body.append(this.selectRowLabeled("Time zone", normalizeTimezone(config.timeScale.timezone), timezoneOptions(config.timeScale.timezone), (v) => this.emit({ timeScale: { timezone: v } })));
@@ -16446,7 +16635,16 @@ void main() {
16446
16635
  }
16447
16636
  }
16448
16637
  };
16638
+ const renderChartTypeSections = (placement) => {
16639
+ for (const def of chartTypes()) {
16640
+ const typeSettings = def.settings;
16641
+ if (!typeSettings) continue;
16642
+ if ((typeSettings.placement ?? "end") !== placement) continue;
16643
+ this.chartTypeSection(def.id, typeSettings, config, body);
16644
+ }
16645
+ };
16449
16646
  renderHostSections("symbol");
16647
+ renderChartTypeSections("after-symbol");
16450
16648
  renderHostSections("after-symbol");
16451
16649
  body.append(this.section("Scales and lines"));
16452
16650
  body.append(this.sectionTitle("Price scale"));
@@ -16487,27 +16685,7 @@ void main() {
16487
16685
  body.append(this.sectionTitle("Theme"));
16488
16686
  body.append(this.selectRow("Color theme", tc.current === "dark" ? "Dark" : "Light", ["Dark", "Light"], (v) => tc.onSelect(v === "Dark" ? "dark" : "light")));
16489
16687
  }
16490
- for (const def of chartTypes()) {
16491
- const typeSettings = def.settings;
16492
- if (!typeSettings) continue;
16493
- const marker = this.section(typeSettings.title);
16494
- marker.dataset.sdStyle = def.id;
16495
- marker.dataset.sdVisibility = typeSettings.visibility ?? "active";
16496
- body.append(marker);
16497
- const values = config.chartTypes[def.id] ?? {};
16498
- for (const r of typeSettings.rows) {
16499
- const current2 = values[r.key];
16500
- if (r.kind === "toggle") {
16501
- body.append(this.boolRow(r.label, typeof current2 === "boolean" ? current2 : r.defval, (v) => this.emitType(def.id, r.key, v)));
16502
- } else if (r.kind === "number") {
16503
- body.append(this.numberRow(r.label, typeof current2 === "number" ? current2 : r.defval, r.min ?? 0, r.max ?? 1e6, r.step ?? 1, (v) => this.emitType(def.id, r.key, v)));
16504
- } else if (r.kind === "color") {
16505
- body.append(this.colorRow(r.label, typeof current2 === "string" ? current2 : r.defval, (v) => this.emitType(def.id, r.key, v)));
16506
- } else {
16507
- body.append(this.selectRow(r.label, typeof current2 === "string" ? current2 : r.defval, [...r.options], (v) => this.emitType(def.id, r.key, v)));
16508
- }
16509
- }
16510
- }
16688
+ renderChartTypeSections("end");
16511
16689
  renderHostSections("end");
16512
16690
  const shell = document.createElement("div");
16513
16691
  shell.style.cssText = "display:flex;min-height:360px;max-height:calc(70vh - 100px);flex:1 1 auto;";
@@ -16526,7 +16704,7 @@ void main() {
16526
16704
  const tab = document.createElement("button");
16527
16705
  tab.type = "button";
16528
16706
  tab.textContent = title;
16529
- tab.className = "vela-sd-tab";
16707
+ tab.className = "vela-sd-tab" + (child.dataset.sdSub !== void 0 ? " vela-sd-tab-sub" : "");
16530
16708
  panes.push({ title, el, tab, style: child.dataset.sdStyle, visibility: child.dataset.sdVisibility });
16531
16709
  current = el;
16532
16710
  child.remove();
@@ -16566,7 +16744,14 @@ void main() {
16566
16744
  scrim.appendChild(dlg);
16567
16745
  this.container.appendChild(scrim);
16568
16746
  this.root = scrim;
16569
- for (const p of panes) this.layoutSettingsGrids(p.el, dlg);
16747
+ for (const p of panes) {
16748
+ const hosts = [...p.el.querySelectorAll("[data-sd-rows-host]")];
16749
+ if (hosts.length === 0) {
16750
+ this.layoutSettingsGrids(p.el, dlg);
16751
+ continue;
16752
+ }
16753
+ for (const h of hosts) this.layoutSettingsGrids(h, dlg);
16754
+ }
16570
16755
  }
16571
16756
  close() {
16572
16757
  closeColorPopover();
@@ -16596,6 +16781,291 @@ void main() {
16596
16781
  el.textContent = text;
16597
16782
  return el;
16598
16783
  }
16784
+ /**
16785
+ * One chart type's settings tab (and its subsection tabs), appended to the linear
16786
+ * `body` for the pane splitter to file. One live values BAG serves the whole section
16787
+ * — instances and subsections share the per-type store, so a `when` gate anywhere
16788
+ * can reference a key edited anywhere else; every edit runs every registered
16789
+ * refresher.
16790
+ */
16791
+ chartTypeSection(typeId, section, config, body) {
16792
+ const marker = this.section(section.title);
16793
+ marker.dataset.sdStyle = typeId;
16794
+ marker.dataset.sdVisibility = section.visibility ?? "active";
16795
+ body.append(marker);
16796
+ const values = config.chartTypes[typeId] ?? {};
16797
+ const bag = {};
16798
+ const seedKey = (key, want, defval) => {
16799
+ const v = values[key];
16800
+ bag[key] = typeof v === want ? v : defval;
16801
+ };
16802
+ const seed = (rows) => {
16803
+ for (const r of rows) {
16804
+ if (r.kind === "heading" || r.kind === "header") continue;
16805
+ if (r.kind === "range") {
16806
+ seedKey(r.minKey, "number", r.defval);
16807
+ seedKey(r.maxKey, "number", r.defval);
16808
+ continue;
16809
+ }
16810
+ seedKey(r.key, r.kind === "toggle" ? "boolean" : r.kind === "number" ? "number" : "string", r.defval);
16811
+ if (r.kind === "toggle") for (const c of r.colors ?? []) seedKey(c.key, "string", c.defval);
16812
+ }
16813
+ };
16814
+ if (section.rows) seed(section.rows);
16815
+ for (const inst of section.instances ?? []) seed(inst.rows);
16816
+ for (const sub of section.subsections ?? []) seed(sub.rows);
16817
+ for (const inst of section.instances ?? []) {
16818
+ if (!inst.enableKey || inst.enableKey in bag) continue;
16819
+ bag[inst.enableKey] = values[inst.enableKey] === true;
16820
+ }
16821
+ for (const sub of section.subsections ?? []) {
16822
+ if (!sub.enableKey || sub.enableKey in bag) continue;
16823
+ bag[sub.enableKey] = values[sub.enableKey] === true;
16824
+ }
16825
+ const refreshers = [];
16826
+ const put = (key, v) => {
16827
+ bag[key] = v;
16828
+ this.emitType(typeId, key, v);
16829
+ for (const r of refreshers) r();
16830
+ };
16831
+ if (section.instances && section.instances.length > 0) {
16832
+ body.append(this.instancesBlock(typeId, section.instances, bag, put, refreshers));
16833
+ } else if (section.rows) {
16834
+ this.flatTypeRows(section.rows, bag, put, refreshers, body);
16835
+ }
16836
+ for (const sub of section.subsections ?? []) {
16837
+ const subMarker = this.section(sub.title);
16838
+ subMarker.dataset.sdStyle = typeId;
16839
+ subMarker.dataset.sdVisibility = section.visibility ?? "active";
16840
+ subMarker.dataset.sdSub = "1";
16841
+ body.append(subMarker);
16842
+ body.append(this.groupedRows(`${typeId}/${sub.title}`, sub.rows, bag, put, refreshers, sub.enableKey));
16843
+ }
16844
+ for (const r of refreshers) r();
16845
+ }
16846
+ /** The FLAT chart-type form: rows appended straight to the body (the pane grid wraps
16847
+ * them), headings/headers as inline group titles, `when` gates refreshed live. */
16848
+ flatTypeRows(rows, bag, put, refreshers, body) {
16849
+ const entries = [];
16850
+ for (const r of rows) {
16851
+ const el = r.kind === "heading" || r.kind === "header" ? this.sectionTitle(r.label) : this.typeRow(r, bag, put);
16852
+ entries.push({ el, when: r.when });
16853
+ body.append(el);
16854
+ }
16855
+ refreshers.push(() => {
16856
+ for (const e of entries) e.el.classList.toggle("vela-sd-hide", !settingsRowVisible(e.when, bag));
16857
+ });
16858
+ }
16859
+ /** One value row for a chart-type descriptor, writing through `put`. */
16860
+ typeRow(r, bag, put) {
16861
+ if (r.kind === "toggle") {
16862
+ const swatches = (r.colors ?? []).map((c) => {
16863
+ const sw = this.swatch(bag[c.key], (v) => put(c.key, v));
16864
+ sw.title = c.label;
16865
+ return sw;
16866
+ });
16867
+ return this.toggleRow(r.label, bag[r.key], (v) => put(r.key, v), swatches);
16868
+ }
16869
+ if (r.kind === "number") return this.numberRow(r.label, bag[r.key], r.min ?? 0, r.max ?? 1e6, r.step ?? 1, (v) => put(r.key, v));
16870
+ if (r.kind === "color") return this.colorRow(r.label, bag[r.key], (v) => put(r.key, v));
16871
+ if (r.kind === "range") return this.rangeRow(r, bag, put);
16872
+ return this.selectRowLabeled(r.label, bag[r.key], normalizeSelectOptions(r.options), (v) => put(r.key, v));
16873
+ }
16874
+ /**
16875
+ * A MIN–MAX row: two number inputs on one row (stored under the descriptor's
16876
+ * `minKey`/`maxKey`). With a `placeholder`, an input at the DEFAULT value renders
16877
+ * empty showing it, and clearing an input stores the default back — the
16878
+ * placeholder names the unset state ('Off' for 0-disables bounds).
16879
+ */
16880
+ rangeRow(r, bag, put) {
16881
+ const { wrap } = this.row(r.label);
16882
+ const input = (key, title) => {
16883
+ const ni = document.createElement("input");
16884
+ ni.type = "number";
16885
+ ni.className = "vela-sd-number";
16886
+ ni.min = String(r.min ?? 0);
16887
+ ni.max = String(r.max ?? 1e6);
16888
+ ni.step = String(r.step ?? 1);
16889
+ ni.title = title;
16890
+ const current = bag[key];
16891
+ if (r.placeholder !== void 0) {
16892
+ ni.placeholder = r.placeholder;
16893
+ if (current !== r.defval) ni.value = String(current);
16894
+ } else {
16895
+ ni.value = String(current);
16896
+ }
16897
+ ni.addEventListener("change", () => {
16898
+ const raw = ni.value.trim() === "" ? r.defval : Number(ni.value);
16899
+ const v = Number.isFinite(raw) ? Math.min(r.max ?? Infinity, Math.max(r.min ?? -Infinity, raw)) : r.defval;
16900
+ ni.value = r.placeholder !== void 0 && v === r.defval ? "" : String(v);
16901
+ put(key, v);
16902
+ });
16903
+ return ni;
16904
+ };
16905
+ const box = document.createElement("div");
16906
+ box.style.cssText = "display:flex;align-items:center;gap:6px;flex:0 0 auto;";
16907
+ const dash = document.createElement("span");
16908
+ dash.textContent = "\u2013";
16909
+ dash.style.cssText = "color:var(--vela-fg-muted);";
16910
+ box.append(input(r.minKey, `${r.label} \u2014 min`), dash, input(r.maxKey, `${r.label} \u2014 max`));
16911
+ wrap.appendChild(box);
16912
+ return wrap;
16913
+ }
16914
+ /**
16915
+ * The INSTANCE STRIP block: a tab per present instance (label, `×` on the active
16916
+ * removable one), a dashed `+` while an instance is still absent, and one
16917
+ * grouped-rows content per instance below — only the active tab's content shows.
16918
+ * Presence is the boolean at each instance's `enableKey`; the strip rebuilds on
16919
+ * every section edit, so gates elsewhere stay coherent.
16920
+ */
16921
+ instancesBlock(typeId, instances, bag, put, refreshers) {
16922
+ const wrap = document.createElement("div");
16923
+ const strip = document.createElement("div");
16924
+ strip.className = "vela-sd-itabs";
16925
+ wrap.append(strip);
16926
+ const contents = instances.map((inst, i) => {
16927
+ const content = this.groupedRows(`${typeId}/#${i}`, inst.rows, bag, put, refreshers);
16928
+ wrap.append(content);
16929
+ return content;
16930
+ });
16931
+ const present = (inst) => !inst.enableKey || bag[inst.enableKey] === true;
16932
+ const refresh = () => {
16933
+ let active = this.typeActiveInstance.get(typeId) ?? 0;
16934
+ if (!present(instances[active] ?? instances[0])) active = 0;
16935
+ this.typeActiveInstance.set(typeId, active);
16936
+ strip.replaceChildren();
16937
+ instances.forEach((inst, i) => {
16938
+ if (!present(inst)) return;
16939
+ const tab = document.createElement("button");
16940
+ tab.type = "button";
16941
+ tab.className = "vela-sd-itab" + (i === active ? " on" : "");
16942
+ const lbl = document.createElement("span");
16943
+ lbl.textContent = inst.label;
16944
+ tab.append(lbl);
16945
+ if (i === active && inst.enableKey) {
16946
+ const x = document.createElement("span");
16947
+ x.className = "vela-sd-ix";
16948
+ x.textContent = "\u2715";
16949
+ x.title = `Remove ${inst.label}`;
16950
+ x.addEventListener("click", (e) => {
16951
+ e.stopPropagation();
16952
+ this.typeActiveInstance.set(typeId, 0);
16953
+ put(inst.enableKey, false);
16954
+ });
16955
+ tab.append(x);
16956
+ }
16957
+ tab.addEventListener("click", () => {
16958
+ this.typeActiveInstance.set(typeId, i);
16959
+ refresh();
16960
+ });
16961
+ strip.append(tab);
16962
+ });
16963
+ const absent = instances.findIndex((inst) => !present(inst));
16964
+ if (absent >= 0) {
16965
+ const add = document.createElement("button");
16966
+ add.type = "button";
16967
+ add.className = "vela-sd-itab vela-sd-itab-add";
16968
+ add.textContent = "+";
16969
+ add.title = `Add ${instances[absent].label}`;
16970
+ add.addEventListener("click", () => {
16971
+ this.typeActiveInstance.set(typeId, absent);
16972
+ put(instances[absent].enableKey, true);
16973
+ });
16974
+ strip.append(add);
16975
+ }
16976
+ contents.forEach((c, i) => c.classList.toggle("vela-sd-hide", i !== active));
16977
+ };
16978
+ refreshers.push(refresh);
16979
+ return wrap;
16980
+ }
16981
+ /**
16982
+ * A GROUPED rows pane: a TOC column of the group labels (from `heading` rows) and
16983
+ * ONE rows host holding every row — the active group's rows show, the rest hide.
16984
+ * Rows BEFORE the first heading are the always block, visible above every group.
16985
+ * `header` rows stay in the rows column as in-group subgroup titles (not TOC
16986
+ * entries). A group whose value rows are all gated out (or whose heading's own
16987
+ * `when` fails) leaves the TOC; the whole TOC hides when no group is live. The
16988
+ * rows host is tagged for `layoutSettingsGrids`, keeping one shared label column
16989
+ * across all groups.
16990
+ *
16991
+ * `enableKey` (optional): while that bag boolean is false, every row except the one
16992
+ * whose key matches is soft-disabled (visible but grayed / non-interactive) so the
16993
+ * pane stays browseable with the feature off.
16994
+ */
16995
+ groupedRows(paneKey, rows, bag, put, refreshers, enableKey) {
16996
+ const wrap = document.createElement("div");
16997
+ wrap.className = "vela-sd-struct";
16998
+ const toc = document.createElement("div");
16999
+ toc.className = "vela-sd-toc";
17000
+ const rowsHost = document.createElement("div");
17001
+ rowsHost.dataset.sdRowsHost = "1";
17002
+ rowsHost.style.cssText = "flex:1 1 auto;min-width:0;";
17003
+ wrap.append(toc, rowsHost);
17004
+ const entries = [];
17005
+ const groups = [];
17006
+ const groupWhens = [];
17007
+ let g = -1;
17008
+ for (const r of rows) {
17009
+ if (r.kind === "heading") {
17010
+ g = groups.length;
17011
+ groups.push(r.label);
17012
+ groupWhens.push(r.when);
17013
+ continue;
17014
+ }
17015
+ if (r.kind === "header") {
17016
+ entries.push({ el: this.sectionTitle(r.label), when: r.when, group: g, header: true });
17017
+ rowsHost.append(entries[entries.length - 1].el);
17018
+ continue;
17019
+ }
17020
+ const el = this.typeRow(r, bag, put);
17021
+ const key = r.kind === "range" ? void 0 : r.key;
17022
+ entries.push({ el, when: r.when, group: g, key });
17023
+ rowsHost.append(el);
17024
+ }
17025
+ const refresh = () => {
17026
+ const live = groups.map((_, gi) => settingsRowVisible(groupWhens[gi], bag) && entries.some((e) => e.group === gi && !e.header && settingsRowVisible(e.when, bag)));
17027
+ let activeIdx = groups.indexOf(this.typeActiveGroup.get(paneKey) ?? "");
17028
+ if (activeIdx < 0 || !live[activeIdx]) activeIdx = live.findIndex(Boolean);
17029
+ if (activeIdx >= 0) this.typeActiveGroup.set(paneKey, groups[activeIdx]);
17030
+ toc.replaceChildren();
17031
+ groups.forEach((label, gi) => {
17032
+ if (!live[gi]) return;
17033
+ const b = document.createElement("button");
17034
+ b.type = "button";
17035
+ b.className = "vela-sd-toc-btn" + (gi === activeIdx ? " on" : "");
17036
+ b.textContent = label;
17037
+ b.addEventListener("click", () => {
17038
+ this.typeActiveGroup.set(paneKey, label);
17039
+ refresh();
17040
+ });
17041
+ toc.append(b);
17042
+ });
17043
+ const anyGroup = live.some(Boolean);
17044
+ toc.classList.toggle("vela-sd-hide", !anyGroup);
17045
+ wrap.classList.toggle("no-toc", !anyGroup);
17046
+ const enabled = !enableKey || bag[enableKey] === true;
17047
+ for (let i = 0; i < entries.length; i++) {
17048
+ const e = entries[i];
17049
+ let visible = (e.group === -1 || e.group === activeIdx) && settingsRowVisible(e.when, bag);
17050
+ if (visible && e.header) {
17051
+ let hasContent = false;
17052
+ for (let j = i + 1; j < entries.length; j++) {
17053
+ const n = entries[j];
17054
+ if (n.group !== e.group || n.header) break;
17055
+ if (settingsRowVisible(n.when, bag)) {
17056
+ hasContent = true;
17057
+ break;
17058
+ }
17059
+ }
17060
+ visible = hasContent;
17061
+ }
17062
+ e.el.classList.toggle("vela-sd-hide", !visible);
17063
+ e.el.classList.toggle("vela-sd-soft", visible && !enabled && e.key !== enableKey);
17064
+ }
17065
+ };
17066
+ refreshers.push(refresh);
17067
+ return wrap;
17068
+ }
16599
17069
  /** Vertical breathing space between row clusters — grouping reads from whitespace,
16600
17070
  * so no drawn rule. */
16601
17071
  separator() {
@@ -16858,7 +17328,7 @@ void main() {
16858
17328
  const { wrap } = this.row(label);
16859
17329
  const sel = document.createElement("select");
16860
17330
  sel.className = "vela-sd-select";
16861
- sel.style.cssText = "max-width:140px;flex:0 0 auto;";
17331
+ sel.style.cssText = "max-width:200px;flex:0 0 auto;";
16862
17332
  for (const [val, lbl] of options) {
16863
17333
  const o = document.createElement("option");
16864
17334
  o.value = val;
@@ -16874,7 +17344,7 @@ void main() {
16874
17344
  const { wrap } = this.row(label);
16875
17345
  const sel = document.createElement("select");
16876
17346
  sel.className = "vela-sd-select";
16877
- sel.style.cssText = "max-width:140px;flex:0 0 auto;";
17347
+ sel.style.cssText = "max-width:200px;flex:0 0 auto;";
16878
17348
  for (const opt of options) {
16879
17349
  const o = document.createElement("option");
16880
17350
  o.value = opt;
@@ -16911,6 +17381,9 @@ void main() {
16911
17381
  }
16912
17382
  };
16913
17383
  var AUTO_MANUAL_OPTS = [["auto", "Auto"], ["manual", "Manual"]];
17384
+ function normalizeSelectOptions(options) {
17385
+ return options.map((o) => typeof o === "string" ? [o, o] : o);
17386
+ }
16914
17387
  function timezoneOptions(current) {
16915
17388
  const options = TIMEZONES.map((t) => [t.value, tzMenuLabel(t.value, t.label)]);
16916
17389
  const normalized = normalizeTimezone(current);
@@ -18269,6 +18742,8 @@ void main() {
18269
18742
  // src/renderers/native/drawings/DrawingInteraction.ts
18270
18743
  var DRAG_SLOP2 = 3;
18271
18744
  var FREEHAND_SAMPLE_PX = 4;
18745
+ var ANGLE_SNAP_STEP = Math.PI / 4;
18746
+ var ANGLE_SNAP_TYPES = /* @__PURE__ */ new Set(["trendline", "ray", "extendedline", "infoline", "trendangle", "arrow"]);
18272
18747
  var DrawingInteraction = class {
18273
18748
  // magnet target for the ring
18274
18749
  constructor(deps) {
@@ -18309,6 +18784,27 @@ void main() {
18309
18784
  this.snapAt = changed ? { point: snapped, paneId } : null;
18310
18785
  return snapped;
18311
18786
  }
18787
+ /** Resolve a pixel to a data point with the segment angle locked to 45° steps around
18788
+ * `pivot` (Shift held on a line tool). Works in PIXEL space — the user reasons about
18789
+ * the angle they see, not about time/price units. The magnet is bypassed: snapping
18790
+ * the result to a candle afterwards would break the exact angle just enforced. */
18791
+ resolveAngleSnapped(pivot, paneId, x, y) {
18792
+ const proj = this.deps.projector();
18793
+ const px = proj.xOf(pivot.time);
18794
+ const py = proj.yOf(pivot.price, paneId);
18795
+ this.snapAt = null;
18796
+ if (py == null) return proj.pxToPoint(x, y, paneId);
18797
+ const r = Math.hypot(x - px, y - py);
18798
+ if (r < 1) return proj.pxToPoint(x, y, paneId);
18799
+ const angle = Math.round(Math.atan2(y - py, x - px) / ANGLE_SNAP_STEP) * ANGLE_SNAP_STEP;
18800
+ return proj.pxToPoint(px + r * Math.cos(angle), py + r * Math.sin(angle), paneId);
18801
+ }
18802
+ /** The pivot the free endpoint angle-snaps around while placing `draft`, or null when
18803
+ * the tool is not a two-point segment (or no anchor is committed yet). */
18804
+ placingPivot(draft) {
18805
+ if (!ANGLE_SNAP_TYPES.has(draft.type)) return null;
18806
+ return draft.anchors[draft.anchors.length - 1] ?? null;
18807
+ }
18312
18808
  /** Should the drawing layer win this press (vs pan)? Armed/placing/pressed, or over a drawing/handle. */
18313
18809
  claim(x, y) {
18314
18810
  if (this.deps.activeTool() != null || this.state.kind !== "idle") return true;
@@ -18330,12 +18826,15 @@ void main() {
18330
18826
  return this.state.kind === "pressed" && this.state.moved ? this.state.id : null;
18331
18827
  }
18332
18828
  /** A press: place the next anchor, or grab a drawing/handle (resolved as click or drag on release).
18333
- * `shift` over a drawing toggles it in/out of the selection (multi-select) instead of dragging. */
18829
+ * `shift` over a drawing toggles it in/out of the selection (multi-select) instead of dragging;
18830
+ * while placing a line tool it locks the segment angle to 45° steps. */
18334
18831
  down(x, y, mode = "off", shift = false) {
18335
18832
  const proj = this.deps.projector();
18336
18833
  if (this.state.kind === "placing") {
18337
18834
  if (this.state.draft.placementMode() !== "click") return;
18338
- this.state.draft.anchors.push(this.placementAnchor(this.state.draft, this.resolve(x, y, this.state.draft.paneId, mode)));
18835
+ const pivot = shift ? this.placingPivot(this.state.draft) : null;
18836
+ const point = pivot ? this.resolveAngleSnapped(pivot, this.state.draft.paneId, x, y) : this.resolve(x, y, this.state.draft.paneId, mode);
18837
+ this.state.draft.anchors.push(this.placementAnchor(this.state.draft, point));
18339
18838
  if (this.state.draft.anchors.length >= this.state.need) this.finalize();
18340
18839
  else this.deps.changed();
18341
18840
  return;
@@ -18360,8 +18859,9 @@ void main() {
18360
18859
  this.state = { kind: "pressed", id: hit.id, handle, grab: proj.pxToPoint(x, y, hit.paneId), orig: cloneAnchors(hit), px: x, py: y, moved: false };
18361
18860
  }
18362
18861
  }
18363
- /** Pointer move: advance the placing ghost, or live-preview a drag once past the slop. */
18364
- move(x, y, mode = "off") {
18862
+ /** Pointer move: advance the placing ghost, or live-preview a drag once past the slop.
18863
+ * `shift` on a line tool locks the segment angle to 45° steps (placing + handle drags). */
18864
+ move(x, y, mode = "off", shift = false) {
18365
18865
  if (this.state.kind === "idle") {
18366
18866
  const tool = this.deps.activeTool();
18367
18867
  const had = this.snapAt != null;
@@ -18375,7 +18875,8 @@ void main() {
18375
18875
  this.captureFreehand(x, y);
18376
18876
  return;
18377
18877
  }
18378
- this.state.cursor = this.resolve(x, y, this.state.draft.paneId, mode);
18878
+ const pivot = shift ? this.placingPivot(this.state.draft) : null;
18879
+ this.state.cursor = pivot ? this.resolveAngleSnapped(pivot, this.state.draft.paneId, x, y) : this.resolve(x, y, this.state.draft.paneId, mode);
18379
18880
  this.deps.changed();
18380
18881
  return;
18381
18882
  }
@@ -18386,7 +18887,11 @@ void main() {
18386
18887
  this.state.moved = true;
18387
18888
  if (this.state.handle >= 0) {
18388
18889
  const a = d.anchors[this.state.handle];
18389
- if (a) applyFree(a, this.resolve(x, y, d.paneId, mode), d.anchorSchema().slots[this.state.handle]?.free ?? "both");
18890
+ if (a) {
18891
+ const pivot = shift && ANGLE_SNAP_TYPES.has(d.type) && d.anchors.length === 2 ? d.anchors[1 - this.state.handle] : void 0;
18892
+ const point = pivot ? this.resolveAngleSnapped(pivot, d.paneId, x, y) : this.resolve(x, y, d.paneId, mode);
18893
+ applyFree(a, point, d.anchorSchema().slots[this.state.handle]?.free ?? "both");
18894
+ }
18390
18895
  d.constrainHandleDrag(this.state.handle);
18391
18896
  } else {
18392
18897
  const pt = this.deps.projector().pxToPoint(x, y, d.paneId);
@@ -19739,6 +20244,8 @@ void main() {
19739
20244
  this.eraserActive = false;
19740
20245
  this.stayBtn = null;
19741
20246
  this.stayActive = false;
20247
+ this.syncBtn = null;
20248
+ this.syncActive = false;
19742
20249
  this.collapseBtn = null;
19743
20250
  this.collapsed = false;
19744
20251
  this.visible = false;
@@ -19755,6 +20262,7 @@ void main() {
19755
20262
  this.dock = options.dock ?? "absolute";
19756
20263
  this.onCollapse = options.onCollapse ?? (() => {
19757
20264
  });
20265
+ this.onDrawingsSync = options.onDrawingsSync ?? null;
19758
20266
  ensureStyles4();
19759
20267
  this.root = document.createElement("div");
19760
20268
  this.root.className = "vela-dtb";
@@ -19853,6 +20361,10 @@ void main() {
19853
20361
  this.root.appendChild(this.makeMagnetCell());
19854
20362
  this.stayBtn = this.makeButton(STAY_ICON, "Stay in drawing mode", () => this.toggleStay());
19855
20363
  this.root.appendChild(this.stayBtn);
20364
+ if (this.onDrawingsSync) {
20365
+ this.syncBtn = this.makeButton(SYNC_ICON, "Sync drawings on all charts", () => this.toggleDrawingsSync());
20366
+ this.root.appendChild(this.syncBtn);
20367
+ }
19856
20368
  this.collapseBtn = this.makeButton(COLLAPSE_ICON, "Collapse toolbar", () => this.toggleCollapsed());
19857
20369
  this.collapseBtn.classList.add("vela-dtb-collapse");
19858
20370
  this.root.appendChild(this.collapseBtn);
@@ -19860,6 +20372,7 @@ void main() {
19860
20372
  this.paintEraser();
19861
20373
  this.paintMagnet();
19862
20374
  this.paintStay();
20375
+ this.paintDrawingsSync();
19863
20376
  this.paintCollapse();
19864
20377
  this.highlight();
19865
20378
  }
@@ -19910,7 +20423,7 @@ void main() {
19910
20423
  cell.appendChild(arrow);
19911
20424
  }
19912
20425
  this.tipDisposers.push(
19913
- attachChromeTooltip(cell, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(cell) ?? label, placement: "right", delayMs: 2e3 })
20426
+ attachChromeTooltip(cell, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(cell) ?? label, placement: "right" })
19914
20427
  );
19915
20428
  return { cell, icon: icon2, arrow };
19916
20429
  }
@@ -20020,6 +20533,23 @@ void main() {
20020
20533
  b.dataset.active = this.stayActive ? "1" : "";
20021
20534
  b.style.opacity = this.stayActive ? "1" : "0.5";
20022
20535
  }
20536
+ /** Toggle drawings sync on/off and notify the host. */
20537
+ toggleDrawingsSync() {
20538
+ this.syncActive = !this.syncActive;
20539
+ this.onDrawingsSync?.(this.syncActive);
20540
+ this.paintDrawingsSync();
20541
+ }
20542
+ /** Reflect drawings sync externally (e.g. set through the API) without notifying back. */
20543
+ setDrawingsSyncMode(on) {
20544
+ this.syncActive = on;
20545
+ this.paintDrawingsSync();
20546
+ }
20547
+ paintDrawingsSync() {
20548
+ const b = this.syncBtn;
20549
+ if (!b) return;
20550
+ b.dataset.active = this.syncActive ? "1" : "";
20551
+ b.style.opacity = this.syncActive ? "1" : "0.5";
20552
+ }
20023
20553
  // ── flyout ──
20024
20554
  /** Create the positioned flyout panel anchored to a cell, register it as open, and start the
20025
20555
  * outside-dismiss. Callers fill it with items. */
@@ -20179,6 +20709,8 @@ void main() {
20179
20709
  const type = this.lastUsed.get(group.id) ?? group.tools[0]?.type;
20180
20710
  const tool = group.tools.find((t) => t.type === type) ?? group.tools[0];
20181
20711
  iconBtn.innerHTML = hitHtml(tool?.icon ?? "");
20712
+ const cell = this.groupCells.get(group.id);
20713
+ if (cell) this.tipText.set(cell, tool?.label ?? group.label);
20182
20714
  }
20183
20715
  /** Reconcile active tints from state (armed group / open flyout / cursor / modes). Active is a
20184
20716
  * `data-active` attribute the stylesheet paints — no inline background, so hover never sticks. */
@@ -20202,7 +20734,7 @@ void main() {
20202
20734
  this.tipText.set(btn, title);
20203
20735
  if (icon2) btn.innerHTML = hitHtml(icon2);
20204
20736
  this.tipDisposers.push(
20205
- attachChromeTooltip(btn, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(btn) ?? title, placement: "right", delayMs: 2e3 })
20737
+ attachChromeTooltip(btn, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(btn) ?? title, placement: "right" })
20206
20738
  );
20207
20739
  btn.addEventListener("click", onClick);
20208
20740
  return btn;
@@ -20268,6 +20800,7 @@ void main() {
20268
20800
  var EXPAND_ICON = icon("chevrons-right");
20269
20801
  var MAGNET_ICON = icon("magnet");
20270
20802
  var STAY_ICON = icon("pen-lock");
20803
+ var SYNC_ICON = icon("pen-sync");
20271
20804
  var ERASER_ICON = icon("eraser");
20272
20805
  var ARROW_ICON2 = icon("chevron-right");
20273
20806
  var CHECK_ICON = icon("check");
@@ -20491,6 +21024,10 @@ void main() {
20491
21024
  this.activeTool = null;
20492
21025
  // last-used style for the armed tool (seeds the placement ghost)
20493
21026
  this.intentCb = null;
21027
+ /** Another chart's in-progress placement, mirrored here as a ghost (drawings sync). */
21028
+ this.externalGhost = null;
21029
+ /** Last draft fingerprint reported upstream — gates the per-render emission to actual changes. */
21030
+ this.lastDraftKey = null;
20494
21031
  this.measure = new MeasureOverlay();
20495
21032
  // transient ruler — not a persistent drawing
20496
21033
  this.toolbarVisible = false;
@@ -20562,6 +21099,11 @@ void main() {
20562
21099
  syncToolbarGutter() {
20563
21100
  this.deps.setToolbarGutter(this.toolbarVisible ? this.toolbarCollapsed ? TOOLBAR_COLLAPSED_WIDTH : TOOLBAR_WIDTH : 0);
20564
21101
  }
21102
+ /** Core push: mirror (or clear) another chart's in-progress placement as a ghost. */
21103
+ setExternalGhost(doc) {
21104
+ this.externalGhost = doc ? deserializeDrawing(doc) : null;
21105
+ this.render();
21106
+ }
20565
21107
  syncDrawings(docs) {
20566
21108
  this.drawings = docs.map((d) => deserializeDrawing(d)).filter((d) => d != null);
20567
21109
  if (this.textEditor && !this.editedDrawing(this.textEditor.id)) this.closeTextEditor();
@@ -20720,7 +21262,7 @@ void main() {
20720
21262
  }
20721
21263
  this.interaction.down(x, y, snap, shift);
20722
21264
  }
20723
- pointerMove(x, y, snap = "off") {
21265
+ pointerMove(x, y, snap = "off", shift = false) {
20724
21266
  if (this.eraserMode) {
20725
21267
  if (this.erasing) this.deleteAt(x, y);
20726
21268
  return;
@@ -20730,7 +21272,7 @@ void main() {
20730
21272
  this.render();
20731
21273
  return;
20732
21274
  }
20733
- this.interaction.move(x, y, snap);
21275
+ this.interaction.move(x, y, snap, shift);
20734
21276
  this.updateHover(x, y);
20735
21277
  }
20736
21278
  /** Track which drawing is hovered so its handles appear on hover (and only then). */
@@ -21116,6 +21658,8 @@ void main() {
21116
21658
  this.layoutTextEditor();
21117
21659
  const ghost = this.interaction.ghost();
21118
21660
  if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
21661
+ if (this.externalGhost) this.painter.paintGhost(ctx, this.externalGhost, proj, this.deps.theme());
21662
+ this.emitDraft(ghost);
21119
21663
  const markers = this.interaction.placingMarkers(proj);
21120
21664
  if (markers) this.painter.paintHandles(ctx, markers);
21121
21665
  const m = this.interaction.snapMarker();
@@ -21177,6 +21721,15 @@ void main() {
21177
21721
  }
21178
21722
  }, () => this.clearSelection());
21179
21723
  }
21724
+ /** Report placement progress upstream (`draft` intent) so the drawings sync can
21725
+ * mirror the ghost live on linked charts. Called from every render; only actual
21726
+ * shape changes emit, and the end of a placement emits one `null`. */
21727
+ emitDraft(ghost) {
21728
+ const key = ghost ? JSON.stringify({ t: ghost.type, p: ghost.paneId, a: ghost.anchors, s: ghost.style, x: ghost.text }) : null;
21729
+ if (key === this.lastDraftKey) return;
21730
+ this.lastDraftKey = key;
21731
+ this.intentCb?.({ kind: "draft", doc: ghost ? ghost.serialize() : null });
21732
+ }
21180
21733
  emit(i) {
21181
21734
  if (i.kind === "create") {
21182
21735
  if (i.doc.text && i.doc.text.color === void 0) {
@@ -21720,10 +22273,10 @@ void main() {
21720
22273
  var NativeRenderer = class {
21721
22274
  constructor(opts) {
21722
22275
  this.capabilities = NATIVE_CAPABILITIES;
21723
- // The app "chrome" surface (drawing toolbar + axis-scale gutters): background + text.
22276
+ // The app "chrome" surface (drawing toolbar + in-chart dialogs): background + text.
21724
22277
  // Owned by the app theme alone (mount/setTheme) — config-level background/text edits
21725
- // (settings dialog, persisted configs) recolor only the plot, keeping the toolbar and
21726
- // scales on a consistent, always-readable surface.
22278
+ // (settings dialog, persisted configs) recolor the plot AND the axis scales (see
22279
+ // axisSurface), but never the toolbar/dialog chrome.
21727
22280
  this.surfaceBackground = DARK_THEME.background;
21728
22281
  this.surfaceTextColor = DARK_THEME.textColor;
21729
22282
  // the plot area (canvases + DOM overlays), inset to the right of the toolbar gutter
@@ -21923,6 +22476,7 @@ void main() {
21923
22476
  this.candleDown = opts.downColor;
21924
22477
  this.scene.priceStyle = opts.priceStyle;
21925
22478
  this.scene.basePainting = basePaintingOf(opts.priceStyle);
22479
+ this.scene.candleOverride = candleOverrideFor(opts.priceStyle, this.scene.style.chartTypes);
21926
22480
  }
21927
22481
  this.theme = this.deriveTheme(DARK_THEME);
21928
22482
  }
@@ -22400,6 +22954,7 @@ void main() {
22400
22954
  baselineLevel: next.baseline.baselineLevel
22401
22955
  };
22402
22956
  this.setPriceStyle(next.series.style);
22957
+ this.scene.candleOverride = candleOverrideFor(this.scene.priceStyle, s.chartTypes);
22403
22958
  this.scene.baselineValue = next.series.baseline;
22404
22959
  this.coords.setPitchScale(next.series.spacing);
22405
22960
  this.coords.setViewport(this.clampViewport(this.coords.getViewport().barSpacing, this.coords.getViewport().rightOffset));
@@ -22505,7 +23060,7 @@ void main() {
22505
23060
  (patch) => this.applyConfig(patch),
22506
23061
  (json) => this.applyConfig(json),
22507
23062
  () => {
22508
- if (this.factoryConfig) this.applyConfig(this.factoryConfig);
23063
+ if (this.factoryConfig) this.applyConfig(factoryResetConfig(this.factoryConfig));
22509
23064
  this.settingsDialog?.close();
22510
23065
  this.openSettingsDialog();
22511
23066
  },
@@ -22706,10 +23261,16 @@ void main() {
22706
23261
  borderColor: isDarkColor(this.surfaceBackground) ? DARK_THEME.borderColor : LIGHT_THEME.borderColor
22707
23262
  };
22708
23263
  }
23264
+ /** The colors the axis-scale gutters (price + time) paint with: the LIVE chart background
23265
+ * (`layout.background`) and its contrast-corrected text, so the scales read as part of the
23266
+ * plot. Only the toolbar/dialog chrome stays on the stable app-theme surface. */
23267
+ axisSurface() {
23268
+ return { background: this.theme.background, textColor: this.theme.textColor };
23269
+ }
22709
23270
  // ── lifecycle ──
22710
23271
  mount(container, theme) {
22711
23272
  this.mountContainer = container;
22712
- this.publishToolbarGutter();
23273
+ this.publishGutters();
22713
23274
  this.theme = this.deriveTheme(theme);
22714
23275
  this.surfaceBackground = theme.background;
22715
23276
  this.surfaceTextColor = theme.textColor;
@@ -22781,7 +23342,7 @@ void main() {
22781
23342
  drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y) ?? false,
22782
23343
  drawingsSnapMode: () => this.snapMode,
22783
23344
  drawingsPointerDown: (x, y, snap, shift) => this.userDrawings?.pointerDown(x, y, snap, shift),
22784
- drawingsPointerMove: (x, y, snap) => this.userDrawings?.pointerMove(x, y, snap),
23345
+ drawingsPointerMove: (x, y, snap, shift) => this.userDrawings?.pointerMove(x, y, snap, shift),
22785
23346
  drawingsPointerUp: (x, y) => this.userDrawings?.pointerUp(x, y),
22786
23347
  drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
22787
23348
  drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
@@ -23018,6 +23579,7 @@ void main() {
23018
23579
  this.attributionEl?.remove();
23019
23580
  this.attributionEl = null;
23020
23581
  this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
23582
+ this.mountContainer?.style.removeProperty("--vela-scale-gutter");
23021
23583
  this.mountContainer = null;
23022
23584
  this.wrapper?.remove();
23023
23585
  }
@@ -23186,6 +23748,7 @@ void main() {
23186
23748
  const w = RIGHT_AXIS_W + AXIS_COL_W * this.maxOwnScaleColumns();
23187
23749
  if (w === this.rightAxisW) return false;
23188
23750
  this.rightAxisW = w;
23751
+ this.publishGutters();
23189
23752
  if (this.coords.width > 0) this.syncSize();
23190
23753
  return true;
23191
23754
  }
@@ -23345,6 +23908,7 @@ void main() {
23345
23908
  if (style === this.scene.priceStyle) return;
23346
23909
  this.scene.priceStyle = style;
23347
23910
  this.scene.basePainting = basePaintingOf(style);
23911
+ this.scene.candleOverride = candleOverrideFor(style, this.scene.style.chartTypes);
23348
23912
  for (const cb of this.priceStyleCbs) cb(style);
23349
23913
  }
23350
23914
  onRemoveIndicator(cb) {
@@ -23506,7 +24070,10 @@ void main() {
23506
24070
  active = true;
23507
24071
  }
23508
24072
  }
23509
- this.coords.setViewport(this.clampViewport(barSpacing, rightOffset));
24073
+ const clamped = this.clampViewport(barSpacing, rightOffset);
24074
+ if (clamped.barSpacing !== barSpacing) this.targetBarSpacing = clamped.barSpacing;
24075
+ if (this.scrollTargetRO != null && clamped.rightOffset !== rightOffset) this.scrollTargetRO = clamped.rightOffset;
24076
+ this.coords.setViewport(clamped);
23510
24077
  this.computeScales();
23511
24078
  if (this.easeScales(dtMs)) active = true;
23512
24079
  if (this.easeLiveBar(dtMs)) active = true;
@@ -23958,9 +24525,37 @@ void main() {
23958
24525
  this.paintData();
23959
24526
  } else if (repaintsChrome(level) && this.paintedData) {
23960
24527
  this.chrome.prepare(this.scene, this.coords, this.theme);
23961
- this.chrome.render(this.scene, this.coords, this.theme, { background: this.surfaceBackground, textColor: this.surfaceTextColor });
24528
+ this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
23962
24529
  }
23963
24530
  this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
24531
+ if (!repaintsData(level) && this.paintedData) this.repaintCursorLayers();
24532
+ }
24533
+ /** Repaint the SDK layers that opted into cursor tracking (their own canvas only). */
24534
+ repaintCursorLayers() {
24535
+ const pane = this.scene.panes.get(PRICE_PANE_ID);
24536
+ if (!pane) return;
24537
+ const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
24538
+ for (const l of this.extLayers) {
24539
+ if (!l.def.repaintOnCursor) continue;
24540
+ l.instance.render(this.extLayerArgs(l.def.id, pane.scale, pane.bounds, nowMs));
24541
+ if (this.animZoom && l.instance.animating?.()) this.animator.start();
24542
+ }
24543
+ }
24544
+ /** One frame's args for an SDK renderer layer (shared by the data + cursor paint paths). */
24545
+ extLayerArgs(id, scale, bounds, nowMs) {
24546
+ return {
24547
+ bars: this.scene.bars,
24548
+ data: this.scene.nativeData.get(id),
24549
+ settings: this.scene.nativeData.get(`${id}-settings`) ?? {},
24550
+ pending: this.scene.nativePending.get(id) ?? [],
24551
+ coords: this.coords,
24552
+ scale,
24553
+ bounds,
24554
+ theme: this.theme,
24555
+ priceStyle: this.scene.priceStyle,
24556
+ nowMs,
24557
+ cursor: this.lastPointer
24558
+ };
23964
24559
  }
23965
24560
  /** Paint the below-data (L-1) + geometry (L0) + chrome (L1) layers from the current scene/coords. */
23966
24561
  paintData() {
@@ -23993,18 +24588,16 @@ void main() {
23993
24588
  });
23994
24589
  const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
23995
24590
  for (const l of this.extLayers) {
23996
- l.instance.render({
23997
- bars: this.scene.bars,
23998
- data: this.scene.nativeData.get(l.def.id),
23999
- settings: this.scene.nativeData.get(`${l.def.id}-settings`) ?? {},
24000
- pending: this.scene.nativePending.get(l.def.id) ?? [],
24001
- coords: this.coords,
24002
- scale: pane.scale,
24003
- bounds: pane.bounds,
24004
- theme: this.theme,
24005
- priceStyle: this.scene.priceStyle,
24006
- nowMs
24007
- });
24591
+ const args = this.extLayerArgs(l.def.id, pane.scale, pane.bounds, nowMs);
24592
+ l.instance.render(args);
24593
+ if (l.def.id === this.scene.priceStyle && l.instance.modulateBase) {
24594
+ const mod = l.instance.modulateBase(args);
24595
+ if (mod) {
24596
+ if (mod.candleBodyScale != null) this.backend.candleBodyScale = clamp012(mod.candleBodyScale) || 0.01;
24597
+ if (mod.candleBodyAlpha != null) this.backend.candleBodyAlpha = clamp012(mod.candleBodyAlpha) * this.candleBodyAlpha;
24598
+ if (mod.gridAlpha != null) this.backend.gridAlpha = clamp012(mod.gridAlpha);
24599
+ }
24600
+ }
24008
24601
  if (this.animZoom && l.instance.animating?.()) this.animator.start();
24009
24602
  }
24010
24603
  }
@@ -24014,7 +24607,7 @@ void main() {
24014
24607
  if (easeLive && liveActual) this.bars[li] = { ...liveActual, high: this.liveEaseHigh, low: this.liveEaseLow, close: this.liveEaseClose };
24015
24608
  this.scene.drawingSlices = this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map();
24016
24609
  this.backend.render(this.scene, this.coords, this.theme);
24017
- this.chrome.render(this.scene, this.coords, this.theme, { background: this.surfaceBackground, textColor: this.surfaceTextColor });
24610
+ this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
24018
24611
  this.userDrawings?.render();
24019
24612
  if (easeLive && liveActual) this.bars[li] = liveActual;
24020
24613
  this.paintedData = true;
@@ -24448,15 +25041,18 @@ void main() {
24448
25041
  setToolbarGutter(px) {
24449
25042
  if (px === this.toolbarGutter) return;
24450
25043
  this.toolbarGutter = px;
24451
- this.publishToolbarGutter();
25044
+ this.publishGutters();
24452
25045
  this.positionAttribution();
24453
25046
  this.syncSize();
24454
25047
  }
24455
- /** Publish the gutter on the mount container as `--vela-toolbar-gutter`, so host
24456
- * overlays sharing that container (a status line, a custom legend) can anchor to
24457
- * the plot's left edge without reaching into the renderer's DOM. */
24458
- publishToolbarGutter() {
25048
+ /** Publish the gutters on the mount container as `--vela-toolbar-gutter` (left,
25049
+ * drawings toolbar) and `--vela-scale-gutter` (right, the full price-scale width
25050
+ * incl. merged own-scale columns), so host overlays sharing that container (a
25051
+ * status line, a watermark, a custom legend) can anchor to the plot's edges
25052
+ * without reaching into the renderer's DOM. */
25053
+ publishGutters() {
24459
25054
  this.mountContainer?.style.setProperty("--vela-toolbar-gutter", `${this.toolbarGutter}px`);
25055
+ this.mountContainer?.style.setProperty("--vela-scale-gutter", `${this.rightAxisW}px`);
24460
25056
  }
24461
25057
  /** The built-in mark, or the host's own when one is set. */
24462
25058
  buildAttributionEl() {
@@ -24526,7 +25122,13 @@ void main() {
24526
25122
  this.fitContent();
24527
25123
  this.didInitialFit = true;
24528
25124
  }
24529
- this.scheduler.flushNow(4 /* Full */);
25125
+ if (this.animator.active) {
25126
+ this.computeScales();
25127
+ this.paintData();
25128
+ this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
25129
+ } else {
25130
+ this.scheduler.flushNow(4 /* Full */);
25131
+ }
24530
25132
  }
24531
25133
  applyBackground() {
24532
25134
  this.wrapper.style.background = this.theme.background;
@@ -24595,6 +25197,9 @@ void main() {
24595
25197
  }
24596
25198
  return out.sort((a, b) => a.from - b.from);
24597
25199
  }
25200
+ function clamp012(v) {
25201
+ return Math.max(0, Math.min(1, v));
25202
+ }
24598
25203
  function isPriceStyle2(v) {
24599
25204
  return typeof v === "string" && priceStyleIds().includes(v);
24600
25205
  }