@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
@@ -1,6 +1,6 @@
1
- import { deserializeDrawing, chartTypes, rendererLayers, registerChartType, rendererDefaults, getDrawingType, TextLabel, Callout, resetDrawingSettings, registerNativeIndicator, chartType, getNativeIndicator, nativeIndicatorDescriptors, drawingTypes, formatDuration, RegressionChannel, FixedRangeVolumeProfile, SegmentDrawing, ArrowMark, GlyphStamp, RadialFib, FibSpiral, DedekindTessellation, MachFigure, GannSquare, FibRatios, MeasureBox, PositionTool, AnchoredVwap, PatternDrawing, Comment, PriceNote, Signpost, Note, PriceLabel, GANN_SQUARE_ARCS, lineSegmentIntersection, DEFAULT_DRAWING_COLOR, GLYPH_OPTIONS, STAMP_SIZE_OPTIONS, LINE_STYLE_OPTIONS, DEDEKIND_CURVATURE_OPTIONS, MACH_NUMBER_OPTIONS, MACH_WAVE_COUNT_OPTIONS, TEXT_SIZE_OPTIONS, createDrawing, tickerModifierIds, CalloutBase, DIRECTION_OPTIONS } from './chunk-7UFX5ZIG.js';
2
- import { themeTokens, STATIC_TOKENS } from './chunk-KM6LHB3Y.js';
3
- import { icon, iconAt, withAlpha, NEUTRAL, BEARISH, BULLISH, isDarkColor, toHex6, ACCENT, SERIES_LINE, SLATE, CROSSHAIR, TRADE_EXIT, TRADE_SHORT, TRADE_LONG, SLATE_DEEP, VALID, INVALID, svg24, WARNING, svg24Solid } from './chunk-GYD2THPV.js';
1
+ import { deserializeDrawing, chartTypes, rendererLayers, registerChartType, rendererDefaults, getDrawingType, settingsRowVisible, TextLabel, Callout, resetDrawingSettings, registerNativeIndicator, chartType, getNativeIndicator, nativeIndicatorDescriptors, drawingTypes, formatDuration, RegressionChannel, FixedRangeVolumeProfile, SegmentDrawing, ArrowMark, GlyphStamp, RadialFib, FibSpiral, DedekindTessellation, MachFigure, GannSquare, FibRatios, MeasureBox, PositionTool, AnchoredVwap, PatternDrawing, Comment, PriceNote, Signpost, Note, PriceLabel, GANN_SQUARE_ARCS, lineSegmentIntersection, DEFAULT_DRAWING_COLOR, GLYPH_OPTIONS, STAMP_SIZE_OPTIONS, LINE_STYLE_OPTIONS, DEDEKIND_CURVATURE_OPTIONS, MACH_NUMBER_OPTIONS, MACH_WAVE_COUNT_OPTIONS, TEXT_SIZE_OPTIONS, createDrawing, tickerModifierIds, CalloutBase, DIRECTION_OPTIONS } from './chunk-S6F6SQ2L.js';
2
+ import { themeTokens, STATIC_TOKENS } from './chunk-ULIX6AE2.js';
3
+ import { icon, iconAt, withAlpha, NEUTRAL, BEARISH, BULLISH, isDarkColor, toHex6, ACCENT, SERIES_LINE, SLATE, CROSSHAIR, TRADE_EXIT, TRADE_SHORT, TRADE_LONG, SLATE_DEEP, VALID, INVALID, svg24, WARNING, svg24Solid } from './chunk-EP6NDJYK.js';
4
4
 
5
5
  // src/core/events/EventBus.ts
