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