6
6
  var TypedEventBus = class {
@@ -1300,6 +1300,14 @@ var DrawingsControl = class {
1300
1300
  if (this.ok("setToolShortcuts")) this.ctrl.setToolShortcuts(map);
1301
1301
  return this;
1302
1302
  }
1303
+ /** Display another chart's in-progress placement as a GHOST at reduced opacity
1304
+ * (`null` clears it) — how a multi-chart host mirrors `drawing:draft` onto linked
1305
+ * charts. Never a real drawing: no store entry, no selection, no persistence.
1306
+ * Silently inert on a renderer without the optional seam. */
1307
+ setExternalGhost(doc) {
1308
+ this.ctrl.setExternalGhost(doc);
1309
+ return this;
1310
+ }
1303
1311
  /** Create a drawing programmatically (no clicking). Returns it, or null if unsupported. */
1304
1312
  add(type, init = {}) {
1305
1313
  if (!this.ok("add")) return null;
@@ -1727,6 +1735,11 @@ var DrawingController = class {
1727
1735
  setToolbar(option) {
1728
1736
  this.port?.setToolbar(buildToolbar(option).definition);
1729
1737
  }
1738
+ /** Display (or clear) another chart's in-progress placement as a ghost — silently
1739
+ * inert on a renderer without the optional `setExternalGhost` seam. */
1740
+ setExternalGhost(doc) {
1741
+ this.port?.setExternalGhost?.(doc);
1742
+ }
1730
1743
  /** Push per-tool shortcut hints (pre-formatted display strings) to the toolbar flyouts. */
1731
1744
  setToolShortcuts(map) {
1732
1745
  this.port?.setToolShortcuts?.(map);
@@ -1957,6 +1970,9 @@ var DrawingController = class {
1957
1970
  case "arm":
1958
1971
  this.setTool(i.type);
1959
1972
  break;
1973
+ case "draft":
1974
+ this.events.emit("drawing:draft", { doc: i.doc });
1975
+ break;
1960
1976
  case "create": {
1961
1977
  const before = this.store.serialize();
1962
1978
  const last = this.lastStyle.get(i.doc.type);
@@ -2150,6 +2166,8 @@ var DrawingToolbar = class {
2150
2166
  this.eraserActive = false;
2151
2167
  this.stayBtn = null;
2152
2168
  this.stayActive = false;
2169
+ this.syncBtn = null;
2170
+ this.syncActive = false;
2153
2171
  this.collapseBtn = null;
2154
2172
  this.collapsed = false;
2155
2173
  this.visible = false;
@@ -2166,6 +2184,7 @@ var DrawingToolbar = class {
2166
2184
  this.dock = options.dock ?? "absolute";
2167
2185
  this.onCollapse = options.onCollapse ?? (() => {
2168
2186
  });
2187
+ this.onDrawingsSync = options.onDrawingsSync ?? null;
2169
2188
  ensureStyles();
2170
2189
  this.root = document.createElement("div");
2171
2190
  this.root.className = "vela-dtb";
@@ -2264,6 +2283,10 @@ var DrawingToolbar = class {
2264
2283
  this.root.appendChild(this.makeMagnetCell());
2265
2284
  this.stayBtn = this.makeButton(STAY_ICON, "Stay in drawing mode", () => this.toggleStay());
2266
2285
  this.root.appendChild(this.stayBtn);
2286
+ if (this.onDrawingsSync) {
2287
+ this.syncBtn = this.makeButton(SYNC_ICON, "Sync drawings on all charts", () => this.toggleDrawingsSync());
2288
+ this.root.appendChild(this.syncBtn);
2289
+ }
2267
2290
  this.collapseBtn = this.makeButton(COLLAPSE_ICON, "Collapse toolbar", () => this.toggleCollapsed());
2268
2291
  this.collapseBtn.classList.add("vela-dtb-collapse");
2269
2292
  this.root.appendChild(this.collapseBtn);
@@ -2271,6 +2294,7 @@ var DrawingToolbar = class {
2271
2294
  this.paintEraser();
2272
2295
  this.paintMagnet();
2273
2296
  this.paintStay();
2297
+ this.paintDrawingsSync();
2274
2298
  this.paintCollapse();
2275
2299
  this.highlight();
2276
2300
  }
@@ -2321,7 +2345,7 @@ var DrawingToolbar = class {
2321
2345
  cell.appendChild(arrow);
2322
2346
  }
2323
2347
  this.tipDisposers.push(
2324
- attachChromeTooltip(cell, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(cell) ?? label, placement: "right", delayMs: 2e3 })
2348
+ attachChromeTooltip(cell, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(cell) ?? label, placement: "right" })
2325
2349
  );
2326
2350
  return { cell, icon: icon2, arrow };
2327
2351
  }
@@ -2431,6 +2455,23 @@ var DrawingToolbar = class {
2431
2455
  b.dataset.active = this.stayActive ? "1" : "";
2432
2456
  b.style.opacity = this.stayActive ? "1" : "0.5";
2433
2457
  }
2458
+ /** Toggle drawings sync on/off and notify the host. */
2459
+ toggleDrawingsSync() {
2460
+ this.syncActive = !this.syncActive;
2461
+ this.onDrawingsSync?.(this.syncActive);
2462
+ this.paintDrawingsSync();
2463
+ }
2464
+ /** Reflect drawings sync externally (e.g. set through the API) without notifying back. */
2465
+ setDrawingsSyncMode(on) {
2466
+ this.syncActive = on;
2467
+ this.paintDrawingsSync();
2468
+ }
2469
+ paintDrawingsSync() {
2470
+ const b = this.syncBtn;
2471
+ if (!b) return;
2472
+ b.dataset.active = this.syncActive ? "1" : "";
2473
+ b.style.opacity = this.syncActive ? "1" : "0.5";
2474
+ }
2434
2475
  // ── flyout ──
2435
2476
  /** Create the positioned flyout panel anchored to a cell, register it as open, and start the
2436
2477
  * outside-dismiss. Callers fill it with items. */
@@ -2590,6 +2631,8 @@ var DrawingToolbar = class {
2590
2631
  const type = this.lastUsed.get(group.id) ?? group.tools[0]?.type;
2591
2632
  const tool = group.tools.find((t) => t.type === type) ?? group.tools[0];
2592
2633
  iconBtn.innerHTML = hitHtml(tool?.icon ?? "");
2634
+ const cell = this.groupCells.get(group.id);
2635
+ if (cell) this.tipText.set(cell, tool?.label ?? group.label);
2593
2636
  }
2594
2637
  /** Reconcile active tints from state (armed group / open flyout / cursor / modes). Active is a
2595
2638
  * `data-active` attribute the stylesheet paints — no inline background, so hover never sticks. */
@@ -2613,7 +2656,7 @@ var DrawingToolbar = class {
2613
2656
  this.tipText.set(btn, title);
2614
2657
  if (icon2) btn.innerHTML = hitHtml(icon2);
2615
2658
  this.tipDisposers.push(
2616
- attachChromeTooltip(btn, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(btn) ?? title, placement: "right", delayMs: 2e3 })
2659
+ attachChromeTooltip(btn, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(btn) ?? title, placement: "right" })
2617
2660
  );
2618
2661
  btn.addEventListener("click", onClick);
2619
2662
  return btn;
@@ -2679,6 +2722,7 @@ var COLLAPSE_ICON = icon("chevrons-left");
2679
2722
  var EXPAND_ICON = icon("chevrons-right");
2680
2723
  var MAGNET_ICON = icon("magnet");
2681
2724
  var STAY_ICON = icon("pen-lock");
2725
+ var SYNC_ICON = icon("pen-sync");
2682
2726
  var ERASER_ICON = icon("eraser");
2683
2727
  var ARROW_ICON = icon("chevron-right");
2684
2728
  var CHECK_ICON = icon("check");
@@ -4470,6 +4514,44 @@ function basePaintingOf(style) {
4470
4514
  for (const t of chartTypes()) if (t.id === style) return t.basePainting ?? "candles";
4471
4515
  return "candles";
4472
4516
  }
4517
+ function hasOwnCandlePaint(style) {
4518
+ if (BUILTIN_PRICE_STYLES.includes(style)) return false;
4519
+ for (const t of chartTypes()) if (t.id === style) return (t.basePainting ?? "candles") === "candles";
4520
+ return false;
4521
+ }
4522
+ function candleOverrideFor(style, bags) {
4523
+ if (!hasOwnCandlePaint(style)) return null;
4524
+ const bag = bags[style] ?? {};
4525
+ const color = (v) => isColor(v) ? v : null;
4526
+ const bool = (v) => isBool(v) ? v : null;
4527
+ return {
4528
+ upColor: color(bag.candleUpColor),
4529
+ downColor: color(bag.candleDownColor),
4530
+ bodyVisible: bool(bag.candleBodyVisible),
4531
+ borderVisible: bool(bag.candleBorderVisible),
4532
+ borderUpColor: color(bag.candleBorderUpColor),
4533
+ borderDownColor: color(bag.candleBorderDownColor),
4534
+ wickVisible: bool(bag.candleWickVisible),
4535
+ wickUpColor: color(bag.candleWickUpColor),
4536
+ wickDownColor: color(bag.candleWickDownColor)
4537
+ };
4538
+ }
4539
+ function effectiveCandlePaint(base, override, themeUp, themeDown) {
4540
+ if (!override) return { up: themeUp, down: themeDown, candle: base };
4541
+ return {
4542
+ up: override.upColor ?? themeUp,
4543
+ down: override.downColor ?? themeDown,
4544
+ candle: {
4545
+ bodyVisible: override.bodyVisible ?? base.bodyVisible,
4546
+ borderVisible: override.borderVisible ?? base.borderVisible,
4547
+ borderUpColor: override.borderUpColor ?? base.borderUpColor,
4548
+ borderDownColor: override.borderDownColor ?? base.borderDownColor,
4549
+ wickVisible: override.wickVisible ?? base.wickVisible,
4550
+ wickUpColor: override.wickUpColor ?? base.wickUpColor,
4551
+ wickDownColor: override.wickDownColor ?? base.wickDownColor
4552
+ }
4553
+ };
4554
+ }
4473
4555
  function priceStyleIds() {
4474
4556
  const out = [...BUILTIN_PRICE_STYLES];
4475
4557
  for (const t of chartTypes()) if (!out.includes(t.id)) out.push(t.id);
@@ -4508,6 +4590,40 @@ function clampLevel(v) {
4508
4590
  function clampSpacing(v) {
4509
4591
  return v < 0.1 ? 0.1 : v > 10 ? 10 : v;
4510
4592
  }
4593
+ function factoryResetConfig(factory) {
4594
+ const bag = {};
4595
+ for (const t of chartTypes()) {
4596
+ const section = t.settings;
4597
+ if (!section) continue;
4598
+ const defaults = {};
4599
+ const addRows = (rows) => {
4600
+ for (const r of rows ?? []) {
4601
+ if (r.kind === "heading" || r.kind === "header") continue;
4602
+ if (r.kind === "range") {
4603
+ defaults[r.minKey] = r.defval;
4604
+ defaults[r.maxKey] = r.defval;
4605
+ continue;
4606
+ }
4607
+ defaults[r.key] = r.defval;
4608
+ if (r.kind === "toggle") for (const c of r.colors ?? []) defaults[c.key] = c.defval;
4609
+ }
4610
+ };
4611
+ for (const inst of section.instances ?? []) {
4612
+ if (inst.enableKey) defaults[inst.enableKey] = false;
4613
+ addRows(inst.rows);
4614
+ }
4615
+ for (const sub of section.subsections ?? []) {
4616
+ if (sub.enableKey) defaults[sub.enableKey] = false;
4617
+ addRows(sub.rows);
4618
+ }
4619
+ if (!section.instances) addRows(section.rows);
4620
+ bag[t.id] = defaults;
4621
+ }
4622
+ for (const [typeId, vals] of Object.entries(factory.chartTypes)) {
4623
+ bag[typeId] = { ...bag[typeId] ?? {}, ...vals };
4624
+ }
4625
+ return { ...factory, chartTypes: bag };
4626
+ }
4511
4627
  function mergeConfig(base, patch) {
4512
4628
  const p = asObject(patch);
4513
4629
  const ctPatch = asObject(p.chartTypes);
@@ -4822,6 +4938,9 @@ var SceneGraph = class {
4822
4938
  this.priceStyle = "candles";
4823
4939
  /** Price-series base painting for the ACTIVE style (see ChartTypeDefinition.basePainting). */
4824
4940
  this.basePainting = "candles";
4941
+ /** The ACTIVE style's own candle cosmetics (`chartTypes.<id>.candle*`) when it is a
4942
+ * candle-based plugin type; null ⇒ paint with the shared `style.candle` block. */
4943
+ this.candleOverride = null;
4825
4944
  /** Explicit baseline reference price for `priceStyle:'baseline'`; when null the
4826
4945
  * baseline follows `style.baseline.baselineLevel` as a percent of the visible pane
4827
4946
  * range (resolved per frame via `baselinePriceFor`). */
@@ -6051,14 +6170,15 @@ var WebGL2Backend = class {
6051
6170
  emitCandles(b, scene, bars, i0, i1, coords, pane, theme, barColors) {
6052
6171
  const spacing = coords.bodySpacing();
6053
6172
  const tier = candleTier(spacing);
6173
+ const paint = effectiveCandlePaint(scene.style.candle, scene.candleOverride, theme.upColor, theme.downColor);
6054
6174
  if (tier === "aggregate") {
6055
- this.emitCandlesAggregated(b, bars, i0, i1, coords, pane, theme.upColor, theme.downColor, barColors);
6175
+ this.emitCandlesAggregated(b, bars, i0, i1, coords, pane, paint.up, paint.down, barColors);
6056
6176
  return;
6057
6177
  }
6058
6178
  const drawBody = tier === "full";
6059
- const up = theme.upColor;
6060
- const down = theme.downColor;
6061
- const cs = scene.style.candle;
6179
+ const up = paint.up;
6180
+ const down = paint.down;
6181
+ const cs = paint.candle;
6062
6182
  const fading = this.candleStructureAlpha > this.candleBodyAlpha + 1e-3;
6063
6183
  for (let i = i0; i <= i1; i += 1) {
6064
6184
  const bar = bars[i];
@@ -6650,7 +6770,6 @@ function effectiveSnapMode(momentaryOverride, sticky) {
6650
6770
  return momentaryOverride ? "strong" : sticky;
6651
6771
  }
6652
6772
  var InputController = class {
6653
- // the last middle press deleted a drawing (suppress autoscroll/paste)
6654
6773
  constructor(deps) {
6655
6774
  this.deps = deps;
6656
6775
  /** When true (the default), wheel-zoom pins the right edge instead of the bar
@@ -6678,9 +6797,23 @@ var InputController = class {
6678
6797
  this.vx = 0;
6679
6798
  // smoothed pointer velocity, px/ms
6680
6799
  this.middleDeleted = false;
6800
+ // the last middle press deleted a drawing (suppress autoscroll/paste)
6801
+ // Last cursor position over the element (NaN once it leaves) — lets a modifier
6802
+ // press/release re-shape the drawings preview with the pointer stationary.
6803
+ this.cursorX = NaN;
6804
+ this.cursorY = NaN;
6681
6805
  this.onMiddleGuard = (e) => {
6682
6806
  if (e.button === 1 && this.middleDeleted) e.preventDefault();
6683
6807
  };
6808
+ /** A modifier press/release with the pointer stationary: re-issue the last cursor
6809
+ * position so the placing ghost / drag preview reflects the new state immediately
6810
+ * (Shift's 45° angle lock, Ctrl/Cmd's momentary strong magnet). */
6811
+ this.onModifier = (e) => {
6812
+ if (e.repeat || e.key !== "Shift" && e.key !== "Control" && e.key !== "Meta") return;
6813
+ if (Number.isNaN(this.cursorX)) return;
6814
+ if (this.dragging && this.region !== "drawing") return;
6815
+ this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey);
6816
+ };
6684
6817
  this.onDown = (e) => {
6685
6818
  if (e.button === 1) {
6686
6819
  const { x: x2, y: y2 } = this.local(e);
@@ -6722,6 +6855,8 @@ var InputController = class {
6722
6855
  };
6723
6856
  this.onMove = (e) => {
6724
6857
  const { x, y } = this.local(e);
6858
+ this.cursorX = x;
6859
+ this.cursorY = y;
6725
6860
  if (this.dragging) {
6726
6861
  if (this.region === "price") {
6727
6862
  if (Math.abs(y - this.startY) > DRAG_SLOP) this.moved = true;
@@ -6735,7 +6870,7 @@ var InputController = class {
6735
6870
  this.deps.apply({ barSpacing, rightOffset: this.startRightOffset });
6736
6871
  } else if (this.region === "drawing") {
6737
6872
  if (Math.abs(x - this.startX) > DRAG_SLOP || Math.abs(y - this.startY) > DRAG_SLOP) this.moved = true;
6738
- this.deps.drawingsPointerMove?.(x, y, this.snapMode(e));
6873
+ this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
6739
6874
  } else {
6740
6875
  const coords = this.deps.getCoords();
6741
6876
  const vp = coords.getViewport();
@@ -6758,7 +6893,7 @@ var InputController = class {
6758
6893
  const drawCursor = this.deps.drawingsCursor?.(x, y);
6759
6894
  const r = this.regionAt(x, y);
6760
6895
  this.el.style.cursor = drawCursor ?? (r === "price" ? "ns-resize" : r === "time" ? "ew-resize" : r === "separator" ? "row-resize" : "");
6761
- this.deps.drawingsPointerMove?.(x, y, this.snapMode(e));
6896
+ this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
6762
6897
  }
6763
6898
  this.deps.onPointerMove(x, y);
6764
6899
  };
@@ -6783,6 +6918,8 @@ var InputController = class {
6783
6918
  }
6784
6919
  };
6785
6920
  this.onLeave = () => {
6921
+ this.cursorX = NaN;
6922
+ this.cursorY = NaN;
6786
6923
  this.deps.onPointerMove(null, null);
6787
6924
  };
6788
6925
  this.onDblClick = (e) => {
@@ -6821,10 +6958,16 @@ var InputController = class {
6821
6958
  el.addEventListener("wheel", this.onWheel, { passive: false });
6822
6959
  el.addEventListener("mousedown", this.onMiddleGuard);
6823
6960
  el.addEventListener("auxclick", this.onMiddleGuard);
6961
+ const win = el.ownerDocument?.defaultView;
6962
+ win?.addEventListener("keydown", this.onModifier);
6963
+ win?.addEventListener("keyup", this.onModifier);
6824
6964
  }
6825
6965
  detach() {
6826
6966
  const el = this.el;
6827
6967
  if (!el) return;
6968
+ const win = el.ownerDocument?.defaultView;
6969
+ win?.removeEventListener("keydown", this.onModifier);
6970
+ win?.removeEventListener("keyup", this.onModifier);
6828
6971
  el.removeEventListener("pointerdown", this.onDown);
6829
6972
  el.removeEventListener("pointermove", this.onMove);
6830
6973
  el.removeEventListener("pointerup", this.onUp);
@@ -7242,12 +7385,13 @@ var Canvas2dBackend = class {
7242
7385
  drawCandles(ctx, scene, bars, i0, i1, coords, pane, theme, barColors) {
7243
7386
  const spacing = coords.bodySpacing();
7244
7387
  const tier = candleTier(spacing);
7388
+ const paint = effectiveCandlePaint(scene.style.candle, scene.candleOverride, theme.upColor, theme.downColor);
7245
7389
  if (tier === "aggregate") {
7246
- this.drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, theme.upColor, theme.downColor, barColors);
7390
+ this.drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, paint.up, paint.down, barColors);
7247
7391
  return;
7248
7392
  }
7249
7393
  const drawBody = tier === "full";
7250
- const cs = scene.style.candle;
7394
+ const cs = paint.candle;
7251
7395
  const fading = this.candleStructureAlpha > this.candleBodyAlpha + 1e-3;
7252
7396
  for (let i = i0; i <= i1; i += 1) {
7253
7397
  const b = bars[i];
@@ -7255,7 +7399,7 @@ var Canvas2dBackend = class {
7255
7399
  const g = candleGeometry(coords.logicalToX(i), spacing, coords.dpr, this.candleBodyScale);
7256
7400
  const x = g.center;
7257
7401
  const up = b.close >= b.open;
7258
- const dir = up ? theme.upColor : theme.downColor;
7402
+ const dir = up ? paint.up : paint.down;
7259
7403
  const bc = barColors.get(b.time);
7260
7404
  const color = bc ?? dir;
7261
7405
  let top = 0;
@@ -8366,7 +8510,7 @@ var ChromeRenderer = class {
8366
8510
  constructor() {
8367
8511
  this.canvas = null;
8368
8512
  this.ctx = null;
8369
- // The color for axis tick labels — the stable chrome surface text, set each frame in render().
8513
+ // The color for axis tick labels — the host-passed surface text, set each frame in render().
8370
8514
  this.axisTextColor = DARK_THEME.textColor;
8371
8515
  // Shared Pine-drawing renderer (line/box/label/polyline/linefill); widthCache persists.
8372
8516
  this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
@@ -8398,8 +8542,8 @@ var ChromeRenderer = class {
8398
8542
  return dr;
8399
8543
  }
8400
8544
  /** Clear the chrome canvas and draw drawings + axes + current-price line.
8401
- * `surface` (background + text) paints the axis-scale gutters so they keep the app
8402
- * chrome color and stay readable, rather than tracking the user-editable plot cosmetics.
8545
+ * `surface` (background + text) paints the axis-scale gutters the host passes the live
8546
+ * chart background so the scales read as part of the plot, with contrast-corrected text.
8403
8547
  * Falls back to the theme's own colors when no surface is supplied. */
8404
8548
  render(scene, coords, theme, surface) {
8405
8549
  const ctx = this.ctx;
@@ -9166,6 +9310,37 @@ function ensureControlStyles() {
9166
9310
  .vela-sd-btn:hover{background:var(--vela-hover);border-color:var(--vela-border-strong);color:var(--vela-fg-bright);}
9167
9311
  .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;}
9168
9312
  .vela-sd-close:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
9313
+ /* Rows/blocks gated away by chart-type conditions, TOC filters, or the instance strip.
9314
+ !important: the pane grid rewrites inline display ('contents') AFTER the initial
9315
+ visibility pass, so a class must win. */
9316
+ .vela-sd-hide{display:none !important;}
9317
+ /* Indented rail sub-entry (a chart-type section's subsection tab). */
9318
+ .vela-sd-tab-sub{padding-left:28px;font-weight:600;font-size:12px;}
9319
+ /* Instance strip: a tab per present instance (label, \u2715 on the active removable one)
9320
+ and a dashed + that turns on the next absent instance. The rule under it separates
9321
+ the strip from the instance's TOC + rows area. */
9322
+ .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);}
9323
+ .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;}
9324
+ .vela-sd-itab:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
9325
+ .vela-sd-itab.on{background:var(--vela-active);color:var(--vela-fg-bright);border-color:var(--vela-border-strong);}
9326
+ .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;}
9327
+ .vela-sd-ix:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
9328
+ .vela-sd-itab-add{border-style:dashed;min-width:30px;justify-content:center;padding:0;}
9329
+ /* Structured pane: group TOC column + rows column, TOP-ALIGNED (the shared padding-top
9330
+ lives on the wrap, never on one column). The TOC sticks while the pane scrolls; the
9331
+ vertical rule sits on the rows column so it spans the full content height. When every
9332
+ group gates out the TOC hides and .no-toc drops the rule with it. */
9333
+ .vela-sd-struct{display:flex;align-items:flex-start;padding-top:14px;}
9334
+ .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;}
9335
+ .vela-sd-struct>[data-sd-rows-host]{border-left:1px solid var(--vela-border);padding-left:18px;}
9336
+ .vela-sd-struct.no-toc>[data-sd-rows-host]{border-left:none;padding-left:0;}
9337
+ .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;}
9338
+ .vela-sd-toc-btn:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
9339
+ .vela-sd-toc-btn.on{background:var(--vela-active);color:var(--vela-fg-bright);}
9340
+ /* Soft-disable: a subsection's enableKey is off \u2014 rows stay visible (browseable) but
9341
+ muted and non-interactive. Applied to each row's children so it survives display:contents;
9342
+ !important beats the inline opacity on labels. */
9343
+ .vela-sd-soft>*{opacity:0.4 !important;pointer-events:none !important;}
9169
9344
  `;
9170
9345
  document.head.appendChild(st);
9171
9346
  }
@@ -9186,6 +9361,10 @@ var SettingsDialog = class {
9186
9361
  this.themeControl = null;
9187
9362
  /** The built tabs, by title — how `showSection` reaches a pane while the dialog is open. */
9188
9363
  this.tabs = [];
9364
+ /** Active instance-strip tab per chart type — remembered across dialog rebuilds. */
9365
+ this.typeActiveInstance = /* @__PURE__ */ new Map();
9366
+ /** Active TOC group per structured pane (`<typeId>/<pane>` → group label). */
9367
+ this.typeActiveGroup = /* @__PURE__ */ new Map();
9189
9368
  /** The tab currently shown, so a theme change (which rebuilds) lands back on it. */
9190
9369
  this.activeSection = null;
9191
9370
  if (getComputedStyle(container).position === "static") container.style.position = "relative";
@@ -9347,6 +9526,29 @@ var SettingsDialog = class {
9347
9526
  baseline.append(this.numberRow("Width", config.baseline.width, 1, 10, 1, (v) => this.emit({ baseline: { width: v } })));
9348
9527
  groups.baseline = baseline;
9349
9528
  body.append(baseline);
9529
+ for (const def of chartTypes()) {
9530
+ if (!hasOwnCandlePaint(def.id)) continue;
9531
+ const bag = config.chartTypes[def.id] ?? {};
9532
+ const colorOf2 = (key, shared) => typeof bag[key] === "string" && bag[key] !== "" ? bag[key] : shared;
9533
+ const boolOf = (key, shared) => typeof bag[key] === "boolean" ? bag[key] : shared;
9534
+ const g = this.group();
9535
+ g.append(this.sectionTitle("Candles"));
9536
+ g.append(this.toggleRow("Body", boolOf("candleBodyVisible", config.candles.bodyVisible), (v) => this.emitType(def.id, "candleBodyVisible", v), [
9537
+ this.swatch(colorOf2("candleUpColor", config.candles.upColor), (v) => this.emitType(def.id, "candleUpColor", v)),
9538
+ this.swatch(colorOf2("candleDownColor", config.candles.downColor), (v) => this.emitType(def.id, "candleDownColor", v))
9539
+ ]));
9540
+ g.append(this.toggleRow("Borders", boolOf("candleBorderVisible", config.candles.borderVisible), (v) => this.emitType(def.id, "candleBorderVisible", v), [
9541
+ this.swatch(colorOf2("candleBorderUpColor", config.candles.borderUpColor), (v) => this.emitType(def.id, "candleBorderUpColor", v)),
9542
+ this.swatch(colorOf2("candleBorderDownColor", config.candles.borderDownColor), (v) => this.emitType(def.id, "candleBorderDownColor", v))
9543
+ ]));
9544
+ g.append(this.toggleRow("Wick", boolOf("candleWickVisible", config.candles.wickVisible), (v) => this.emitType(def.id, "candleWickVisible", v), [
9545
+ this.swatch(colorOf2("candleWickUpColor", config.candles.wickUpColor), (v) => this.emitType(def.id, "candleWickUpColor", v)),
9546
+ this.swatch(colorOf2("candleWickDownColor", config.candles.wickDownColor), (v) => this.emitType(def.id, "candleWickDownColor", v))
9547
+ ]));
9548
+ g.append(this.numberRow("Spacing", config.series.spacing, 0.1, 10, 0.1, (v) => this.emit({ series: { spacing: v } })));
9549
+ groups[def.id] = g;
9550
+ body.append(g);
9551
+ }
9350
9552
  showActive(config.series.style);
9351
9553
  body.append(this.sectionTitle("Time zone"));
9352
9554
  body.append(this.selectRowLabeled("Time zone", normalizeTimezone(config.timeScale.timezone), timezoneOptions(config.timeScale.timezone), (v) => this.emit({ timeScale: { timezone: v } })));
@@ -9361,7 +9563,16 @@ var SettingsDialog = class {
9361
9563
  }
9362
9564
  }
9363
9565
  };
9566
+ const renderChartTypeSections = (placement) => {
9567
+ for (const def of chartTypes()) {
9568
+ const typeSettings = def.settings;
9569
+ if (!typeSettings) continue;
9570
+ if ((typeSettings.placement ?? "end") !== placement) continue;
9571
+ this.chartTypeSection(def.id, typeSettings, config, body);
9572
+ }
9573
+ };
9364
9574
  renderHostSections("symbol");
9575
+ renderChartTypeSections("after-symbol");
9365
9576
  renderHostSections("after-symbol");
9366
9577
  body.append(this.section("Scales and lines"));
9367
9578
  body.append(this.sectionTitle("Price scale"));
@@ -9402,27 +9613,7 @@ var SettingsDialog = class {
9402
9613
  body.append(this.sectionTitle("Theme"));
9403
9614
  body.append(this.selectRow("Color theme", tc.current === "dark" ? "Dark" : "Light", ["Dark", "Light"], (v) => tc.onSelect(v === "Dark" ? "dark" : "light")));
9404
9615
  }
9405
- for (const def of chartTypes()) {
9406
- const typeSettings = def.settings;
9407
- if (!typeSettings) continue;
9408
- const marker = this.section(typeSettings.title);
9409
- marker.dataset.sdStyle = def.id;
9410
- marker.dataset.sdVisibility = typeSettings.visibility ?? "active";
9411
- body.append(marker);
9412
- const values = config.chartTypes[def.id] ?? {};
9413
- for (const r of typeSettings.rows) {
9414
- const current2 = values[r.key];
9415
- if (r.kind === "toggle") {
9416
- body.append(this.boolRow(r.label, typeof current2 === "boolean" ? current2 : r.defval, (v) => this.emitType(def.id, r.key, v)));
9417
- } else if (r.kind === "number") {
9418
- 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)));
9419
- } else if (r.kind === "color") {
9420
- body.append(this.colorRow(r.label, typeof current2 === "string" ? current2 : r.defval, (v) => this.emitType(def.id, r.key, v)));
9421
- } else {
9422
- body.append(this.selectRow(r.label, typeof current2 === "string" ? current2 : r.defval, [...r.options], (v) => this.emitType(def.id, r.key, v)));
9423
- }
9424
- }
9425
- }
9616
+ renderChartTypeSections("end");
9426
9617
  renderHostSections("end");
9427
9618
  const shell = document.createElement("div");
9428
9619
  shell.style.cssText = "display:flex;min-height:360px;max-height:calc(70vh - 100px);flex:1 1 auto;";
@@ -9441,7 +9632,7 @@ var SettingsDialog = class {
9441
9632
  const tab = document.createElement("button");
9442
9633
  tab.type = "button";
9443
9634
  tab.textContent = title;
9444
- tab.className = "vela-sd-tab";
9635
+ tab.className = "vela-sd-tab" + (child.dataset.sdSub !== void 0 ? " vela-sd-tab-sub" : "");
9445
9636
  panes.push({ title, el, tab, style: child.dataset.sdStyle, visibility: child.dataset.sdVisibility });
9446
9637
  current = el;
9447
9638
  child.remove();
@@ -9481,7 +9672,14 @@ var SettingsDialog = class {
9481
9672
  scrim.appendChild(dlg);
9482
9673
  this.container.appendChild(scrim);
9483
9674
  this.root = scrim;
9484
- for (const p of panes) this.layoutSettingsGrids(p.el, dlg);
9675
+ for (const p of panes) {
9676
+ const hosts = [...p.el.querySelectorAll("[data-sd-rows-host]")];
9677
+ if (hosts.length === 0) {
9678
+ this.layoutSettingsGrids(p.el, dlg);
9679
+ continue;
9680
+ }
9681
+ for (const h of hosts) this.layoutSettingsGrids(h, dlg);
9682
+ }
9485
9683
  }
9486
9684
  close() {
9487
9685
  closeColorPopover();
@@ -9511,6 +9709,291 @@ var SettingsDialog = class {
9511
9709
  el.textContent = text;
9512
9710
  return el;
9513
9711
  }
9712
+ /**
9713
+ * One chart type's settings tab (and its subsection tabs), appended to the linear
9714
+ * `body` for the pane splitter to file. One live values BAG serves the whole section
9715
+ * — instances and subsections share the per-type store, so a `when` gate anywhere
9716
+ * can reference a key edited anywhere else; every edit runs every registered
9717
+ * refresher.
9718
+ */
9719
+ chartTypeSection(typeId, section, config, body) {
9720
+ const marker = this.section(section.title);
9721
+ marker.dataset.sdStyle = typeId;
9722
+ marker.dataset.sdVisibility = section.visibility ?? "active";
9723
+ body.append(marker);
9724
+ const values = config.chartTypes[typeId] ?? {};
9725
+ const bag = {};
9726
+ const seedKey = (key, want, defval) => {
9727
+ const v = values[key];
9728
+ bag[key] = typeof v === want ? v : defval;
9729
+ };
9730
+ const seed = (rows) => {
9731
+ for (const r of rows) {
9732
+ if (r.kind === "heading" || r.kind === "header") continue;
9733
+ if (r.kind === "range") {
9734
+ seedKey(r.minKey, "number", r.defval);
9735
+ seedKey(r.maxKey, "number", r.defval);
9736
+ continue;
9737
+ }
9738
+ seedKey(r.key, r.kind === "toggle" ? "boolean" : r.kind === "number" ? "number" : "string", r.defval);
9739
+ if (r.kind === "toggle") for (const c of r.colors ?? []) seedKey(c.key, "string", c.defval);
9740
+ }
9741
+ };
9742
+ if (section.rows) seed(section.rows);
9743
+ for (const inst of section.instances ?? []) seed(inst.rows);
9744
+ for (const sub of section.subsections ?? []) seed(sub.rows);
9745
+ for (const inst of section.instances ?? []) {
9746
+ if (!inst.enableKey || inst.enableKey in bag) continue;
9747
+ bag[inst.enableKey] = values[inst.enableKey] === true;
9748
+ }
9749
+ for (const sub of section.subsections ?? []) {
9750
+ if (!sub.enableKey || sub.enableKey in bag) continue;
9751
+ bag[sub.enableKey] = values[sub.enableKey] === true;
9752
+ }
9753
+ const refreshers = [];
9754
+ const put = (key, v) => {
9755
+ bag[key] = v;
9756
+ this.emitType(typeId, key, v);
9757
+ for (const r of refreshers) r();
9758
+ };
9759
+ if (section.instances && section.instances.length > 0) {
9760
+ body.append(this.instancesBlock(typeId, section.instances, bag, put, refreshers));
9761
+ } else if (section.rows) {
9762
+ this.flatTypeRows(section.rows, bag, put, refreshers, body);
9763
+ }
9764
+ for (const sub of section.subsections ?? []) {
9765
+ const subMarker = this.section(sub.title);
9766
+ subMarker.dataset.sdStyle = typeId;
9767
+ subMarker.dataset.sdVisibility = section.visibility ?? "active";
9768
+ subMarker.dataset.sdSub = "1";
9769
+ body.append(subMarker);
9770
+ body.append(this.groupedRows(`${typeId}/${sub.title}`, sub.rows, bag, put, refreshers, sub.enableKey));
9771
+ }
9772
+ for (const r of refreshers) r();
9773
+ }
9774
+ /** The FLAT chart-type form: rows appended straight to the body (the pane grid wraps
9775
+ * them), headings/headers as inline group titles, `when` gates refreshed live. */
9776
+ flatTypeRows(rows, bag, put, refreshers, body) {
9777
+ const entries = [];
9778
+ for (const r of rows) {
9779
+ const el = r.kind === "heading" || r.kind === "header" ? this.sectionTitle(r.label) : this.typeRow(r, bag, put);
9780
+ entries.push({ el, when: r.when });
9781
+ body.append(el);
9782
+ }
9783
+ refreshers.push(() => {
9784
+ for (const e of entries) e.el.classList.toggle("vela-sd-hide", !settingsRowVisible(e.when, bag));
9785
+ });
9786
+ }
9787
+ /** One value row for a chart-type descriptor, writing through `put`. */
9788
+ typeRow(r, bag, put) {
9789
+ if (r.kind === "toggle") {
9790
+ const swatches = (r.colors ?? []).map((c) => {
9791
+ const sw = this.swatch(bag[c.key], (v) => put(c.key, v));
9792
+ sw.title = c.label;
9793
+ return sw;
9794
+ });
9795
+ return this.toggleRow(r.label, bag[r.key], (v) => put(r.key, v), swatches);
9796
+ }
9797
+ 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));
9798
+ if (r.kind === "color") return this.colorRow(r.label, bag[r.key], (v) => put(r.key, v));
9799
+ if (r.kind === "range") return this.rangeRow(r, bag, put);
9800
+ return this.selectRowLabeled(r.label, bag[r.key], normalizeSelectOptions(r.options), (v) => put(r.key, v));
9801
+ }
9802
+ /**
9803
+ * A MIN–MAX row: two number inputs on one row (stored under the descriptor's
9804
+ * `minKey`/`maxKey`). With a `placeholder`, an input at the DEFAULT value renders
9805
+ * empty showing it, and clearing an input stores the default back — the
9806
+ * placeholder names the unset state ('Off' for 0-disables bounds).
9807
+ */
9808
+ rangeRow(r, bag, put) {
9809
+ const { wrap } = this.row(r.label);
9810
+ const input = (key, title) => {
9811
+ const ni = document.createElement("input");
9812
+ ni.type = "number";
9813
+ ni.className = "vela-sd-number";
9814
+ ni.min = String(r.min ?? 0);
9815
+ ni.max = String(r.max ?? 1e6);
9816
+ ni.step = String(r.step ?? 1);
9817
+ ni.title = title;
9818
+ const current = bag[key];
9819
+ if (r.placeholder !== void 0) {
9820
+ ni.placeholder = r.placeholder;
9821
+ if (current !== r.defval) ni.value = String(current);
9822
+ } else {
9823
+ ni.value = String(current);
9824
+ }
9825
+ ni.addEventListener("change", () => {
9826
+ const raw = ni.value.trim() === "" ? r.defval : Number(ni.value);
9827
+ const v = Number.isFinite(raw) ? Math.min(r.max ?? Infinity, Math.max(r.min ?? -Infinity, raw)) : r.defval;
9828
+ ni.value = r.placeholder !== void 0 && v === r.defval ? "" : String(v);
9829
+ put(key, v);
9830
+ });
9831
+ return ni;
9832
+ };
9833
+ const box = document.createElement("div");
9834
+ box.style.cssText = "display:flex;align-items:center;gap:6px;flex:0 0 auto;";
9835
+ const dash = document.createElement("span");
9836
+ dash.textContent = "\u2013";
9837
+ dash.style.cssText = "color:var(--vela-fg-muted);";
9838
+ box.append(input(r.minKey, `${r.label} \u2014 min`), dash, input(r.maxKey, `${r.label} \u2014 max`));
9839
+ wrap.appendChild(box);
9840
+ return wrap;
9841
+ }
9842
+ /**
9843
+ * The INSTANCE STRIP block: a tab per present instance (label, `×` on the active
9844
+ * removable one), a dashed `+` while an instance is still absent, and one
9845
+ * grouped-rows content per instance below — only the active tab's content shows.
9846
+ * Presence is the boolean at each instance's `enableKey`; the strip rebuilds on
9847
+ * every section edit, so gates elsewhere stay coherent.
9848
+ */
9849
+ instancesBlock(typeId, instances, bag, put, refreshers) {
9850
+ const wrap = document.createElement("div");
9851
+ const strip = document.createElement("div");
9852
+ strip.className = "vela-sd-itabs";
9853
+ wrap.append(strip);
9854
+ const contents = instances.map((inst, i) => {
9855
+ const content = this.groupedRows(`${typeId}/#${i}`, inst.rows, bag, put, refreshers);
9856
+ wrap.append(content);
9857
+ return content;
9858
+ });
9859
+ const present = (inst) => !inst.enableKey || bag[inst.enableKey] === true;
9860
+ const refresh = () => {
9861
+ let active = this.typeActiveInstance.get(typeId) ?? 0;
9862
+ if (!present(instances[active] ?? instances[0])) active = 0;
9863
+ this.typeActiveInstance.set(typeId, active);
9864
+ strip.replaceChildren();
9865
+ instances.forEach((inst, i) => {
9866
+ if (!present(inst)) return;
9867
+ const tab = document.createElement("button");
9868
+ tab.type = "button";
9869
+ tab.className = "vela-sd-itab" + (i === active ? " on" : "");
9870
+ const lbl = document.createElement("span");
9871
+ lbl.textContent = inst.label;
9872
+ tab.append(lbl);
9873
+ if (i === active && inst.enableKey) {
9874
+ const x = document.createElement("span");
9875
+ x.className = "vela-sd-ix";
9876
+ x.textContent = "\u2715";
9877
+ x.title = `Remove ${inst.label}`;
9878
+ x.addEventListener("click", (e) => {
9879
+ e.stopPropagation();
9880
+ this.typeActiveInstance.set(typeId, 0);
9881
+ put(inst.enableKey, false);
9882
+ });
9883
+ tab.append(x);
9884
+ }
9885
+ tab.addEventListener("click", () => {
9886
+ this.typeActiveInstance.set(typeId, i);
9887
+ refresh();
9888
+ });
9889
+ strip.append(tab);
9890
+ });
9891
+ const absent = instances.findIndex((inst) => !present(inst));
9892
+ if (absent >= 0) {
9893
+ const add = document.createElement("button");
9894
+ add.type = "button";
9895
+ add.className = "vela-sd-itab vela-sd-itab-add";
9896
+ add.textContent = "+";
9897
+ add.title = `Add ${instances[absent].label}`;
9898
+ add.addEventListener("click", () => {
9899
+ this.typeActiveInstance.set(typeId, absent);
9900
+ put(instances[absent].enableKey, true);
9901
+ });
9902
+ strip.append(add);
9903
+ }
9904
+ contents.forEach((c, i) => c.classList.toggle("vela-sd-hide", i !== active));
9905
+ };
9906
+ refreshers.push(refresh);
9907
+ return wrap;
9908
+ }
9909
+ /**
9910
+ * A GROUPED rows pane: a TOC column of the group labels (from `heading` rows) and
9911
+ * ONE rows host holding every row — the active group's rows show, the rest hide.
9912
+ * Rows BEFORE the first heading are the always block, visible above every group.
9913
+ * `header` rows stay in the rows column as in-group subgroup titles (not TOC
9914
+ * entries). A group whose value rows are all gated out (or whose heading's own
9915
+ * `when` fails) leaves the TOC; the whole TOC hides when no group is live. The
9916
+ * rows host is tagged for `layoutSettingsGrids`, keeping one shared label column
9917
+ * across all groups.
9918
+ *
9919
+ * `enableKey` (optional): while that bag boolean is false, every row except the one
9920
+ * whose key matches is soft-disabled (visible but grayed / non-interactive) so the
9921
+ * pane stays browseable with the feature off.
9922
+ */
9923
+ groupedRows(paneKey, rows, bag, put, refreshers, enableKey) {
9924
+ const wrap = document.createElement("div");
9925
+ wrap.className = "vela-sd-struct";
9926
+ const toc = document.createElement("div");
9927
+ toc.className = "vela-sd-toc";
9928
+ const rowsHost = document.createElement("div");
9929
+ rowsHost.dataset.sdRowsHost = "1";
9930
+ rowsHost.style.cssText = "flex:1 1 auto;min-width:0;";
9931
+ wrap.append(toc, rowsHost);
9932
+ const entries = [];
9933
+ const groups = [];
9934
+ const groupWhens = [];
9935
+ let g = -1;
9936
+ for (const r of rows) {
9937
+ if (r.kind === "heading") {
9938
+ g = groups.length;
9939
+ groups.push(r.label);
9940
+ groupWhens.push(r.when);
9941
+ continue;
9942
+ }
9943
+ if (r.kind === "header") {
9944
+ entries.push({ el: this.sectionTitle(r.label), when: r.when, group: g, header: true });
9945
+ rowsHost.append(entries[entries.length - 1].el);
9946
+ continue;
9947
+ }
9948
+ const el = this.typeRow(r, bag, put);
9949
+ const key = r.kind === "range" ? void 0 : r.key;
9950
+ entries.push({ el, when: r.when, group: g, key });
9951
+ rowsHost.append(el);
9952
+ }
9953
+ const refresh = () => {
9954
+ const live = groups.map((_, gi) => settingsRowVisible(groupWhens[gi], bag) && entries.some((e) => e.group === gi && !e.header && settingsRowVisible(e.when, bag)));
9955
+ let activeIdx = groups.indexOf(this.typeActiveGroup.get(paneKey) ?? "");
9956
+ if (activeIdx < 0 || !live[activeIdx]) activeIdx = live.findIndex(Boolean);
9957
+ if (activeIdx >= 0) this.typeActiveGroup.set(paneKey, groups[activeIdx]);
9958
+ toc.replaceChildren();
9959
+ groups.forEach((label, gi) => {
9960
+ if (!live[gi]) return;
9961
+ const b = document.createElement("button");
9962
+ b.type = "button";
9963
+ b.className = "vela-sd-toc-btn" + (gi === activeIdx ? " on" : "");
9964
+ b.textContent = label;
9965
+ b.addEventListener("click", () => {
9966
+ this.typeActiveGroup.set(paneKey, label);
9967
+ refresh();
9968
+ });
9969
+ toc.append(b);
9970
+ });
9971
+ const anyGroup = live.some(Boolean);
9972
+ toc.classList.toggle("vela-sd-hide", !anyGroup);
9973
+ wrap.classList.toggle("no-toc", !anyGroup);
9974
+ const enabled = !enableKey || bag[enableKey] === true;
9975
+ for (let i = 0; i < entries.length; i++) {
9976
+ const e = entries[i];
9977
+ let visible = (e.group === -1 || e.group === activeIdx) && settingsRowVisible(e.when, bag);
9978
+ if (visible && e.header) {
9979
+ let hasContent = false;
9980
+ for (let j = i + 1; j < entries.length; j++) {
9981
+ const n = entries[j];
9982
+ if (n.group !== e.group || n.header) break;
9983
+ if (settingsRowVisible(n.when, bag)) {
9984
+ hasContent = true;
9985
+ break;
9986
+ }
9987
+ }
9988
+ visible = hasContent;
9989
+ }
9990
+ e.el.classList.toggle("vela-sd-hide", !visible);
9991
+ e.el.classList.toggle("vela-sd-soft", visible && !enabled && e.key !== enableKey);
9992
+ }
9993
+ };
9994
+ refreshers.push(refresh);
9995
+ return wrap;
9996
+ }
9514
9997
  /** Vertical breathing space between row clusters — grouping reads from whitespace,
9515
9998
  * so no drawn rule. */
9516
9999
  separator() {
@@ -9773,7 +10256,7 @@ var SettingsDialog = class {
9773
10256
  const { wrap } = this.row(label);
9774
10257
  const sel = document.createElement("select");
9775
10258
  sel.className = "vela-sd-select";
9776
- sel.style.cssText = "max-width:140px;flex:0 0 auto;";
10259
+ sel.style.cssText = "max-width:200px;flex:0 0 auto;";
9777
10260
  for (const [val, lbl] of options) {
9778
10261
  const o = document.createElement("option");
9779
10262
  o.value = val;
@@ -9789,7 +10272,7 @@ var SettingsDialog = class {
9789
10272
  const { wrap } = this.row(label);
9790
10273
  const sel = document.createElement("select");
9791
10274
  sel.className = "vela-sd-select";
9792
- sel.style.cssText = "max-width:140px;flex:0 0 auto;";
10275
+ sel.style.cssText = "max-width:200px;flex:0 0 auto;";
9793
10276
  for (const opt of options) {
9794
10277
  const o = document.createElement("option");
9795
10278
  o.value = opt;
@@ -9826,6 +10309,9 @@ var SettingsDialog = class {
9826
10309
  }
9827
10310
  };
9828
10311
  var AUTO_MANUAL_OPTS = [["auto", "Auto"], ["manual", "Manual"]];
10312
+ function normalizeSelectOptions(options) {
10313
+ return options.map((o) => typeof o === "string" ? [o, o] : o);
10314
+ }
9829
10315
  function timezoneOptions(current) {
9830
10316
  const options = TIMEZONES.map((t) => [t.value, tzMenuLabel(t.value, t.label)]);
9831
10317
  const normalized = normalizeTimezone(current);
@@ -11184,6 +11670,8 @@ function topDrawingAt(drawings, x, y, proj, tol = HIT_TOLERANCE) {
11184
11670
  // src/renderers/native/drawings/DrawingInteraction.ts
11185
11671
  var DRAG_SLOP2 = 3;
11186
11672
  var FREEHAND_SAMPLE_PX = 4;
11673
+ var ANGLE_SNAP_STEP = Math.PI / 4;
11674
+ var ANGLE_SNAP_TYPES = /* @__PURE__ */ new Set(["trendline", "ray", "extendedline", "infoline", "trendangle", "arrow"]);
11187
11675
  var DrawingInteraction = class {
11188
11676
  // magnet target for the ring
11189
11677
  constructor(deps) {
@@ -11224,6 +11712,27 @@ var DrawingInteraction = class {
11224
11712
  this.snapAt = changed ? { point: snapped, paneId } : null;
11225
11713
  return snapped;
11226
11714
  }
11715
+ /** Resolve a pixel to a data point with the segment angle locked to 45° steps around
11716
+ * `pivot` (Shift held on a line tool). Works in PIXEL space — the user reasons about
11717
+ * the angle they see, not about time/price units. The magnet is bypassed: snapping
11718
+ * the result to a candle afterwards would break the exact angle just enforced. */
11719
+ resolveAngleSnapped(pivot, paneId, x, y) {
11720
+ const proj = this.deps.projector();
11721
+ const px = proj.xOf(pivot.time);
11722
+ const py = proj.yOf(pivot.price, paneId);
11723
+ this.snapAt = null;
11724
+ if (py == null) return proj.pxToPoint(x, y, paneId);
11725
+ const r = Math.hypot(x - px, y - py);
11726
+ if (r < 1) return proj.pxToPoint(x, y, paneId);
11727
+ const angle = Math.round(Math.atan2(y - py, x - px) / ANGLE_SNAP_STEP) * ANGLE_SNAP_STEP;
11728
+ return proj.pxToPoint(px + r * Math.cos(angle), py + r * Math.sin(angle), paneId);
11729
+ }
11730
+ /** The pivot the free endpoint angle-snaps around while placing `draft`, or null when
11731
+ * the tool is not a two-point segment (or no anchor is committed yet). */
11732
+ placingPivot(draft) {
11733
+ if (!ANGLE_SNAP_TYPES.has(draft.type)) return null;
11734
+ return draft.anchors[draft.anchors.length - 1] ?? null;
11735
+ }
11227
11736
  /** Should the drawing layer win this press (vs pan)? Armed/placing/pressed, or over a drawing/handle. */
11228
11737
  claim(x, y) {
11229
11738
  if (this.deps.activeTool() != null || this.state.kind !== "idle") return true;
@@ -11245,12 +11754,15 @@ var DrawingInteraction = class {
11245
11754
  return this.state.kind === "pressed" && this.state.moved ? this.state.id : null;
11246
11755
  }
11247
11756
  /** A press: place the next anchor, or grab a drawing/handle (resolved as click or drag on release).
11248
- * `shift` over a drawing toggles it in/out of the selection (multi-select) instead of dragging. */
11757
+ * `shift` over a drawing toggles it in/out of the selection (multi-select) instead of dragging;
11758
+ * while placing a line tool it locks the segment angle to 45° steps. */
11249
11759
  down(x, y, mode = "off", shift = false) {
11250
11760
  const proj = this.deps.projector();
11251
11761
  if (this.state.kind === "placing") {
11252
11762
  if (this.state.draft.placementMode() !== "click") return;
11253
- this.state.draft.anchors.push(this.placementAnchor(this.state.draft, this.resolve(x, y, this.state.draft.paneId, mode)));
11763
+ const pivot = shift ? this.placingPivot(this.state.draft) : null;
11764
+ const point = pivot ? this.resolveAngleSnapped(pivot, this.state.draft.paneId, x, y) : this.resolve(x, y, this.state.draft.paneId, mode);
11765
+ this.state.draft.anchors.push(this.placementAnchor(this.state.draft, point));
11254
11766
  if (this.state.draft.anchors.length >= this.state.need) this.finalize();
11255
11767
  else this.deps.changed();
11256
11768
  return;
@@ -11275,8 +11787,9 @@ var DrawingInteraction = class {
11275
11787
  this.state = { kind: "pressed", id: hit.id, handle, grab: proj.pxToPoint(x, y, hit.paneId), orig: cloneAnchors(hit), px: x, py: y, moved: false };
11276
11788
  }
11277
11789
  }
11278
- /** Pointer move: advance the placing ghost, or live-preview a drag once past the slop. */
11279
- move(x, y, mode = "off") {
11790
+ /** Pointer move: advance the placing ghost, or live-preview a drag once past the slop.
11791
+ * `shift` on a line tool locks the segment angle to 45° steps (placing + handle drags). */
11792
+ move(x, y, mode = "off", shift = false) {
11280
11793
  if (this.state.kind === "idle") {
11281
11794
  const tool = this.deps.activeTool();
11282
11795
  const had = this.snapAt != null;
@@ -11290,7 +11803,8 @@ var DrawingInteraction = class {
11290
11803
  this.captureFreehand(x, y);
11291
11804
  return;
11292
11805
  }
11293
- this.state.cursor = this.resolve(x, y, this.state.draft.paneId, mode);
11806
+ const pivot = shift ? this.placingPivot(this.state.draft) : null;
11807
+ this.state.cursor = pivot ? this.resolveAngleSnapped(pivot, this.state.draft.paneId, x, y) : this.resolve(x, y, this.state.draft.paneId, mode);
11294
11808
  this.deps.changed();
11295
11809
  return;
11296
11810
  }
@@ -11301,7 +11815,11 @@ var DrawingInteraction = class {
11301
11815
  this.state.moved = true;
11302
11816
  if (this.state.handle >= 0) {
11303
11817
  const a = d.anchors[this.state.handle];
11304
- if (a) applyFree(a, this.resolve(x, y, d.paneId, mode), d.anchorSchema().slots[this.state.handle]?.free ?? "both");
11818
+ if (a) {
11819
+ const pivot = shift && ANGLE_SNAP_TYPES.has(d.type) && d.anchors.length === 2 ? d.anchors[1 - this.state.handle] : void 0;
11820
+ const point = pivot ? this.resolveAngleSnapped(pivot, d.paneId, x, y) : this.resolve(x, y, d.paneId, mode);
11821
+ applyFree(a, point, d.anchorSchema().slots[this.state.handle]?.free ?? "both");
11822
+ }
11305
11823
  d.constrainHandleDrag(this.state.handle);
11306
11824
  } else {
11307
11825
  const pt = this.deps.projector().pxToPoint(x, y, d.paneId);
@@ -12810,6 +13328,10 @@ var UserDrawingController = class {
12810
13328
  this.activeTool = null;
12811
13329
  // last-used style for the armed tool (seeds the placement ghost)
12812
13330
  this.intentCb = null;
13331
+ /** Another chart's in-progress placement, mirrored here as a ghost (drawings sync). */
13332
+ this.externalGhost = null;
13333
+ /** Last draft fingerprint reported upstream — gates the per-render emission to actual changes. */
13334
+ this.lastDraftKey = null;
12813
13335
  this.measure = new MeasureOverlay();
12814
13336
  // transient ruler — not a persistent drawing
12815
13337
  this.toolbarVisible = false;
@@ -12881,6 +13403,11 @@ var UserDrawingController = class {
12881
13403
  syncToolbarGutter() {
12882
13404
  this.deps.setToolbarGutter(this.toolbarVisible ? this.toolbarCollapsed ? TOOLBAR_COLLAPSED_WIDTH : TOOLBAR_WIDTH : 0);
12883
13405
  }
13406
+ /** Core push: mirror (or clear) another chart's in-progress placement as a ghost. */
13407
+ setExternalGhost(doc) {
13408
+ this.externalGhost = doc ? deserializeDrawing(doc) : null;
13409
+ this.render();
13410
+ }
12884
13411
  syncDrawings(docs) {
12885
13412
  this.drawings = docs.map((d) => deserializeDrawing(d)).filter((d) => d != null);
12886
13413
  if (this.textEditor && !this.editedDrawing(this.textEditor.id)) this.closeTextEditor();
@@ -13039,7 +13566,7 @@ var UserDrawingController = class {
13039
13566
  }
13040
13567
  this.interaction.down(x, y, snap, shift);
13041
13568
  }
13042
- pointerMove(x, y, snap = "off") {
13569
+ pointerMove(x, y, snap = "off", shift = false) {
13043
13570
  if (this.eraserMode) {
13044
13571
  if (this.erasing) this.deleteAt(x, y);
13045
13572
  return;
@@ -13049,7 +13576,7 @@ var UserDrawingController = class {
13049
13576
  this.render();
13050
13577
  return;
13051
13578
  }
13052
- this.interaction.move(x, y, snap);
13579
+ this.interaction.move(x, y, snap, shift);
13053
13580
  this.updateHover(x, y);
13054
13581
  }
13055
13582
  /** Track which drawing is hovered so its handles appear on hover (and only then). */
@@ -13435,6 +13962,8 @@ var UserDrawingController = class {
13435
13962
  this.layoutTextEditor();
13436
13963
  const ghost = this.interaction.ghost();
13437
13964
  if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
13965
+ if (this.externalGhost) this.painter.paintGhost(ctx, this.externalGhost, proj, this.deps.theme());
13966
+ this.emitDraft(ghost);
13438
13967
  const markers = this.interaction.placingMarkers(proj);
13439
13968
  if (markers) this.painter.paintHandles(ctx, markers);
13440
13969
  const m = this.interaction.snapMarker();
@@ -13496,6 +14025,15 @@ var UserDrawingController = class {
13496
14025
  }
13497
14026
  }, () => this.clearSelection());
13498
14027
  }
14028
+ /** Report placement progress upstream (`draft` intent) so the drawings sync can
14029
+ * mirror the ghost live on linked charts. Called from every render; only actual
14030
+ * shape changes emit, and the end of a placement emits one `null`. */
14031
+ emitDraft(ghost) {
14032
+ const key = ghost ? JSON.stringify({ t: ghost.type, p: ghost.paneId, a: ghost.anchors, s: ghost.style, x: ghost.text }) : null;
14033
+ if (key === this.lastDraftKey) return;
14034
+ this.lastDraftKey = key;
14035
+ this.intentCb?.({ kind: "draft", doc: ghost ? ghost.serialize() : null });
14036
+ }
13499
14037
  emit(i) {
13500
14038
  if (i.kind === "create") {
13501
14039
  if (i.doc.text && i.doc.text.color === void 0) {
@@ -14027,10 +14565,10 @@ var SEPARATOR_HIT_PX = 4;
14027
14565
  var NativeRenderer = class {
14028
14566
  constructor(opts) {
14029
14567
  this.capabilities = NATIVE_CAPABILITIES;
14030
- // The app "chrome" surface (drawing toolbar + axis-scale gutters): background + text.
14568
+ // The app "chrome" surface (drawing toolbar + in-chart dialogs): background + text.
14031
14569
  // Owned by the app theme alone (mount/setTheme) — config-level background/text edits
14032
- // (settings dialog, persisted configs) recolor only the plot, keeping the toolbar and
14033
- // scales on a consistent, always-readable surface.
14570
+ // (settings dialog, persisted configs) recolor the plot AND the axis scales (see
14571
+ // axisSurface), but never the toolbar/dialog chrome.
14034
14572
  this.surfaceBackground = DARK_THEME.background;
14035
14573
  this.surfaceTextColor = DARK_THEME.textColor;
14036
14574
  // the plot area (canvases + DOM overlays), inset to the right of the toolbar gutter
@@ -14230,6 +14768,7 @@ var NativeRenderer = class {
14230
14768
  this.candleDown = opts.downColor;
14231
14769
  this.scene.priceStyle = opts.priceStyle;
14232
14770
  this.scene.basePainting = basePaintingOf(opts.priceStyle);
14771
+ this.scene.candleOverride = candleOverrideFor(opts.priceStyle, this.scene.style.chartTypes);
14233
14772
  }
14234
14773
  this.theme = this.deriveTheme(DARK_THEME);
14235
14774
  }
@@ -14707,6 +15246,7 @@ var NativeRenderer = class {
14707
15246
  baselineLevel: next.baseline.baselineLevel
14708
15247
  };
14709
15248
  this.setPriceStyle(next.series.style);
15249
+ this.scene.candleOverride = candleOverrideFor(this.scene.priceStyle, s.chartTypes);
14710
15250
  this.scene.baselineValue = next.series.baseline;
14711
15251
  this.coords.setPitchScale(next.series.spacing);
14712
15252
  this.coords.setViewport(this.clampViewport(this.coords.getViewport().barSpacing, this.coords.getViewport().rightOffset));
@@ -14812,7 +15352,7 @@ var NativeRenderer = class {
14812
15352
  (patch) => this.applyConfig(patch),
14813
15353
  (json) => this.applyConfig(json),
14814
15354
  () => {
14815
- if (this.factoryConfig) this.applyConfig(this.factoryConfig);
15355
+ if (this.factoryConfig) this.applyConfig(factoryResetConfig(this.factoryConfig));
14816
15356
  this.settingsDialog?.close();
14817
15357
  this.openSettingsDialog();
14818
15358
  },
@@ -15013,10 +15553,16 @@ var NativeRenderer = class {
15013
15553
  borderColor: isDarkColor(this.surfaceBackground) ? DARK_THEME.borderColor : LIGHT_THEME.borderColor
15014
15554
  };
15015
15555
  }
15556
+ /** The colors the axis-scale gutters (price + time) paint with: the LIVE chart background
15557
+ * (`layout.background`) and its contrast-corrected text, so the scales read as part of the
15558
+ * plot. Only the toolbar/dialog chrome stays on the stable app-theme surface. */
15559
+ axisSurface() {
15560
+ return { background: this.theme.background, textColor: this.theme.textColor };
15561
+ }
15016
15562
  // ── lifecycle ──
15017
15563
  mount(container, theme) {
15018
15564
  this.mountContainer = container;
15019
- this.publishToolbarGutter();
15565
+ this.publishGutters();
15020
15566
  this.theme = this.deriveTheme(theme);
15021
15567
  this.surfaceBackground = theme.background;
15022
15568
  this.surfaceTextColor = theme.textColor;
@@ -15088,7 +15634,7 @@ var NativeRenderer = class {
15088
15634
  drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y) ?? false,
15089
15635
  drawingsSnapMode: () => this.snapMode,
15090
15636
  drawingsPointerDown: (x, y, snap, shift) => this.userDrawings?.pointerDown(x, y, snap, shift),
15091
- drawingsPointerMove: (x, y, snap) => this.userDrawings?.pointerMove(x, y, snap),
15637
+ drawingsPointerMove: (x, y, snap, shift) => this.userDrawings?.pointerMove(x, y, snap, shift),
15092
15638
  drawingsPointerUp: (x, y) => this.userDrawings?.pointerUp(x, y),
15093
15639
  drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
15094
15640
  drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
@@ -15325,6 +15871,7 @@ var NativeRenderer = class {
15325
15871
  this.attributionEl?.remove();
15326
15872
  this.attributionEl = null;
15327
15873
  this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
15874
+ this.mountContainer?.style.removeProperty("--vela-scale-gutter");
15328
15875
  this.mountContainer = null;
15329
15876
  this.wrapper?.remove();
15330
15877
  }
@@ -15493,6 +16040,7 @@ var NativeRenderer = class {
15493
16040
  const w = RIGHT_AXIS_W + AXIS_COL_W * this.maxOwnScaleColumns();
15494
16041
  if (w === this.rightAxisW) return false;
15495
16042
  this.rightAxisW = w;
16043
+ this.publishGutters();
15496
16044
  if (this.coords.width > 0) this.syncSize();
15497
16045
  return true;
15498
16046
  }
@@ -15652,6 +16200,7 @@ var NativeRenderer = class {
15652
16200
  if (style === this.scene.priceStyle) return;
15653
16201
  this.scene.priceStyle = style;
15654
16202
  this.scene.basePainting = basePaintingOf(style);
16203
+ this.scene.candleOverride = candleOverrideFor(style, this.scene.style.chartTypes);
15655
16204
  for (const cb of this.priceStyleCbs) cb(style);
15656
16205
  }
15657
16206
  onRemoveIndicator(cb) {
@@ -15813,7 +16362,10 @@ var NativeRenderer = class {
15813
16362
  active = true;
15814
16363
  }
15815
16364
  }
15816
- this.coords.setViewport(this.clampViewport(barSpacing, rightOffset));
16365
+ const clamped = this.clampViewport(barSpacing, rightOffset);
16366
+ if (clamped.barSpacing !== barSpacing) this.targetBarSpacing = clamped.barSpacing;
16367
+ if (this.scrollTargetRO != null && clamped.rightOffset !== rightOffset) this.scrollTargetRO = clamped.rightOffset;
16368
+ this.coords.setViewport(clamped);
15817
16369
  this.computeScales();
15818
16370
  if (this.easeScales(dtMs)) active = true;
15819
16371
  if (this.easeLiveBar(dtMs)) active = true;
@@ -16265,9 +16817,37 @@ var NativeRenderer = class {
16265
16817
  this.paintData();
16266
16818
  } else if (repaintsChrome(level) && this.paintedData) {
16267
16819
  this.chrome.prepare(this.scene, this.coords, this.theme);
16268
- this.chrome.render(this.scene, this.coords, this.theme, { background: this.surfaceBackground, textColor: this.surfaceTextColor });
16820
+ this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
16269
16821
  }
16270
16822
  this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
16823
+ if (!repaintsData(level) && this.paintedData) this.repaintCursorLayers();
16824
+ }
16825
+ /** Repaint the SDK layers that opted into cursor tracking (their own canvas only). */
16826
+ repaintCursorLayers() {
16827
+ const pane = this.scene.panes.get(PRICE_PANE_ID);
16828
+ if (!pane) return;
16829
+ const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
16830
+ for (const l of this.extLayers) {
16831
+ if (!l.def.repaintOnCursor) continue;
16832
+ l.instance.render(this.extLayerArgs(l.def.id, pane.scale, pane.bounds, nowMs));
16833
+ if (this.animZoom && l.instance.animating?.()) this.animator.start();
16834
+ }
16835
+ }
16836
+ /** One frame's args for an SDK renderer layer (shared by the data + cursor paint paths). */
16837
+ extLayerArgs(id, scale, bounds, nowMs) {
16838
+ return {
16839
+ bars: this.scene.bars,
16840
+ data: this.scene.nativeData.get(id),
16841
+ settings: this.scene.nativeData.get(`${id}-settings`) ?? {},
16842
+ pending: this.scene.nativePending.get(id) ?? [],
16843
+ coords: this.coords,
16844
+ scale,
16845
+ bounds,
16846
+ theme: this.theme,
16847
+ priceStyle: this.scene.priceStyle,
16848
+ nowMs,
16849
+ cursor: this.lastPointer
16850
+ };
16271
16851
  }
16272
16852
  /** Paint the below-data (L-1) + geometry (L0) + chrome (L1) layers from the current scene/coords. */
16273
16853
  paintData() {
@@ -16300,18 +16880,16 @@ var NativeRenderer = class {
16300
16880
  });
16301
16881
  const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
16302
16882
  for (const l of this.extLayers) {
16303
- l.instance.render({
16304
- bars: this.scene.bars,
16305
- data: this.scene.nativeData.get(l.def.id),
16306
- settings: this.scene.nativeData.get(`${l.def.id}-settings`) ?? {},
16307
- pending: this.scene.nativePending.get(l.def.id) ?? [],
16308
- coords: this.coords,
16309
- scale: pane.scale,
16310
- bounds: pane.bounds,
16311
- theme: this.theme,
16312
- priceStyle: this.scene.priceStyle,
16313
- nowMs
16314
- });
16883
+ const args = this.extLayerArgs(l.def.id, pane.scale, pane.bounds, nowMs);
16884
+ l.instance.render(args);
16885
+ if (l.def.id === this.scene.priceStyle && l.instance.modulateBase) {
16886
+ const mod = l.instance.modulateBase(args);
16887
+ if (mod) {
16888
+ if (mod.candleBodyScale != null) this.backend.candleBodyScale = clamp012(mod.candleBodyScale) || 0.01;
16889
+ if (mod.candleBodyAlpha != null) this.backend.candleBodyAlpha = clamp012(mod.candleBodyAlpha) * this.candleBodyAlpha;
16890
+ if (mod.gridAlpha != null) this.backend.gridAlpha = clamp012(mod.gridAlpha);
16891
+ }
16892
+ }
16315
16893
  if (this.animZoom && l.instance.animating?.()) this.animator.start();
16316
16894
  }
16317
16895
  }
@@ -16321,7 +16899,7 @@ var NativeRenderer = class {
16321
16899
  if (easeLive && liveActual) this.bars[li] = { ...liveActual, high: this.liveEaseHigh, low: this.liveEaseLow, close: this.liveEaseClose };
16322
16900
  this.scene.drawingSlices = this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map();
16323
16901
  this.backend.render(this.scene, this.coords, this.theme);
16324
- this.chrome.render(this.scene, this.coords, this.theme, { background: this.surfaceBackground, textColor: this.surfaceTextColor });
16902
+ this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
16325
16903
  this.userDrawings?.render();
16326
16904
  if (easeLive && liveActual) this.bars[li] = liveActual;
16327
16905
  this.paintedData = true;
@@ -16755,15 +17333,18 @@ var NativeRenderer = class {
16755
17333
  setToolbarGutter(px) {
16756
17334
  if (px === this.toolbarGutter) return;
16757
17335
  this.toolbarGutter = px;
16758
- this.publishToolbarGutter();
17336
+ this.publishGutters();
16759
17337
  this.positionAttribution();
16760
17338
  this.syncSize();
16761
17339
  }
16762
- /** Publish the gutter on the mount container as `--vela-toolbar-gutter`, so host
16763
- * overlays sharing that container (a status line, a custom legend) can anchor to
16764
- * the plot's left edge without reaching into the renderer's DOM. */
16765
- publishToolbarGutter() {
17340
+ /** Publish the gutters on the mount container as `--vela-toolbar-gutter` (left,
17341
+ * drawings toolbar) and `--vela-scale-gutter` (right, the full price-scale width
17342
+ * incl. merged own-scale columns), so host overlays sharing that container (a
17343
+ * status line, a watermark, a custom legend) can anchor to the plot's edges
17344
+ * without reaching into the renderer's DOM. */
17345
+ publishGutters() {
16766
17346
  this.mountContainer?.style.setProperty("--vela-toolbar-gutter", `${this.toolbarGutter}px`);
17347
+ this.mountContainer?.style.setProperty("--vela-scale-gutter", `${this.rightAxisW}px`);
16767
17348
  }
16768
17349
  /** The built-in mark, or the host's own when one is set. */
16769
17350
  buildAttributionEl() {
@@ -16833,7 +17414,13 @@ var NativeRenderer = class {
16833
17414
  this.fitContent();
16834
17415
  this.didInitialFit = true;
16835
17416
  }
16836
- this.scheduler.flushNow(4 /* Full */);
17417
+ if (this.animator.active) {
17418
+ this.computeScales();
17419
+ this.paintData();
17420
+ this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
17421
+ } else {
17422
+ this.scheduler.flushNow(4 /* Full */);
17423
+ }
16837
17424
  }
16838
17425
  applyBackground() {
16839
17426
  this.wrapper.style.background = this.theme.background;
@@ -16902,6 +17489,9 @@ function sanitizeHighlights(value) {
16902
17489
  }
16903
17490
  return out.sort((a, b) => a.from - b.from);
16904
17491
  }
17492
+ function clamp012(v) {
17493
+ return Math.max(0, Math.min(1, v));
17494
+ }
16905
17495
  function isPriceStyle2(v) {
16906
17496
  return typeof v === "string" && priceStyleIds().includes(v);
16907
17497
  }