@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/widget.cjs CHANGED
@@ -552,6 +552,11 @@ registerIcon(
552
552
  "pen-lock",
553
553
  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"/>')
554
554
  );
555
+ registerIcon(
556
+ "pen-sync",
557
+ // Pen + two stacked panes — drawing onto every linked chart at once.
558
+ 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"/>')
559
+ );
555
560
  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"/>'));
556
561
  registerIcon(
557
562
  "bucket",
@@ -6555,6 +6560,11 @@ var DrawingController = class {
6555
6560
  setToolbar(option) {
6556
6561
  this.port?.setToolbar(buildToolbar(option).definition);
6557
6562
  }
6563
+ /** Display (or clear) another chart's in-progress placement as a ghost — silently
6564
+ * inert on a renderer without the optional `setExternalGhost` seam. */
6565
+ setExternalGhost(doc) {
6566
+ this.port?.setExternalGhost?.(doc);
6567
+ }
6558
6568
  /** Push per-tool shortcut hints (pre-formatted display strings) to the toolbar flyouts. */
6559
6569
  setToolShortcuts(map) {
6560
6570
  this.port?.setToolShortcuts?.(map);
@@ -6785,6 +6795,9 @@ var DrawingController = class {
6785
6795
  case "arm":
6786
6796
  this.setTool(i.type);
6787
6797
  break;
6798
+ case "draft":
6799
+ this.events.emit("drawing:draft", { doc: i.doc });
6800
+ break;
6788
6801
  case "create": {
6789
6802
  const before = this.store.serialize();
6790
6803
  const last = this.lastStyle.get(i.doc.type);
@@ -7060,6 +7073,11 @@ var ProviderRegistry = class {
7060
7073
  };
7061
7074
 
7062
7075
  // src/chart-types/registry.ts
7076
+ function settingsRowVisible(when, bag) {
7077
+ if (!when) return true;
7078
+ const conds = Array.isArray(when) ? when : [when];
7079
+ return conds.every((c) => c.anyOf ? c.anyOf.some((x) => x === bag[c.key]) : bag[c.key] === c.equals);
7080
+ }
7063
7081
  var registry2 = /* @__PURE__ */ new Map();
7064
7082
  function registerChartType(def) {
7065
7083
  registry2.set(def.id, def);
@@ -9390,6 +9408,14 @@ var DrawingsControl = class {
9390
9408
  if (this.ok("setToolShortcuts")) this.ctrl.setToolShortcuts(map);
9391
9409
  return this;
9392
9410
  }
9411
+ /** Display another chart's in-progress placement as a GHOST at reduced opacity
9412
+ * (`null` clears it) — how a multi-chart host mirrors `drawing:draft` onto linked
9413
+ * charts. Never a real drawing: no store entry, no selection, no persistence.
9414
+ * Silently inert on a renderer without the optional seam. */
9415
+ setExternalGhost(doc) {
9416
+ this.ctrl.setExternalGhost(doc);
9417
+ return this;
9418
+ }
9393
9419
  /** Create a drawing programmatically (no clicking). Returns it, or null if unsupported. */
9394
9420
  add(type, init = {}) {
9395
9421
  if (!this.ok("add")) return null;
@@ -9628,6 +9654,11 @@ function themeTokens(t) {
9628
9654
  "--vela-hover-strong": wash(0.16),
9629
9655
  "--vela-focus": withAlpha(t.textColor, 0.5),
9630
9656
  "--vela-focus-soft": withAlpha(t.textColor, 0.12),
9657
+ // Separator hover — the SAME wash the chart's pane separators paint on hover
9658
+ // (soft full-thickness band + solid 2px center line), so DOM-drawn dividers
9659
+ // (the workspace grid) and canvas-drawn ones read as one family.
9660
+ "--vela-separator-hover-band": withAlpha(t.textColor, 0.1),
9661
+ "--vela-separator-hover-line": withAlpha(t.textColor, 0.55),
9631
9662
  "--vela-scroll": withAlpha(t.textColor, 0.3),
9632
9663
  "--vela-accent": ACCENT,
9633
9664
  "--vela-accent-bright": ACCENT_BRIGHT,
@@ -11471,6 +11502,44 @@ function basePaintingOf(style) {
11471
11502
  for (const t of chartTypes()) if (t.id === style) return t.basePainting ?? "candles";
11472
11503
  return "candles";
11473
11504
  }
11505
+ function hasOwnCandlePaint(style) {
11506
+ if (BUILTIN_PRICE_STYLES.includes(style)) return false;
11507
+ for (const t of chartTypes()) if (t.id === style) return (t.basePainting ?? "candles") === "candles";
11508
+ return false;
11509
+ }
11510
+ function candleOverrideFor(style, bags) {
11511
+ if (!hasOwnCandlePaint(style)) return null;
11512
+ const bag = bags[style] ?? {};
11513
+ const color = (v) => isColor(v) ? v : null;
11514
+ const bool = (v) => isBool(v) ? v : null;
11515
+ return {
11516
+ upColor: color(bag.candleUpColor),
11517
+ downColor: color(bag.candleDownColor),
11518
+ bodyVisible: bool(bag.candleBodyVisible),
11519
+ borderVisible: bool(bag.candleBorderVisible),
11520
+ borderUpColor: color(bag.candleBorderUpColor),
11521
+ borderDownColor: color(bag.candleBorderDownColor),
11522
+ wickVisible: bool(bag.candleWickVisible),
11523
+ wickUpColor: color(bag.candleWickUpColor),
11524
+ wickDownColor: color(bag.candleWickDownColor)
11525
+ };
11526
+ }
11527
+ function effectiveCandlePaint(base, override, themeUp, themeDown) {
11528
+ if (!override) return { up: themeUp, down: themeDown, candle: base };
11529
+ return {
11530
+ up: override.upColor ?? themeUp,
11531
+ down: override.downColor ?? themeDown,
11532
+ candle: {
11533
+ bodyVisible: override.bodyVisible ?? base.bodyVisible,
11534
+ borderVisible: override.borderVisible ?? base.borderVisible,
11535
+ borderUpColor: override.borderUpColor ?? base.borderUpColor,
11536
+ borderDownColor: override.borderDownColor ?? base.borderDownColor,
11537
+ wickVisible: override.wickVisible ?? base.wickVisible,
11538
+ wickUpColor: override.wickUpColor ?? base.wickUpColor,
11539
+ wickDownColor: override.wickDownColor ?? base.wickDownColor
11540
+ }
11541
+ };
11542
+ }
11474
11543
  function priceStyleIds() {
11475
11544
  const out = [...BUILTIN_PRICE_STYLES];
11476
11545
  for (const t of chartTypes()) if (!out.includes(t.id)) out.push(t.id);
@@ -11509,6 +11578,40 @@ function clampLevel(v) {
11509
11578
  function clampSpacing(v) {
11510
11579
  return v < 0.1 ? 0.1 : v > 10 ? 10 : v;
11511
11580
  }
11581
+ function factoryResetConfig(factory) {
11582
+ const bag = {};
11583
+ for (const t of chartTypes()) {
11584
+ const section = t.settings;
11585
+ if (!section) continue;
11586
+ const defaults2 = {};
11587
+ const addRows = (rows) => {
11588
+ for (const r of rows ?? []) {
11589
+ if (r.kind === "heading" || r.kind === "header") continue;
11590
+ if (r.kind === "range") {
11591
+ defaults2[r.minKey] = r.defval;
11592
+ defaults2[r.maxKey] = r.defval;
11593
+ continue;
11594
+ }
11595
+ defaults2[r.key] = r.defval;
11596
+ if (r.kind === "toggle") for (const c of r.colors ?? []) defaults2[c.key] = c.defval;
11597
+ }
11598
+ };
11599
+ for (const inst of section.instances ?? []) {
11600
+ if (inst.enableKey) defaults2[inst.enableKey] = false;
11601
+ addRows(inst.rows);
11602
+ }
11603
+ for (const sub of section.subsections ?? []) {
11604
+ if (sub.enableKey) defaults2[sub.enableKey] = false;
11605
+ addRows(sub.rows);
11606
+ }
11607
+ if (!section.instances) addRows(section.rows);
11608
+ bag[t.id] = defaults2;
11609
+ }
11610
+ for (const [typeId, vals] of Object.entries(factory.chartTypes)) {
11611
+ bag[typeId] = { ...bag[typeId] ?? {}, ...vals };
11612
+ }
11613
+ return { ...factory, chartTypes: bag };
11614
+ }
11512
11615
  function mergeConfig(base, patch) {
11513
11616
  const p = asObject(patch);
11514
11617
  const ctPatch = asObject(p.chartTypes);
@@ -11823,6 +11926,9 @@ var SceneGraph = class {
11823
11926
  this.priceStyle = "candles";
11824
11927
  /** Price-series base painting for the ACTIVE style (see ChartTypeDefinition.basePainting). */
11825
11928
  this.basePainting = "candles";
11929
+ /** The ACTIVE style's own candle cosmetics (`chartTypes.<id>.candle*`) when it is a
11930
+ * candle-based plugin type; null ⇒ paint with the shared `style.candle` block. */
11931
+ this.candleOverride = null;
11826
11932
  /** Explicit baseline reference price for `priceStyle:'baseline'`; when null the
11827
11933
  * baseline follows `style.baseline.baselineLevel` as a percent of the visible pane
11828
11934
  * range (resolved per frame via `baselinePriceFor`). */
@@ -13052,14 +13158,15 @@ var WebGL2Backend = class {
13052
13158
  emitCandles(b, scene, bars, i0, i1, coords, pane, theme, barColors) {
13053
13159
  const spacing = coords.bodySpacing();
13054
13160
  const tier = candleTier(spacing);
13161
+ const paint = effectiveCandlePaint(scene.style.candle, scene.candleOverride, theme.upColor, theme.downColor);
13055
13162
  if (tier === "aggregate") {
13056
- this.emitCandlesAggregated(b, bars, i0, i1, coords, pane, theme.upColor, theme.downColor, barColors);
13163
+ this.emitCandlesAggregated(b, bars, i0, i1, coords, pane, paint.up, paint.down, barColors);
13057
13164
  return;
13058
13165
  }
13059
13166
  const drawBody = tier === "full";
13060
- const up = theme.upColor;
13061
- const down = theme.downColor;
13062
- const cs = scene.style.candle;
13167
+ const up = paint.up;
13168
+ const down = paint.down;
13169
+ const cs = paint.candle;
13063
13170
  const fading = this.candleStructureAlpha > this.candleBodyAlpha + 1e-3;
13064
13171
  for (let i = i0; i <= i1; i += 1) {
13065
13172
  const bar = bars[i];
@@ -13651,7 +13758,6 @@ function effectiveSnapMode(momentaryOverride, sticky) {
13651
13758
  return momentaryOverride ? "strong" : sticky;
13652
13759
  }
13653
13760
  var InputController = class {
13654
- // the last middle press deleted a drawing (suppress autoscroll/paste)
13655
13761
  constructor(deps) {
13656
13762
  this.deps = deps;
13657
13763
  /** When true (the default), wheel-zoom pins the right edge instead of the bar
@@ -13679,9 +13785,23 @@ var InputController = class {
13679
13785
  this.vx = 0;
13680
13786
  // smoothed pointer velocity, px/ms
13681
13787
  this.middleDeleted = false;
13788
+ // the last middle press deleted a drawing (suppress autoscroll/paste)
13789
+ // Last cursor position over the element (NaN once it leaves) — lets a modifier
13790
+ // press/release re-shape the drawings preview with the pointer stationary.
13791
+ this.cursorX = NaN;
13792
+ this.cursorY = NaN;
13682
13793
  this.onMiddleGuard = (e) => {
13683
13794
  if (e.button === 1 && this.middleDeleted) e.preventDefault();
13684
13795
  };
13796
+ /** A modifier press/release with the pointer stationary: re-issue the last cursor
13797
+ * position so the placing ghost / drag preview reflects the new state immediately
13798
+ * (Shift's 45° angle lock, Ctrl/Cmd's momentary strong magnet). */
13799
+ this.onModifier = (e) => {
13800
+ if (e.repeat || e.key !== "Shift" && e.key !== "Control" && e.key !== "Meta") return;
13801
+ if (Number.isNaN(this.cursorX)) return;
13802
+ if (this.dragging && this.region !== "drawing") return;
13803
+ this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey);
13804
+ };
13685
13805
  this.onDown = (e) => {
13686
13806
  if (e.button === 1) {
13687
13807
  const { x: x2, y: y2 } = this.local(e);
@@ -13723,6 +13843,8 @@ var InputController = class {
13723
13843
  };
13724
13844
  this.onMove = (e) => {
13725
13845
  const { x, y } = this.local(e);
13846
+ this.cursorX = x;
13847
+ this.cursorY = y;
13726
13848
  if (this.dragging) {
13727
13849
  if (this.region === "price") {
13728
13850
  if (Math.abs(y - this.startY) > DRAG_SLOP) this.moved = true;
@@ -13736,7 +13858,7 @@ var InputController = class {
13736
13858
  this.deps.apply({ barSpacing, rightOffset: this.startRightOffset });
13737
13859
  } else if (this.region === "drawing") {
13738
13860
  if (Math.abs(x - this.startX) > DRAG_SLOP || Math.abs(y - this.startY) > DRAG_SLOP) this.moved = true;
13739
- this.deps.drawingsPointerMove?.(x, y, this.snapMode(e));
13861
+ this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
13740
13862
  } else {
13741
13863
  const coords = this.deps.getCoords();
13742
13864
  const vp = coords.getViewport();
@@ -13759,7 +13881,7 @@ var InputController = class {
13759
13881
  const drawCursor = this.deps.drawingsCursor?.(x, y);
13760
13882
  const r = this.regionAt(x, y);
13761
13883
  this.el.style.cursor = drawCursor ?? (r === "price" ? "ns-resize" : r === "time" ? "ew-resize" : r === "separator" ? "row-resize" : "");
13762
- this.deps.drawingsPointerMove?.(x, y, this.snapMode(e));
13884
+ this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
13763
13885
  }
13764
13886
  this.deps.onPointerMove(x, y);
13765
13887
  };
@@ -13784,6 +13906,8 @@ var InputController = class {
13784
13906
  }
13785
13907
  };
13786
13908
  this.onLeave = () => {
13909
+ this.cursorX = NaN;
13910
+ this.cursorY = NaN;
13787
13911
  this.deps.onPointerMove(null, null);
13788
13912
  };
13789
13913
  this.onDblClick = (e) => {
@@ -13822,10 +13946,16 @@ var InputController = class {
13822
13946
  el.addEventListener("wheel", this.onWheel, { passive: false });
13823
13947
  el.addEventListener("mousedown", this.onMiddleGuard);
13824
13948
  el.addEventListener("auxclick", this.onMiddleGuard);
13949
+ const win = el.ownerDocument?.defaultView;
13950
+ win?.addEventListener("keydown", this.onModifier);
13951
+ win?.addEventListener("keyup", this.onModifier);
13825
13952
  }
13826
13953
  detach() {
13827
13954
  const el = this.el;
13828
13955
  if (!el) return;
13956
+ const win = el.ownerDocument?.defaultView;
13957
+ win?.removeEventListener("keydown", this.onModifier);
13958
+ win?.removeEventListener("keyup", this.onModifier);
13829
13959
  el.removeEventListener("pointerdown", this.onDown);
13830
13960
  el.removeEventListener("pointermove", this.onMove);
13831
13961
  el.removeEventListener("pointerup", this.onUp);
@@ -14243,12 +14373,13 @@ var Canvas2dBackend = class {
14243
14373
  drawCandles(ctx, scene, bars, i0, i1, coords, pane, theme, barColors) {
14244
14374
  const spacing = coords.bodySpacing();
14245
14375
  const tier = candleTier(spacing);
14376
+ const paint = effectiveCandlePaint(scene.style.candle, scene.candleOverride, theme.upColor, theme.downColor);
14246
14377
  if (tier === "aggregate") {
14247
- this.drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, theme.upColor, theme.downColor, barColors);
14378
+ this.drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, paint.up, paint.down, barColors);
14248
14379
  return;
14249
14380
  }
14250
14381
  const drawBody = tier === "full";
14251
- const cs = scene.style.candle;
14382
+ const cs = paint.candle;
14252
14383
  const fading = this.candleStructureAlpha > this.candleBodyAlpha + 1e-3;
14253
14384
  for (let i = i0; i <= i1; i += 1) {
14254
14385
  const b = bars[i];
@@ -14256,7 +14387,7 @@ var Canvas2dBackend = class {
14256
14387
  const g = candleGeometry(coords.logicalToX(i), spacing, coords.dpr, this.candleBodyScale);
14257
14388
  const x = g.center;
14258
14389
  const up = b.close >= b.open;
14259
- const dir = up ? theme.upColor : theme.downColor;
14390
+ const dir = up ? paint.up : paint.down;
14260
14391
  const bc = barColors.get(b.time);
14261
14392
  const color = bc ?? dir;
14262
14393
  let top = 0;
@@ -15367,7 +15498,7 @@ var ChromeRenderer = class {
15367
15498
  constructor() {
15368
15499
  this.canvas = null;
15369
15500
  this.ctx = null;
15370
- // The color for axis tick labels — the stable chrome surface text, set each frame in render().
15501
+ // The color for axis tick labels — the host-passed surface text, set each frame in render().
15371
15502
  this.axisTextColor = DARK_THEME.textColor;
15372
15503
  // Shared Pine-drawing renderer (line/box/label/polyline/linefill); widthCache persists.
15373
15504
  this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
@@ -15399,8 +15530,8 @@ var ChromeRenderer = class {
15399
15530
  return dr;
15400
15531
  }
15401
15532
  /** Clear the chrome canvas and draw drawings + axes + current-price line.
15402
- * `surface` (background + text) paints the axis-scale gutters so they keep the app
15403
- * chrome color and stay readable, rather than tracking the user-editable plot cosmetics.
15533
+ * `surface` (background + text) paints the axis-scale gutters the host passes the live
15534
+ * chart background so the scales read as part of the plot, with contrast-corrected text.
15404
15535
  * Falls back to the theme's own colors when no surface is supplied. */
15405
15536
  render(scene, coords, theme, surface) {
15406
15537
  const ctx = this.ctx;
@@ -16265,6 +16396,37 @@ function ensureControlStyles() {
16265
16396
  .vela-sd-btn:hover{background:var(--vela-hover);border-color:var(--vela-border-strong);color:var(--vela-fg-bright);}
16266
16397
  .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;}
16267
16398
  .vela-sd-close:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
16399
+ /* Rows/blocks gated away by chart-type conditions, TOC filters, or the instance strip.
16400
+ !important: the pane grid rewrites inline display ('contents') AFTER the initial
16401
+ visibility pass, so a class must win. */
16402
+ .vela-sd-hide{display:none !important;}
16403
+ /* Indented rail sub-entry (a chart-type section's subsection tab). */
16404
+ .vela-sd-tab-sub{padding-left:28px;font-weight:600;font-size:12px;}
16405
+ /* Instance strip: a tab per present instance (label, \u2715 on the active removable one)
16406
+ and a dashed + that turns on the next absent instance. The rule under it separates
16407
+ the strip from the instance's TOC + rows area. */
16408
+ .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);}
16409
+ .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;}
16410
+ .vela-sd-itab:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
16411
+ .vela-sd-itab.on{background:var(--vela-active);color:var(--vela-fg-bright);border-color:var(--vela-border-strong);}
16412
+ .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;}
16413
+ .vela-sd-ix:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
16414
+ .vela-sd-itab-add{border-style:dashed;min-width:30px;justify-content:center;padding:0;}
16415
+ /* Structured pane: group TOC column + rows column, TOP-ALIGNED (the shared padding-top
16416
+ lives on the wrap, never on one column). The TOC sticks while the pane scrolls; the
16417
+ vertical rule sits on the rows column so it spans the full content height. When every
16418
+ group gates out the TOC hides and .no-toc drops the rule with it. */
16419
+ .vela-sd-struct{display:flex;align-items:flex-start;padding-top:14px;}
16420
+ .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;}
16421
+ .vela-sd-struct>[data-sd-rows-host]{border-left:1px solid var(--vela-border);padding-left:18px;}
16422
+ .vela-sd-struct.no-toc>[data-sd-rows-host]{border-left:none;padding-left:0;}
16423
+ .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;}
16424
+ .vela-sd-toc-btn:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
16425
+ .vela-sd-toc-btn.on{background:var(--vela-active);color:var(--vela-fg-bright);}
16426
+ /* Soft-disable: a subsection's enableKey is off \u2014 rows stay visible (browseable) but
16427
+ muted and non-interactive. Applied to each row's children so it survives display:contents;
16428
+ !important beats the inline opacity on labels. */
16429
+ .vela-sd-soft>*{opacity:0.4 !important;pointer-events:none !important;}
16268
16430
  `;
16269
16431
  document.head.appendChild(st);
16270
16432
  }
@@ -16285,6 +16447,10 @@ var SettingsDialog = class {
16285
16447
  this.themeControl = null;
16286
16448
  /** The built tabs, by title — how `showSection` reaches a pane while the dialog is open. */
16287
16449
  this.tabs = [];
16450
+ /** Active instance-strip tab per chart type — remembered across dialog rebuilds. */
16451
+ this.typeActiveInstance = /* @__PURE__ */ new Map();
16452
+ /** Active TOC group per structured pane (`<typeId>/<pane>` → group label). */
16453
+ this.typeActiveGroup = /* @__PURE__ */ new Map();
16288
16454
  /** The tab currently shown, so a theme change (which rebuilds) lands back on it. */
16289
16455
  this.activeSection = null;
16290
16456
  if (getComputedStyle(container).position === "static") container.style.position = "relative";
@@ -16446,6 +16612,29 @@ var SettingsDialog = class {
16446
16612
  baseline.append(this.numberRow("Width", config.baseline.width, 1, 10, 1, (v) => this.emit({ baseline: { width: v } })));
16447
16613
  groups.baseline = baseline;
16448
16614
  body.append(baseline);
16615
+ for (const def of chartTypes()) {
16616
+ if (!hasOwnCandlePaint(def.id)) continue;
16617
+ const bag = config.chartTypes[def.id] ?? {};
16618
+ const colorOf2 = (key, shared) => typeof bag[key] === "string" && bag[key] !== "" ? bag[key] : shared;
16619
+ const boolOf = (key, shared) => typeof bag[key] === "boolean" ? bag[key] : shared;
16620
+ const g = this.group();
16621
+ g.append(this.sectionTitle("Candles"));
16622
+ g.append(this.toggleRow("Body", boolOf("candleBodyVisible", config.candles.bodyVisible), (v) => this.emitType(def.id, "candleBodyVisible", v), [
16623
+ this.swatch(colorOf2("candleUpColor", config.candles.upColor), (v) => this.emitType(def.id, "candleUpColor", v)),
16624
+ this.swatch(colorOf2("candleDownColor", config.candles.downColor), (v) => this.emitType(def.id, "candleDownColor", v))
16625
+ ]));
16626
+ g.append(this.toggleRow("Borders", boolOf("candleBorderVisible", config.candles.borderVisible), (v) => this.emitType(def.id, "candleBorderVisible", v), [
16627
+ this.swatch(colorOf2("candleBorderUpColor", config.candles.borderUpColor), (v) => this.emitType(def.id, "candleBorderUpColor", v)),
16628
+ this.swatch(colorOf2("candleBorderDownColor", config.candles.borderDownColor), (v) => this.emitType(def.id, "candleBorderDownColor", v))
16629
+ ]));
16630
+ g.append(this.toggleRow("Wick", boolOf("candleWickVisible", config.candles.wickVisible), (v) => this.emitType(def.id, "candleWickVisible", v), [
16631
+ this.swatch(colorOf2("candleWickUpColor", config.candles.wickUpColor), (v) => this.emitType(def.id, "candleWickUpColor", v)),
16632
+ this.swatch(colorOf2("candleWickDownColor", config.candles.wickDownColor), (v) => this.emitType(def.id, "candleWickDownColor", v))
16633
+ ]));
16634
+ g.append(this.numberRow("Spacing", config.series.spacing, 0.1, 10, 0.1, (v) => this.emit({ series: { spacing: v } })));
16635
+ groups[def.id] = g;
16636
+ body.append(g);
16637
+ }
16449
16638
  showActive(config.series.style);
16450
16639
  body.append(this.sectionTitle("Time zone"));
16451
16640
  body.append(this.selectRowLabeled("Time zone", normalizeTimezone(config.timeScale.timezone), timezoneOptions(config.timeScale.timezone), (v) => this.emit({ timeScale: { timezone: v } })));
@@ -16460,7 +16649,16 @@ var SettingsDialog = class {
16460
16649
  }
16461
16650
  }
16462
16651
  };
16652
+ const renderChartTypeSections = (placement) => {
16653
+ for (const def of chartTypes()) {
16654
+ const typeSettings = def.settings;
16655
+ if (!typeSettings) continue;
16656
+ if ((typeSettings.placement ?? "end") !== placement) continue;
16657
+ this.chartTypeSection(def.id, typeSettings, config, body);
16658
+ }
16659
+ };
16463
16660
  renderHostSections("symbol");
16661
+ renderChartTypeSections("after-symbol");
16464
16662
  renderHostSections("after-symbol");
16465
16663
  body.append(this.section("Scales and lines"));
16466
16664
  body.append(this.sectionTitle("Price scale"));
@@ -16501,27 +16699,7 @@ var SettingsDialog = class {
16501
16699
  body.append(this.sectionTitle("Theme"));
16502
16700
  body.append(this.selectRow("Color theme", tc.current === "dark" ? "Dark" : "Light", ["Dark", "Light"], (v) => tc.onSelect(v === "Dark" ? "dark" : "light")));
16503
16701
  }
16504
- for (const def of chartTypes()) {
16505
- const typeSettings = def.settings;
16506
- if (!typeSettings) continue;
16507
- const marker = this.section(typeSettings.title);
16508
- marker.dataset.sdStyle = def.id;
16509
- marker.dataset.sdVisibility = typeSettings.visibility ?? "active";
16510
- body.append(marker);
16511
- const values = config.chartTypes[def.id] ?? {};
16512
- for (const r of typeSettings.rows) {
16513
- const current2 = values[r.key];
16514
- if (r.kind === "toggle") {
16515
- body.append(this.boolRow(r.label, typeof current2 === "boolean" ? current2 : r.defval, (v) => this.emitType(def.id, r.key, v)));
16516
- } else if (r.kind === "number") {
16517
- 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)));
16518
- } else if (r.kind === "color") {
16519
- body.append(this.colorRow(r.label, typeof current2 === "string" ? current2 : r.defval, (v) => this.emitType(def.id, r.key, v)));
16520
- } else {
16521
- body.append(this.selectRow(r.label, typeof current2 === "string" ? current2 : r.defval, [...r.options], (v) => this.emitType(def.id, r.key, v)));
16522
- }
16523
- }
16524
- }
16702
+ renderChartTypeSections("end");
16525
16703
  renderHostSections("end");
16526
16704
  const shell = document.createElement("div");
16527
16705
  shell.style.cssText = "display:flex;min-height:360px;max-height:calc(70vh - 100px);flex:1 1 auto;";
@@ -16540,7 +16718,7 @@ var SettingsDialog = class {
16540
16718
  const tab = document.createElement("button");
16541
16719
  tab.type = "button";
16542
16720
  tab.textContent = title;
16543
- tab.className = "vela-sd-tab";
16721
+ tab.className = "vela-sd-tab" + (child.dataset.sdSub !== void 0 ? " vela-sd-tab-sub" : "");
16544
16722
  panes.push({ title, el, tab, style: child.dataset.sdStyle, visibility: child.dataset.sdVisibility });
16545
16723
  current = el;
16546
16724
  child.remove();
@@ -16580,7 +16758,14 @@ var SettingsDialog = class {
16580
16758
  scrim.appendChild(dlg);
16581
16759
  this.container.appendChild(scrim);
16582
16760
  this.root = scrim;
16583
- for (const p of panes) this.layoutSettingsGrids(p.el, dlg);
16761
+ for (const p of panes) {
16762
+ const hosts = [...p.el.querySelectorAll("[data-sd-rows-host]")];
16763
+ if (hosts.length === 0) {
16764
+ this.layoutSettingsGrids(p.el, dlg);
16765
+ continue;
16766
+ }
16767
+ for (const h of hosts) this.layoutSettingsGrids(h, dlg);
16768
+ }
16584
16769
  }
16585
16770
  close() {
16586
16771
  closeColorPopover();
@@ -16610,6 +16795,291 @@ var SettingsDialog = class {
16610
16795
  el.textContent = text;
16611
16796
  return el;
16612
16797
  }
16798
+ /**
16799
+ * One chart type's settings tab (and its subsection tabs), appended to the linear
16800
+ * `body` for the pane splitter to file. One live values BAG serves the whole section
16801
+ * — instances and subsections share the per-type store, so a `when` gate anywhere
16802
+ * can reference a key edited anywhere else; every edit runs every registered
16803
+ * refresher.
16804
+ */
16805
+ chartTypeSection(typeId, section, config, body) {
16806
+ const marker = this.section(section.title);
16807
+ marker.dataset.sdStyle = typeId;
16808
+ marker.dataset.sdVisibility = section.visibility ?? "active";
16809
+ body.append(marker);
16810
+ const values = config.chartTypes[typeId] ?? {};
16811
+ const bag = {};
16812
+ const seedKey = (key, want, defval) => {
16813
+ const v = values[key];
16814
+ bag[key] = typeof v === want ? v : defval;
16815
+ };
16816
+ const seed = (rows) => {
16817
+ for (const r of rows) {
16818
+ if (r.kind === "heading" || r.kind === "header") continue;
16819
+ if (r.kind === "range") {
16820
+ seedKey(r.minKey, "number", r.defval);
16821
+ seedKey(r.maxKey, "number", r.defval);
16822
+ continue;
16823
+ }
16824
+ seedKey(r.key, r.kind === "toggle" ? "boolean" : r.kind === "number" ? "number" : "string", r.defval);
16825
+ if (r.kind === "toggle") for (const c of r.colors ?? []) seedKey(c.key, "string", c.defval);
16826
+ }
16827
+ };
16828
+ if (section.rows) seed(section.rows);
16829
+ for (const inst of section.instances ?? []) seed(inst.rows);
16830
+ for (const sub of section.subsections ?? []) seed(sub.rows);
16831
+ for (const inst of section.instances ?? []) {
16832
+ if (!inst.enableKey || inst.enableKey in bag) continue;
16833
+ bag[inst.enableKey] = values[inst.enableKey] === true;
16834
+ }
16835
+ for (const sub of section.subsections ?? []) {
16836
+ if (!sub.enableKey || sub.enableKey in bag) continue;
16837
+ bag[sub.enableKey] = values[sub.enableKey] === true;
16838
+ }
16839
+ const refreshers = [];
16840
+ const put = (key, v) => {
16841
+ bag[key] = v;
16842
+ this.emitType(typeId, key, v);
16843
+ for (const r of refreshers) r();
16844
+ };
16845
+ if (section.instances && section.instances.length > 0) {
16846
+ body.append(this.instancesBlock(typeId, section.instances, bag, put, refreshers));
16847
+ } else if (section.rows) {
16848
+ this.flatTypeRows(section.rows, bag, put, refreshers, body);
16849
+ }
16850
+ for (const sub of section.subsections ?? []) {
16851
+ const subMarker = this.section(sub.title);
16852
+ subMarker.dataset.sdStyle = typeId;
16853
+ subMarker.dataset.sdVisibility = section.visibility ?? "active";
16854
+ subMarker.dataset.sdSub = "1";
16855
+ body.append(subMarker);
16856
+ body.append(this.groupedRows(`${typeId}/${sub.title}`, sub.rows, bag, put, refreshers, sub.enableKey));
16857
+ }
16858
+ for (const r of refreshers) r();
16859
+ }
16860
+ /** The FLAT chart-type form: rows appended straight to the body (the pane grid wraps
16861
+ * them), headings/headers as inline group titles, `when` gates refreshed live. */
16862
+ flatTypeRows(rows, bag, put, refreshers, body) {
16863
+ const entries = [];
16864
+ for (const r of rows) {
16865
+ const el = r.kind === "heading" || r.kind === "header" ? this.sectionTitle(r.label) : this.typeRow(r, bag, put);
16866
+ entries.push({ el, when: r.when });
16867
+ body.append(el);
16868
+ }
16869
+ refreshers.push(() => {
16870
+ for (const e of entries) e.el.classList.toggle("vela-sd-hide", !settingsRowVisible(e.when, bag));
16871
+ });
16872
+ }
16873
+ /** One value row for a chart-type descriptor, writing through `put`. */
16874
+ typeRow(r, bag, put) {
16875
+ if (r.kind === "toggle") {
16876
+ const swatches = (r.colors ?? []).map((c) => {
16877
+ const sw = this.swatch(bag[c.key], (v) => put(c.key, v));
16878
+ sw.title = c.label;
16879
+ return sw;
16880
+ });
16881
+ return this.toggleRow(r.label, bag[r.key], (v) => put(r.key, v), swatches);
16882
+ }
16883
+ 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));
16884
+ if (r.kind === "color") return this.colorRow(r.label, bag[r.key], (v) => put(r.key, v));
16885
+ if (r.kind === "range") return this.rangeRow(r, bag, put);
16886
+ return this.selectRowLabeled(r.label, bag[r.key], normalizeSelectOptions(r.options), (v) => put(r.key, v));
16887
+ }
16888
+ /**
16889
+ * A MIN–MAX row: two number inputs on one row (stored under the descriptor's
16890
+ * `minKey`/`maxKey`). With a `placeholder`, an input at the DEFAULT value renders
16891
+ * empty showing it, and clearing an input stores the default back — the
16892
+ * placeholder names the unset state ('Off' for 0-disables bounds).
16893
+ */
16894
+ rangeRow(r, bag, put) {
16895
+ const { wrap } = this.row(r.label);
16896
+ const input = (key, title) => {
16897
+ const ni = document.createElement("input");
16898
+ ni.type = "number";
16899
+ ni.className = "vela-sd-number";
16900
+ ni.min = String(r.min ?? 0);
16901
+ ni.max = String(r.max ?? 1e6);
16902
+ ni.step = String(r.step ?? 1);
16903
+ ni.title = title;
16904
+ const current = bag[key];
16905
+ if (r.placeholder !== void 0) {
16906
+ ni.placeholder = r.placeholder;
16907
+ if (current !== r.defval) ni.value = String(current);
16908
+ } else {
16909
+ ni.value = String(current);
16910
+ }
16911
+ ni.addEventListener("change", () => {
16912
+ const raw = ni.value.trim() === "" ? r.defval : Number(ni.value);
16913
+ const v = Number.isFinite(raw) ? Math.min(r.max ?? Infinity, Math.max(r.min ?? -Infinity, raw)) : r.defval;
16914
+ ni.value = r.placeholder !== void 0 && v === r.defval ? "" : String(v);
16915
+ put(key, v);
16916
+ });
16917
+ return ni;
16918
+ };
16919
+ const box = document.createElement("div");
16920
+ box.style.cssText = "display:flex;align-items:center;gap:6px;flex:0 0 auto;";
16921
+ const dash = document.createElement("span");
16922
+ dash.textContent = "\u2013";
16923
+ dash.style.cssText = "color:var(--vela-fg-muted);";
16924
+ box.append(input(r.minKey, `${r.label} \u2014 min`), dash, input(r.maxKey, `${r.label} \u2014 max`));
16925
+ wrap.appendChild(box);
16926
+ return wrap;
16927
+ }
16928
+ /**
16929
+ * The INSTANCE STRIP block: a tab per present instance (label, `×` on the active
16930
+ * removable one), a dashed `+` while an instance is still absent, and one
16931
+ * grouped-rows content per instance below — only the active tab's content shows.
16932
+ * Presence is the boolean at each instance's `enableKey`; the strip rebuilds on
16933
+ * every section edit, so gates elsewhere stay coherent.
16934
+ */
16935
+ instancesBlock(typeId, instances, bag, put, refreshers) {
16936
+ const wrap = document.createElement("div");
16937
+ const strip = document.createElement("div");
16938
+ strip.className = "vela-sd-itabs";
16939
+ wrap.append(strip);
16940
+ const contents = instances.map((inst, i) => {
16941
+ const content = this.groupedRows(`${typeId}/#${i}`, inst.rows, bag, put, refreshers);
16942
+ wrap.append(content);
16943
+ return content;
16944
+ });
16945
+ const present = (inst) => !inst.enableKey || bag[inst.enableKey] === true;
16946
+ const refresh = () => {
16947
+ let active = this.typeActiveInstance.get(typeId) ?? 0;
16948
+ if (!present(instances[active] ?? instances[0])) active = 0;
16949
+ this.typeActiveInstance.set(typeId, active);
16950
+ strip.replaceChildren();
16951
+ instances.forEach((inst, i) => {
16952
+ if (!present(inst)) return;
16953
+ const tab = document.createElement("button");
16954
+ tab.type = "button";
16955
+ tab.className = "vela-sd-itab" + (i === active ? " on" : "");
16956
+ const lbl = document.createElement("span");
16957
+ lbl.textContent = inst.label;
16958
+ tab.append(lbl);
16959
+ if (i === active && inst.enableKey) {
16960
+ const x = document.createElement("span");
16961
+ x.className = "vela-sd-ix";
16962
+ x.textContent = "\u2715";
16963
+ x.title = `Remove ${inst.label}`;
16964
+ x.addEventListener("click", (e) => {
16965
+ e.stopPropagation();
16966
+ this.typeActiveInstance.set(typeId, 0);
16967
+ put(inst.enableKey, false);
16968
+ });
16969
+ tab.append(x);
16970
+ }
16971
+ tab.addEventListener("click", () => {
16972
+ this.typeActiveInstance.set(typeId, i);
16973
+ refresh();
16974
+ });
16975
+ strip.append(tab);
16976
+ });
16977
+ const absent = instances.findIndex((inst) => !present(inst));
16978
+ if (absent >= 0) {
16979
+ const add = document.createElement("button");
16980
+ add.type = "button";
16981
+ add.className = "vela-sd-itab vela-sd-itab-add";
16982
+ add.textContent = "+";
16983
+ add.title = `Add ${instances[absent].label}`;
16984
+ add.addEventListener("click", () => {
16985
+ this.typeActiveInstance.set(typeId, absent);
16986
+ put(instances[absent].enableKey, true);
16987
+ });
16988
+ strip.append(add);
16989
+ }
16990
+ contents.forEach((c, i) => c.classList.toggle("vela-sd-hide", i !== active));
16991
+ };
16992
+ refreshers.push(refresh);
16993
+ return wrap;
16994
+ }
16995
+ /**
16996
+ * A GROUPED rows pane: a TOC column of the group labels (from `heading` rows) and
16997
+ * ONE rows host holding every row — the active group's rows show, the rest hide.
16998
+ * Rows BEFORE the first heading are the always block, visible above every group.
16999
+ * `header` rows stay in the rows column as in-group subgroup titles (not TOC
17000
+ * entries). A group whose value rows are all gated out (or whose heading's own
17001
+ * `when` fails) leaves the TOC; the whole TOC hides when no group is live. The
17002
+ * rows host is tagged for `layoutSettingsGrids`, keeping one shared label column
17003
+ * across all groups.
17004
+ *
17005
+ * `enableKey` (optional): while that bag boolean is false, every row except the one
17006
+ * whose key matches is soft-disabled (visible but grayed / non-interactive) so the
17007
+ * pane stays browseable with the feature off.
17008
+ */
17009
+ groupedRows(paneKey, rows, bag, put, refreshers, enableKey) {
17010
+ const wrap = document.createElement("div");
17011
+ wrap.className = "vela-sd-struct";
17012
+ const toc = document.createElement("div");
17013
+ toc.className = "vela-sd-toc";
17014
+ const rowsHost = document.createElement("div");
17015
+ rowsHost.dataset.sdRowsHost = "1";
17016
+ rowsHost.style.cssText = "flex:1 1 auto;min-width:0;";
17017
+ wrap.append(toc, rowsHost);
17018
+ const entries = [];
17019
+ const groups = [];
17020
+ const groupWhens = [];
17021
+ let g = -1;
17022
+ for (const r of rows) {
17023
+ if (r.kind === "heading") {
17024
+ g = groups.length;
17025
+ groups.push(r.label);
17026
+ groupWhens.push(r.when);
17027
+ continue;
17028
+ }
17029
+ if (r.kind === "header") {
17030
+ entries.push({ el: this.sectionTitle(r.label), when: r.when, group: g, header: true });
17031
+ rowsHost.append(entries[entries.length - 1].el);
17032
+ continue;
17033
+ }
17034
+ const el = this.typeRow(r, bag, put);
17035
+ const key = r.kind === "range" ? void 0 : r.key;
17036
+ entries.push({ el, when: r.when, group: g, key });
17037
+ rowsHost.append(el);
17038
+ }
17039
+ const refresh = () => {
17040
+ const live = groups.map((_, gi) => settingsRowVisible(groupWhens[gi], bag) && entries.some((e) => e.group === gi && !e.header && settingsRowVisible(e.when, bag)));
17041
+ let activeIdx = groups.indexOf(this.typeActiveGroup.get(paneKey) ?? "");
17042
+ if (activeIdx < 0 || !live[activeIdx]) activeIdx = live.findIndex(Boolean);
17043
+ if (activeIdx >= 0) this.typeActiveGroup.set(paneKey, groups[activeIdx]);
17044
+ toc.replaceChildren();
17045
+ groups.forEach((label, gi) => {
17046
+ if (!live[gi]) return;
17047
+ const b = document.createElement("button");
17048
+ b.type = "button";
17049
+ b.className = "vela-sd-toc-btn" + (gi === activeIdx ? " on" : "");
17050
+ b.textContent = label;
17051
+ b.addEventListener("click", () => {
17052
+ this.typeActiveGroup.set(paneKey, label);
17053
+ refresh();
17054
+ });
17055
+ toc.append(b);
17056
+ });
17057
+ const anyGroup = live.some(Boolean);
17058
+ toc.classList.toggle("vela-sd-hide", !anyGroup);
17059
+ wrap.classList.toggle("no-toc", !anyGroup);
17060
+ const enabled = !enableKey || bag[enableKey] === true;
17061
+ for (let i = 0; i < entries.length; i++) {
17062
+ const e = entries[i];
17063
+ let visible = (e.group === -1 || e.group === activeIdx) && settingsRowVisible(e.when, bag);
17064
+ if (visible && e.header) {
17065
+ let hasContent = false;
17066
+ for (let j = i + 1; j < entries.length; j++) {
17067
+ const n = entries[j];
17068
+ if (n.group !== e.group || n.header) break;
17069
+ if (settingsRowVisible(n.when, bag)) {
17070
+ hasContent = true;
17071
+ break;
17072
+ }
17073
+ }
17074
+ visible = hasContent;
17075
+ }
17076
+ e.el.classList.toggle("vela-sd-hide", !visible);
17077
+ e.el.classList.toggle("vela-sd-soft", visible && !enabled && e.key !== enableKey);
17078
+ }
17079
+ };
17080
+ refreshers.push(refresh);
17081
+ return wrap;
17082
+ }
16613
17083
  /** Vertical breathing space between row clusters — grouping reads from whitespace,
16614
17084
  * so no drawn rule. */
16615
17085
  separator() {
@@ -16872,7 +17342,7 @@ var SettingsDialog = class {
16872
17342
  const { wrap } = this.row(label);
16873
17343
  const sel = document.createElement("select");
16874
17344
  sel.className = "vela-sd-select";
16875
- sel.style.cssText = "max-width:140px;flex:0 0 auto;";
17345
+ sel.style.cssText = "max-width:200px;flex:0 0 auto;";
16876
17346
  for (const [val, lbl] of options) {
16877
17347
  const o = document.createElement("option");
16878
17348
  o.value = val;
@@ -16888,7 +17358,7 @@ var SettingsDialog = class {
16888
17358
  const { wrap } = this.row(label);
16889
17359
  const sel = document.createElement("select");
16890
17360
  sel.className = "vela-sd-select";
16891
- sel.style.cssText = "max-width:140px;flex:0 0 auto;";
17361
+ sel.style.cssText = "max-width:200px;flex:0 0 auto;";
16892
17362
  for (const opt of options) {
16893
17363
  const o = document.createElement("option");
16894
17364
  o.value = opt;
@@ -16925,6 +17395,9 @@ var SettingsDialog = class {
16925
17395
  }
16926
17396
  };
16927
17397
  var AUTO_MANUAL_OPTS = [["auto", "Auto"], ["manual", "Manual"]];
17398
+ function normalizeSelectOptions(options) {
17399
+ return options.map((o) => typeof o === "string" ? [o, o] : o);
17400
+ }
16928
17401
  function timezoneOptions(current) {
16929
17402
  const options = TIMEZONES.map((t) => [t.value, tzMenuLabel(t.value, t.label)]);
16930
17403
  const normalized = normalizeTimezone(current);
@@ -18283,6 +18756,8 @@ function topDrawingAt(drawings, x, y, proj, tol = HIT_TOLERANCE) {
18283
18756
  // src/renderers/native/drawings/DrawingInteraction.ts
18284
18757
  var DRAG_SLOP2 = 3;
18285
18758
  var FREEHAND_SAMPLE_PX = 4;
18759
+ var ANGLE_SNAP_STEP = Math.PI / 4;
18760
+ var ANGLE_SNAP_TYPES = /* @__PURE__ */ new Set(["trendline", "ray", "extendedline", "infoline", "trendangle", "arrow"]);
18286
18761
  var DrawingInteraction = class {
18287
18762
  // magnet target for the ring
18288
18763
  constructor(deps) {
@@ -18323,6 +18798,27 @@ var DrawingInteraction = class {
18323
18798
  this.snapAt = changed ? { point: snapped, paneId } : null;
18324
18799
  return snapped;
18325
18800
  }
18801
+ /** Resolve a pixel to a data point with the segment angle locked to 45° steps around
18802
+ * `pivot` (Shift held on a line tool). Works in PIXEL space — the user reasons about
18803
+ * the angle they see, not about time/price units. The magnet is bypassed: snapping
18804
+ * the result to a candle afterwards would break the exact angle just enforced. */
18805
+ resolveAngleSnapped(pivot, paneId, x, y) {
18806
+ const proj = this.deps.projector();
18807
+ const px = proj.xOf(pivot.time);
18808
+ const py = proj.yOf(pivot.price, paneId);
18809
+ this.snapAt = null;
18810
+ if (py == null) return proj.pxToPoint(x, y, paneId);
18811
+ const r = Math.hypot(x - px, y - py);
18812
+ if (r < 1) return proj.pxToPoint(x, y, paneId);
18813
+ const angle = Math.round(Math.atan2(y - py, x - px) / ANGLE_SNAP_STEP) * ANGLE_SNAP_STEP;
18814
+ return proj.pxToPoint(px + r * Math.cos(angle), py + r * Math.sin(angle), paneId);
18815
+ }
18816
+ /** The pivot the free endpoint angle-snaps around while placing `draft`, or null when
18817
+ * the tool is not a two-point segment (or no anchor is committed yet). */
18818
+ placingPivot(draft) {
18819
+ if (!ANGLE_SNAP_TYPES.has(draft.type)) return null;
18820
+ return draft.anchors[draft.anchors.length - 1] ?? null;
18821
+ }
18326
18822
  /** Should the drawing layer win this press (vs pan)? Armed/placing/pressed, or over a drawing/handle. */
18327
18823
  claim(x, y) {
18328
18824
  if (this.deps.activeTool() != null || this.state.kind !== "idle") return true;
@@ -18344,12 +18840,15 @@ var DrawingInteraction = class {
18344
18840
  return this.state.kind === "pressed" && this.state.moved ? this.state.id : null;
18345
18841
  }
18346
18842
  /** A press: place the next anchor, or grab a drawing/handle (resolved as click or drag on release).
18347
- * `shift` over a drawing toggles it in/out of the selection (multi-select) instead of dragging. */
18843
+ * `shift` over a drawing toggles it in/out of the selection (multi-select) instead of dragging;
18844
+ * while placing a line tool it locks the segment angle to 45° steps. */
18348
18845
  down(x, y, mode = "off", shift = false) {
18349
18846
  const proj = this.deps.projector();
18350
18847
  if (this.state.kind === "placing") {
18351
18848
  if (this.state.draft.placementMode() !== "click") return;
18352
- this.state.draft.anchors.push(this.placementAnchor(this.state.draft, this.resolve(x, y, this.state.draft.paneId, mode)));
18849
+ const pivot = shift ? this.placingPivot(this.state.draft) : null;
18850
+ const point = pivot ? this.resolveAngleSnapped(pivot, this.state.draft.paneId, x, y) : this.resolve(x, y, this.state.draft.paneId, mode);
18851
+ this.state.draft.anchors.push(this.placementAnchor(this.state.draft, point));
18353
18852
  if (this.state.draft.anchors.length >= this.state.need) this.finalize();
18354
18853
  else this.deps.changed();
18355
18854
  return;
@@ -18374,8 +18873,9 @@ var DrawingInteraction = class {
18374
18873
  this.state = { kind: "pressed", id: hit.id, handle, grab: proj.pxToPoint(x, y, hit.paneId), orig: cloneAnchors(hit), px: x, py: y, moved: false };
18375
18874
  }
18376
18875
  }
18377
- /** Pointer move: advance the placing ghost, or live-preview a drag once past the slop. */
18378
- move(x, y, mode = "off") {
18876
+ /** Pointer move: advance the placing ghost, or live-preview a drag once past the slop.
18877
+ * `shift` on a line tool locks the segment angle to 45° steps (placing + handle drags). */
18878
+ move(x, y, mode = "off", shift = false) {
18379
18879
  if (this.state.kind === "idle") {
18380
18880
  const tool = this.deps.activeTool();
18381
18881
  const had = this.snapAt != null;
@@ -18389,7 +18889,8 @@ var DrawingInteraction = class {
18389
18889
  this.captureFreehand(x, y);
18390
18890
  return;
18391
18891
  }
18392
- this.state.cursor = this.resolve(x, y, this.state.draft.paneId, mode);
18892
+ const pivot = shift ? this.placingPivot(this.state.draft) : null;
18893
+ this.state.cursor = pivot ? this.resolveAngleSnapped(pivot, this.state.draft.paneId, x, y) : this.resolve(x, y, this.state.draft.paneId, mode);
18393
18894
  this.deps.changed();
18394
18895
  return;
18395
18896
  }
@@ -18400,7 +18901,11 @@ var DrawingInteraction = class {
18400
18901
  this.state.moved = true;
18401
18902
  if (this.state.handle >= 0) {
18402
18903
  const a = d.anchors[this.state.handle];
18403
- if (a) applyFree(a, this.resolve(x, y, d.paneId, mode), d.anchorSchema().slots[this.state.handle]?.free ?? "both");
18904
+ if (a) {
18905
+ const pivot = shift && ANGLE_SNAP_TYPES.has(d.type) && d.anchors.length === 2 ? d.anchors[1 - this.state.handle] : void 0;
18906
+ const point = pivot ? this.resolveAngleSnapped(pivot, d.paneId, x, y) : this.resolve(x, y, d.paneId, mode);
18907
+ applyFree(a, point, d.anchorSchema().slots[this.state.handle]?.free ?? "both");
18908
+ }
18404
18909
  d.constrainHandleDrag(this.state.handle);
18405
18910
  } else {
18406
18911
  const pt = this.deps.projector().pxToPoint(x, y, d.paneId);
@@ -19753,6 +20258,8 @@ var DrawingToolbar = class {
19753
20258
  this.eraserActive = false;
19754
20259
  this.stayBtn = null;
19755
20260
  this.stayActive = false;
20261
+ this.syncBtn = null;
20262
+ this.syncActive = false;
19756
20263
  this.collapseBtn = null;
19757
20264
  this.collapsed = false;
19758
20265
  this.visible = false;
@@ -19769,6 +20276,7 @@ var DrawingToolbar = class {
19769
20276
  this.dock = options.dock ?? "absolute";
19770
20277
  this.onCollapse = options.onCollapse ?? (() => {
19771
20278
  });
20279
+ this.onDrawingsSync = options.onDrawingsSync ?? null;
19772
20280
  ensureStyles4();
19773
20281
  this.root = document.createElement("div");
19774
20282
  this.root.className = "vela-dtb";
@@ -19867,6 +20375,10 @@ var DrawingToolbar = class {
19867
20375
  this.root.appendChild(this.makeMagnetCell());
19868
20376
  this.stayBtn = this.makeButton(STAY_ICON, "Stay in drawing mode", () => this.toggleStay());
19869
20377
  this.root.appendChild(this.stayBtn);
20378
+ if (this.onDrawingsSync) {
20379
+ this.syncBtn = this.makeButton(SYNC_ICON, "Sync drawings on all charts", () => this.toggleDrawingsSync());
20380
+ this.root.appendChild(this.syncBtn);
20381
+ }
19870
20382
  this.collapseBtn = this.makeButton(COLLAPSE_ICON, "Collapse toolbar", () => this.toggleCollapsed());
19871
20383
  this.collapseBtn.classList.add("vela-dtb-collapse");
19872
20384
  this.root.appendChild(this.collapseBtn);
@@ -19874,6 +20386,7 @@ var DrawingToolbar = class {
19874
20386
  this.paintEraser();
19875
20387
  this.paintMagnet();
19876
20388
  this.paintStay();
20389
+ this.paintDrawingsSync();
19877
20390
  this.paintCollapse();
19878
20391
  this.highlight();
19879
20392
  }
@@ -19924,7 +20437,7 @@ var DrawingToolbar = class {
19924
20437
  cell.appendChild(arrow);
19925
20438
  }
19926
20439
  this.tipDisposers.push(
19927
- attachChromeTooltip(cell, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(cell) ?? label, placement: "right", delayMs: 2e3 })
20440
+ attachChromeTooltip(cell, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(cell) ?? label, placement: "right" })
19928
20441
  );
19929
20442
  return { cell, icon: icon2, arrow };
19930
20443
  }
@@ -20034,6 +20547,23 @@ var DrawingToolbar = class {
20034
20547
  b.dataset.active = this.stayActive ? "1" : "";
20035
20548
  b.style.opacity = this.stayActive ? "1" : "0.5";
20036
20549
  }
20550
+ /** Toggle drawings sync on/off and notify the host. */
20551
+ toggleDrawingsSync() {
20552
+ this.syncActive = !this.syncActive;
20553
+ this.onDrawingsSync?.(this.syncActive);
20554
+ this.paintDrawingsSync();
20555
+ }
20556
+ /** Reflect drawings sync externally (e.g. set through the API) without notifying back. */
20557
+ setDrawingsSyncMode(on) {
20558
+ this.syncActive = on;
20559
+ this.paintDrawingsSync();
20560
+ }
20561
+ paintDrawingsSync() {
20562
+ const b = this.syncBtn;
20563
+ if (!b) return;
20564
+ b.dataset.active = this.syncActive ? "1" : "";
20565
+ b.style.opacity = this.syncActive ? "1" : "0.5";
20566
+ }
20037
20567
  // ── flyout ──
20038
20568
  /** Create the positioned flyout panel anchored to a cell, register it as open, and start the
20039
20569
  * outside-dismiss. Callers fill it with items. */
@@ -20193,6 +20723,8 @@ var DrawingToolbar = class {
20193
20723
  const type = this.lastUsed.get(group.id) ?? group.tools[0]?.type;
20194
20724
  const tool = group.tools.find((t) => t.type === type) ?? group.tools[0];
20195
20725
  iconBtn.innerHTML = hitHtml(tool?.icon ?? "");
20726
+ const cell = this.groupCells.get(group.id);
20727
+ if (cell) this.tipText.set(cell, tool?.label ?? group.label);
20196
20728
  }
20197
20729
  /** Reconcile active tints from state (armed group / open flyout / cursor / modes). Active is a
20198
20730
  * `data-active` attribute the stylesheet paints — no inline background, so hover never sticks. */
@@ -20216,7 +20748,7 @@ var DrawingToolbar = class {
20216
20748
  this.tipText.set(btn, title);
20217
20749
  if (icon2) btn.innerHTML = hitHtml(icon2);
20218
20750
  this.tipDisposers.push(
20219
- attachChromeTooltip(btn, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(btn) ?? title, placement: "right", delayMs: 2e3 })
20751
+ attachChromeTooltip(btn, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(btn) ?? title, placement: "right" })
20220
20752
  );
20221
20753
  btn.addEventListener("click", onClick);
20222
20754
  return btn;
@@ -20282,6 +20814,7 @@ var COLLAPSE_ICON = icon("chevrons-left");
20282
20814
  var EXPAND_ICON = icon("chevrons-right");
20283
20815
  var MAGNET_ICON = icon("magnet");
20284
20816
  var STAY_ICON = icon("pen-lock");
20817
+ var SYNC_ICON = icon("pen-sync");
20285
20818
  var ERASER_ICON = icon("eraser");
20286
20819
  var ARROW_ICON2 = icon("chevron-right");
20287
20820
  var CHECK_ICON = icon("check");
@@ -20505,6 +21038,10 @@ var UserDrawingController = class {
20505
21038
  this.activeTool = null;
20506
21039
  // last-used style for the armed tool (seeds the placement ghost)
20507
21040
  this.intentCb = null;
21041
+ /** Another chart's in-progress placement, mirrored here as a ghost (drawings sync). */
21042
+ this.externalGhost = null;
21043
+ /** Last draft fingerprint reported upstream — gates the per-render emission to actual changes. */
21044
+ this.lastDraftKey = null;
20508
21045
  this.measure = new MeasureOverlay();
20509
21046
  // transient ruler — not a persistent drawing
20510
21047
  this.toolbarVisible = false;
@@ -20576,6 +21113,11 @@ var UserDrawingController = class {
20576
21113
  syncToolbarGutter() {
20577
21114
  this.deps.setToolbarGutter(this.toolbarVisible ? this.toolbarCollapsed ? TOOLBAR_COLLAPSED_WIDTH : TOOLBAR_WIDTH : 0);
20578
21115
  }
21116
+ /** Core push: mirror (or clear) another chart's in-progress placement as a ghost. */
21117
+ setExternalGhost(doc) {
21118
+ this.externalGhost = doc ? deserializeDrawing(doc) : null;
21119
+ this.render();
21120
+ }
20579
21121
  syncDrawings(docs) {
20580
21122
  this.drawings = docs.map((d) => deserializeDrawing(d)).filter((d) => d != null);
20581
21123
  if (this.textEditor && !this.editedDrawing(this.textEditor.id)) this.closeTextEditor();
@@ -20734,7 +21276,7 @@ var UserDrawingController = class {
20734
21276
  }
20735
21277
  this.interaction.down(x, y, snap, shift);
20736
21278
  }
20737
- pointerMove(x, y, snap = "off") {
21279
+ pointerMove(x, y, snap = "off", shift = false) {
20738
21280
  if (this.eraserMode) {
20739
21281
  if (this.erasing) this.deleteAt(x, y);
20740
21282
  return;
@@ -20744,7 +21286,7 @@ var UserDrawingController = class {
20744
21286
  this.render();
20745
21287
  return;
20746
21288
  }
20747
- this.interaction.move(x, y, snap);
21289
+ this.interaction.move(x, y, snap, shift);
20748
21290
  this.updateHover(x, y);
20749
21291
  }
20750
21292
  /** Track which drawing is hovered so its handles appear on hover (and only then). */
@@ -21130,6 +21672,8 @@ var UserDrawingController = class {
21130
21672
  this.layoutTextEditor();
21131
21673
  const ghost = this.interaction.ghost();
21132
21674
  if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
21675
+ if (this.externalGhost) this.painter.paintGhost(ctx, this.externalGhost, proj, this.deps.theme());
21676
+ this.emitDraft(ghost);
21133
21677
  const markers = this.interaction.placingMarkers(proj);
21134
21678
  if (markers) this.painter.paintHandles(ctx, markers);
21135
21679
  const m = this.interaction.snapMarker();
@@ -21191,6 +21735,15 @@ var UserDrawingController = class {
21191
21735
  }
21192
21736
  }, () => this.clearSelection());
21193
21737
  }
21738
+ /** Report placement progress upstream (`draft` intent) so the drawings sync can
21739
+ * mirror the ghost live on linked charts. Called from every render; only actual
21740
+ * shape changes emit, and the end of a placement emits one `null`. */
21741
+ emitDraft(ghost) {
21742
+ const key = ghost ? JSON.stringify({ t: ghost.type, p: ghost.paneId, a: ghost.anchors, s: ghost.style, x: ghost.text }) : null;
21743
+ if (key === this.lastDraftKey) return;
21744
+ this.lastDraftKey = key;
21745
+ this.intentCb?.({ kind: "draft", doc: ghost ? ghost.serialize() : null });
21746
+ }
21194
21747
  emit(i) {
21195
21748
  if (i.kind === "create") {
21196
21749
  if (i.doc.text && i.doc.text.color === void 0) {
@@ -21728,10 +22281,10 @@ var SEPARATOR_HIT_PX = 4;
21728
22281
  var NativeRenderer = class {
21729
22282
  constructor(opts) {
21730
22283
  this.capabilities = NATIVE_CAPABILITIES;
21731
- // The app "chrome" surface (drawing toolbar + axis-scale gutters): background + text.
22284
+ // The app "chrome" surface (drawing toolbar + in-chart dialogs): background + text.
21732
22285
  // Owned by the app theme alone (mount/setTheme) — config-level background/text edits
21733
- // (settings dialog, persisted configs) recolor only the plot, keeping the toolbar and
21734
- // scales on a consistent, always-readable surface.
22286
+ // (settings dialog, persisted configs) recolor the plot AND the axis scales (see
22287
+ // axisSurface), but never the toolbar/dialog chrome.
21735
22288
  this.surfaceBackground = DARK_THEME.background;
21736
22289
  this.surfaceTextColor = DARK_THEME.textColor;
21737
22290
  // the plot area (canvases + DOM overlays), inset to the right of the toolbar gutter
@@ -21931,6 +22484,7 @@ var NativeRenderer = class {
21931
22484
  this.candleDown = opts.downColor;
21932
22485
  this.scene.priceStyle = opts.priceStyle;
21933
22486
  this.scene.basePainting = basePaintingOf(opts.priceStyle);
22487
+ this.scene.candleOverride = candleOverrideFor(opts.priceStyle, this.scene.style.chartTypes);
21934
22488
  }
21935
22489
  this.theme = this.deriveTheme(DARK_THEME);
21936
22490
  }
@@ -22408,6 +22962,7 @@ var NativeRenderer = class {
22408
22962
  baselineLevel: next.baseline.baselineLevel
22409
22963
  };
22410
22964
  this.setPriceStyle(next.series.style);
22965
+ this.scene.candleOverride = candleOverrideFor(this.scene.priceStyle, s.chartTypes);
22411
22966
  this.scene.baselineValue = next.series.baseline;
22412
22967
  this.coords.setPitchScale(next.series.spacing);
22413
22968
  this.coords.setViewport(this.clampViewport(this.coords.getViewport().barSpacing, this.coords.getViewport().rightOffset));
@@ -22513,7 +23068,7 @@ var NativeRenderer = class {
22513
23068
  (patch) => this.applyConfig(patch),
22514
23069
  (json) => this.applyConfig(json),
22515
23070
  () => {
22516
- if (this.factoryConfig) this.applyConfig(this.factoryConfig);
23071
+ if (this.factoryConfig) this.applyConfig(factoryResetConfig(this.factoryConfig));
22517
23072
  this.settingsDialog?.close();
22518
23073
  this.openSettingsDialog();
22519
23074
  },
@@ -22714,10 +23269,16 @@ var NativeRenderer = class {
22714
23269
  borderColor: isDarkColor(this.surfaceBackground) ? DARK_THEME.borderColor : LIGHT_THEME.borderColor
22715
23270
  };
22716
23271
  }
23272
+ /** The colors the axis-scale gutters (price + time) paint with: the LIVE chart background
23273
+ * (`layout.background`) and its contrast-corrected text, so the scales read as part of the
23274
+ * plot. Only the toolbar/dialog chrome stays on the stable app-theme surface. */
23275
+ axisSurface() {
23276
+ return { background: this.theme.background, textColor: this.theme.textColor };
23277
+ }
22717
23278
  // ── lifecycle ──
22718
23279
  mount(container, theme) {
22719
23280
  this.mountContainer = container;
22720
- this.publishToolbarGutter();
23281
+ this.publishGutters();
22721
23282
  this.theme = this.deriveTheme(theme);
22722
23283
  this.surfaceBackground = theme.background;
22723
23284
  this.surfaceTextColor = theme.textColor;
@@ -22789,7 +23350,7 @@ var NativeRenderer = class {
22789
23350
  drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y) ?? false,
22790
23351
  drawingsSnapMode: () => this.snapMode,
22791
23352
  drawingsPointerDown: (x, y, snap, shift) => this.userDrawings?.pointerDown(x, y, snap, shift),
22792
- drawingsPointerMove: (x, y, snap) => this.userDrawings?.pointerMove(x, y, snap),
23353
+ drawingsPointerMove: (x, y, snap, shift) => this.userDrawings?.pointerMove(x, y, snap, shift),
22793
23354
  drawingsPointerUp: (x, y) => this.userDrawings?.pointerUp(x, y),
22794
23355
  drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
22795
23356
  drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
@@ -23026,6 +23587,7 @@ var NativeRenderer = class {
23026
23587
  this.attributionEl?.remove();
23027
23588
  this.attributionEl = null;
23028
23589
  this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
23590
+ this.mountContainer?.style.removeProperty("--vela-scale-gutter");
23029
23591
  this.mountContainer = null;
23030
23592
  this.wrapper?.remove();
23031
23593
  }
@@ -23194,6 +23756,7 @@ var NativeRenderer = class {
23194
23756
  const w = RIGHT_AXIS_W + AXIS_COL_W * this.maxOwnScaleColumns();
23195
23757
  if (w === this.rightAxisW) return false;
23196
23758
  this.rightAxisW = w;
23759
+ this.publishGutters();
23197
23760
  if (this.coords.width > 0) this.syncSize();
23198
23761
  return true;
23199
23762
  }
@@ -23353,6 +23916,7 @@ var NativeRenderer = class {
23353
23916
  if (style === this.scene.priceStyle) return;
23354
23917
  this.scene.priceStyle = style;
23355
23918
  this.scene.basePainting = basePaintingOf(style);
23919
+ this.scene.candleOverride = candleOverrideFor(style, this.scene.style.chartTypes);
23356
23920
  for (const cb of this.priceStyleCbs) cb(style);
23357
23921
  }
23358
23922
  onRemoveIndicator(cb) {
@@ -23514,7 +24078,10 @@ var NativeRenderer = class {
23514
24078
  active = true;
23515
24079
  }
23516
24080
  }
23517
- this.coords.setViewport(this.clampViewport(barSpacing, rightOffset));
24081
+ const clamped = this.clampViewport(barSpacing, rightOffset);
24082
+ if (clamped.barSpacing !== barSpacing) this.targetBarSpacing = clamped.barSpacing;
24083
+ if (this.scrollTargetRO != null && clamped.rightOffset !== rightOffset) this.scrollTargetRO = clamped.rightOffset;
24084
+ this.coords.setViewport(clamped);
23518
24085
  this.computeScales();
23519
24086
  if (this.easeScales(dtMs)) active = true;
23520
24087
  if (this.easeLiveBar(dtMs)) active = true;
@@ -23966,9 +24533,37 @@ var NativeRenderer = class {
23966
24533
  this.paintData();
23967
24534
  } else if (repaintsChrome(level) && this.paintedData) {
23968
24535
  this.chrome.prepare(this.scene, this.coords, this.theme);
23969
- this.chrome.render(this.scene, this.coords, this.theme, { background: this.surfaceBackground, textColor: this.surfaceTextColor });
24536
+ this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
23970
24537
  }
23971
24538
  this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
24539
+ if (!repaintsData(level) && this.paintedData) this.repaintCursorLayers();
24540
+ }
24541
+ /** Repaint the SDK layers that opted into cursor tracking (their own canvas only). */
24542
+ repaintCursorLayers() {
24543
+ const pane = this.scene.panes.get(PRICE_PANE_ID);
24544
+ if (!pane) return;
24545
+ const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
24546
+ for (const l of this.extLayers) {
24547
+ if (!l.def.repaintOnCursor) continue;
24548
+ l.instance.render(this.extLayerArgs(l.def.id, pane.scale, pane.bounds, nowMs));
24549
+ if (this.animZoom && l.instance.animating?.()) this.animator.start();
24550
+ }
24551
+ }
24552
+ /** One frame's args for an SDK renderer layer (shared by the data + cursor paint paths). */
24553
+ extLayerArgs(id, scale, bounds, nowMs) {
24554
+ return {
24555
+ bars: this.scene.bars,
24556
+ data: this.scene.nativeData.get(id),
24557
+ settings: this.scene.nativeData.get(`${id}-settings`) ?? {},
24558
+ pending: this.scene.nativePending.get(id) ?? [],
24559
+ coords: this.coords,
24560
+ scale,
24561
+ bounds,
24562
+ theme: this.theme,
24563
+ priceStyle: this.scene.priceStyle,
24564
+ nowMs,
24565
+ cursor: this.lastPointer
24566
+ };
23972
24567
  }
23973
24568
  /** Paint the below-data (L-1) + geometry (L0) + chrome (L1) layers from the current scene/coords. */
23974
24569
  paintData() {
@@ -24001,18 +24596,16 @@ var NativeRenderer = class {
24001
24596
  });
24002
24597
  const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
24003
24598
  for (const l of this.extLayers) {
24004
- l.instance.render({
24005
- bars: this.scene.bars,
24006
- data: this.scene.nativeData.get(l.def.id),
24007
- settings: this.scene.nativeData.get(`${l.def.id}-settings`) ?? {},
24008
- pending: this.scene.nativePending.get(l.def.id) ?? [],
24009
- coords: this.coords,
24010
- scale: pane.scale,
24011
- bounds: pane.bounds,
24012
- theme: this.theme,
24013
- priceStyle: this.scene.priceStyle,
24014
- nowMs
24015
- });
24599
+ const args = this.extLayerArgs(l.def.id, pane.scale, pane.bounds, nowMs);
24600
+ l.instance.render(args);
24601
+ if (l.def.id === this.scene.priceStyle && l.instance.modulateBase) {
24602
+ const mod = l.instance.modulateBase(args);
24603
+ if (mod) {
24604
+ if (mod.candleBodyScale != null) this.backend.candleBodyScale = clamp012(mod.candleBodyScale) || 0.01;
24605
+ if (mod.candleBodyAlpha != null) this.backend.candleBodyAlpha = clamp012(mod.candleBodyAlpha) * this.candleBodyAlpha;
24606
+ if (mod.gridAlpha != null) this.backend.gridAlpha = clamp012(mod.gridAlpha);
24607
+ }
24608
+ }
24016
24609
  if (this.animZoom && l.instance.animating?.()) this.animator.start();
24017
24610
  }
24018
24611
  }
@@ -24022,7 +24615,7 @@ var NativeRenderer = class {
24022
24615
  if (easeLive && liveActual) this.bars[li] = { ...liveActual, high: this.liveEaseHigh, low: this.liveEaseLow, close: this.liveEaseClose };
24023
24616
  this.scene.drawingSlices = this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map();
24024
24617
  this.backend.render(this.scene, this.coords, this.theme);
24025
- this.chrome.render(this.scene, this.coords, this.theme, { background: this.surfaceBackground, textColor: this.surfaceTextColor });
24618
+ this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
24026
24619
  this.userDrawings?.render();
24027
24620
  if (easeLive && liveActual) this.bars[li] = liveActual;
24028
24621
  this.paintedData = true;
@@ -24456,15 +25049,18 @@ var NativeRenderer = class {
24456
25049
  setToolbarGutter(px) {
24457
25050
  if (px === this.toolbarGutter) return;
24458
25051
  this.toolbarGutter = px;
24459
- this.publishToolbarGutter();
25052
+ this.publishGutters();
24460
25053
  this.positionAttribution();
24461
25054
  this.syncSize();
24462
25055
  }
24463
- /** Publish the gutter on the mount container as `--vela-toolbar-gutter`, so host
24464
- * overlays sharing that container (a status line, a custom legend) can anchor to
24465
- * the plot's left edge without reaching into the renderer's DOM. */
24466
- publishToolbarGutter() {
25056
+ /** Publish the gutters on the mount container as `--vela-toolbar-gutter` (left,
25057
+ * drawings toolbar) and `--vela-scale-gutter` (right, the full price-scale width
25058
+ * incl. merged own-scale columns), so host overlays sharing that container (a
25059
+ * status line, a watermark, a custom legend) can anchor to the plot's edges
25060
+ * without reaching into the renderer's DOM. */
25061
+ publishGutters() {
24467
25062
  this.mountContainer?.style.setProperty("--vela-toolbar-gutter", `${this.toolbarGutter}px`);
25063
+ this.mountContainer?.style.setProperty("--vela-scale-gutter", `${this.rightAxisW}px`);
24468
25064
  }
24469
25065
  /** The built-in mark, or the host's own when one is set. */
24470
25066
  buildAttributionEl() {
@@ -24534,7 +25130,13 @@ var NativeRenderer = class {
24534
25130
  this.fitContent();
24535
25131
  this.didInitialFit = true;
24536
25132
  }
24537
- this.scheduler.flushNow(4 /* Full */);
25133
+ if (this.animator.active) {
25134
+ this.computeScales();
25135
+ this.paintData();
25136
+ this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
25137
+ } else {
25138
+ this.scheduler.flushNow(4 /* Full */);
25139
+ }
24538
25140
  }
24539
25141
  applyBackground() {
24540
25142
  this.wrapper.style.background = this.theme.background;
@@ -24603,6 +25205,9 @@ function sanitizeHighlights(value) {
24603
25205
  }
24604
25206
  return out.sort((a, b) => a.from - b.from);
24605
25207
  }
25208
+ function clamp012(v) {
25209
+ return Math.max(0, Math.min(1, v));
25210
+ }
24606
25211
  function isPriceStyle2(v) {
24607
25212
  return typeof v === "string" && priceStyleIds().includes(v);
24608
25213
  }
@@ -25839,6 +26444,326 @@ var Dialog = class {
25839
26444
  }
25840
26445
  };
25841
26446
 
26447
+ // src/widget/layout-picker.ts
26448
+ var STYLE_ID6 = "vela-widget-layout-picker-v14";
26449
+ var CSS3 = `
26450
+ .vela-lp-layer { position: absolute; z-index: var(--vela-z-menu); }
26451
+ .vela-lp {
26452
+ background: var(--vela-surface-elev);
26453
+ color: var(--vela-fg);
26454
+ border: 1px solid var(--vela-border-strong);
26455
+ border-radius: 8px;
26456
+ box-shadow: var(--vela-shadow);
26457
+ padding: 10px 12px 10px;
26458
+ font-size: 13px;
26459
+ user-select: none;
26460
+ transform-origin: top;
26461
+ animation: vela-lp-in var(--vela-dur-fast) var(--vela-ease);
26462
+ }
26463
+ @keyframes vela-lp-in {
26464
+ from { opacity: 0; transform: translateY(-4px) scale(0.98); }
26465
+ to { opacity: 1; transform: translateY(0) scale(1); }
26466
+ }
26467
+ .vela-lp-cols { display: flex; align-items: stretch; gap: 14px; }
26468
+ .vela-lp-heading {
26469
+ display: flex;
26470
+ align-items: center;
26471
+ gap: 8px;
26472
+ font-size: 10px;
26473
+ font-weight: 600;
26474
+ letter-spacing: 1.2px;
26475
+ text-transform: uppercase;
26476
+ color: var(--vela-fg-faint);
26477
+ margin-bottom: 10px;
26478
+ }
26479
+ /* "?" help badge: hover it for the canvas explainer. */
26480
+ .vela-lp-badge {
26481
+ display: inline-flex;
26482
+ align-items: center;
26483
+ justify-content: center;
26484
+ width: 14px;
26485
+ height: 14px;
26486
+ border-radius: 50%;
26487
+ border: 1px solid var(--vela-border-strong);
26488
+ color: var(--vela-fg-muted);
26489
+ font-size: 9px;
26490
+ font-weight: 600;
26491
+ letter-spacing: 0;
26492
+ cursor: default;
26493
+ transition: color var(--vela-dur-fast) var(--vela-ease), border-color var(--vela-dur-fast) var(--vela-ease);
26494
+ }
26495
+ .vela-lp-badge:hover { color: var(--vela-fg-bright); border-color: var(--vela-fg-muted); }
26496
+ .vela-lp-tip { display: flex; flex-direction: column; gap: 4px; max-width: 230px; white-space: normal; }
26497
+ .vela-lp-vsep { width: 1px; flex: none; align-self: stretch; background: var(--vela-border-faint); }
26498
+ .vela-lp-layout { width: 132px; display: flex; flex-direction: column; align-items: center; }
26499
+ .vela-lp-layout > .vela-lp-heading,
26500
+ .vela-lp-layout > .vela-lp-presets { align-self: stretch; }
26501
+ .vela-lp-grid { display: grid; grid-template-columns: repeat(4, 24px); grid-auto-rows: 24px; gap: 4px; }
26502
+ .vela-lp-sq {
26503
+ all: unset;
26504
+ box-sizing: border-box;
26505
+ border-radius: 4px;
26506
+ background: var(--vela-hover);
26507
+ border: 1px solid var(--vela-border-faint);
26508
+ cursor: pointer;
26509
+ transition: background var(--vela-dur-fast) var(--vela-ease), border-color var(--vela-dur-fast) var(--vela-ease);
26510
+ }
26511
+ .vela-lp-sq:hover { background: var(--vela-hover-strong); border-color: var(--vela-border-strong); }
26512
+ /* Selected/previewed squares \u2014 the shared monochrome selection chip. */
26513
+ .vela-lp-sq[data-on='1'] {
26514
+ background: var(--vela-selected-bg);
26515
+ border-color: var(--vela-selected-bg);
26516
+ }
26517
+ .vela-lp-presets { display: flex; flex-direction: column; gap: 2px; margin-top: 8px; }
26518
+ .vela-lp-preset { all: unset; padding: 5px 8px; border-radius: 4px; cursor: pointer; color: var(--vela-fg-muted); font-size: 12px; white-space: nowrap; transition: transform 120ms var(--vela-ease); }
26519
+ .vela-lp-preset:hover { background: var(--vela-hover); }
26520
+ .vela-lp-preset:active { transform: scale(0.98); }
26521
+ .vela-lp-preset[data-checked='1'] { background: var(--vela-hover-strong); color: var(--vela-fg-bright); }
26522
+ .vela-lp-sync { display: flex; flex-direction: column; gap: 4px; min-width: 118px; }
26523
+ .vela-lp-sync-row { all: unset; display: flex; align-items: center; gap: 14px; padding: 6px 8px; border-radius: 5px; cursor: pointer; }
26524
+ .vela-lp-sync-row:hover { background: var(--vela-hover); }
26525
+ .vela-lp-sync-row .vela-lp-label { flex: 1 1 auto; }
26526
+ /* Toggle pill \u2014 same control language as the menu's switch rows, monochrome ON state. */
26527
+ .vela-lp-switch {
26528
+ position: relative;
26529
+ flex: none;
26530
+ width: 34px;
26531
+ height: 18px;
26532
+ border-radius: 9px;
26533
+ background: var(--vela-hover);
26534
+ border: 1px solid var(--vela-border-soft);
26535
+ transition: background 0.16s ease, border-color 0.16s ease;
26536
+ }
26537
+ .vela-lp-switch::after {
26538
+ content: '';
26539
+ position: absolute;
26540
+ top: 2px;
26541
+ left: 2px;
26542
+ width: 12px;
26543
+ height: 12px;
26544
+ border-radius: 50%;
26545
+ background: var(--vela-fg-muted);
26546
+ transition: transform 0.16s ease, background 0.16s ease;
26547
+ }
26548
+ .vela-lp-switch.on { background: var(--vela-selected-bg); border-color: var(--vela-selected-bg); }
26549
+ .vela-lp-switch.on::after { transform: translateX(16px); background: var(--vela-selected-fg); }
26550
+ `;
26551
+ var GRID = 4;
26552
+ var LayoutPicker = class {
26553
+ constructor(opts) {
26554
+ this.squares = [];
26555
+ this.isOpen = false;
26556
+ /** Hover preview (1-based rows/cols), null = show current shape. */
26557
+ this.hover = null;
26558
+ this.onDocPointerDown = (e) => {
26559
+ const t = e.target;
26560
+ if (t && (this.layer.contains(t) || this.opts.trigger.contains(t))) return;
26561
+ this.close();
26562
+ };
26563
+ this.onDocKeydown = (e) => {
26564
+ if (e.key === "Escape") this.close();
26565
+ };
26566
+ this.opts = opts;
26567
+ this.doc = opts.host.ownerDocument;
26568
+ injectStyles(STYLE_ID6, CSS3, this.doc);
26569
+ const doc = this.doc;
26570
+ this.layer = doc.createElement("div");
26571
+ this.layer.className = "vela-ui-layer vela-lp-layer";
26572
+ this.layer.style.display = "none";
26573
+ const panel = doc.createElement("div");
26574
+ panel.className = "vela-lp";
26575
+ this.layer.appendChild(panel);
26576
+ const cols = doc.createElement("div");
26577
+ cols.className = "vela-lp-cols";
26578
+ panel.appendChild(cols);
26579
+ const layoutCol = doc.createElement("div");
26580
+ layoutCol.className = "vela-lp-layout";
26581
+ const layoutHeading = doc.createElement("div");
26582
+ layoutHeading.className = "vela-lp-heading";
26583
+ const headingText = doc.createElement("span");
26584
+ headingText.textContent = "Layout";
26585
+ const badge = doc.createElement("span");
26586
+ badge.className = "vela-lp-badge";
26587
+ badge.textContent = "?";
26588
+ layoutHeading.append(headingText, badge);
26589
+ layoutCol.appendChild(layoutHeading);
26590
+ this.infoTip = new Tooltip(badge, {
26591
+ host: opts.host,
26592
+ placement: "bottom",
26593
+ content: () => this.tipNode()
26594
+ });
26595
+ const grid = doc.createElement("div");
26596
+ grid.className = "vela-lp-grid";
26597
+ for (let r = 0; r < GRID; r += 1) {
26598
+ for (let c = 0; c < GRID; c += 1) {
26599
+ const sq = doc.createElement("button");
26600
+ sq.className = "vela-lp-sq";
26601
+ sq.dataset.r = String(r);
26602
+ sq.dataset.c = String(c);
26603
+ sq.setAttribute("aria-label", `${c + 1} \xD7 ${r + 1}`);
26604
+ this.squares.push(sq);
26605
+ grid.appendChild(sq);
26606
+ }
26607
+ }
26608
+ layoutCol.appendChild(grid);
26609
+ this.presetsEl = doc.createElement("div");
26610
+ this.presetsEl.className = "vela-lp-presets";
26611
+ layoutCol.appendChild(this.presetsEl);
26612
+ cols.appendChild(layoutCol);
26613
+ const vsep = doc.createElement("div");
26614
+ vsep.className = "vela-lp-vsep";
26615
+ cols.appendChild(vsep);
26616
+ const syncCol = doc.createElement("div");
26617
+ const syncHeading = doc.createElement("div");
26618
+ syncHeading.className = "vela-lp-heading";
26619
+ syncHeading.textContent = "Sync";
26620
+ syncCol.appendChild(syncHeading);
26621
+ this.syncEl = doc.createElement("div");
26622
+ this.syncEl.className = "vela-lp-sync";
26623
+ syncCol.appendChild(this.syncEl);
26624
+ cols.appendChild(syncCol);
26625
+ grid.addEventListener("pointerdown", (e) => {
26626
+ const sq = this.squareAt(e);
26627
+ if (!sq) return;
26628
+ e.preventDefault();
26629
+ const { r, c } = this.squarePos(sq);
26630
+ this.close();
26631
+ this.opts.onSelectGrid(r + 1, c + 1);
26632
+ });
26633
+ grid.addEventListener("pointermove", (e) => {
26634
+ const sq = this.squareAt(e);
26635
+ if (!sq) return;
26636
+ const { r, c } = this.squarePos(sq);
26637
+ if (this.hover?.rows !== r + 1 || this.hover?.cols !== c + 1) {
26638
+ this.hover = { rows: r + 1, cols: c + 1 };
26639
+ this.render();
26640
+ }
26641
+ });
26642
+ grid.addEventListener("pointerleave", () => {
26643
+ if (this.hover) {
26644
+ this.hover = null;
26645
+ this.render();
26646
+ }
26647
+ });
26648
+ opts.trigger.addEventListener("click", () => this.toggle());
26649
+ opts.trigger.setAttribute("aria-haspopup", "true");
26650
+ opts.trigger.setAttribute("aria-expanded", "false");
26651
+ opts.host.appendChild(this.layer);
26652
+ }
26653
+ toggle() {
26654
+ if (this.isOpen) this.close();
26655
+ else this.open();
26656
+ }
26657
+ open() {
26658
+ if (this.isOpen) return;
26659
+ this.isOpen = true;
26660
+ this.hover = null;
26661
+ this.refresh();
26662
+ this.layer.style.display = "";
26663
+ this.position();
26664
+ this.opts.trigger.setAttribute("aria-expanded", "true");
26665
+ this.doc.addEventListener("pointerdown", this.onDocPointerDown, true);
26666
+ this.doc.addEventListener("keydown", this.onDocKeydown, true);
26667
+ this.opts.onOpenChange?.(true);
26668
+ }
26669
+ close() {
26670
+ if (!this.isOpen) return;
26671
+ this.isOpen = false;
26672
+ this.layer.style.display = "none";
26673
+ this.opts.trigger.setAttribute("aria-expanded", "false");
26674
+ this.doc.removeEventListener("pointerdown", this.onDocPointerDown, true);
26675
+ this.doc.removeEventListener("keydown", this.onDocKeydown, true);
26676
+ this.opts.onOpenChange?.(false);
26677
+ }
26678
+ /** Re-read shape/presets/syncs and re-render (no-op while closed — `open` re-reads). */
26679
+ refresh() {
26680
+ if (!this.isOpen) return;
26681
+ this.renderPresets();
26682
+ this.renderSyncs();
26683
+ this.render();
26684
+ }
26685
+ destroy() {
26686
+ this.close();
26687
+ this.infoTip.destroy();
26688
+ this.layer.remove();
26689
+ }
26690
+ // ── internals ──
26691
+ squareAt(e) {
26692
+ const sq = e.target?.closest?.(".vela-lp-sq");
26693
+ return sq instanceof HTMLButtonElement ? sq : null;
26694
+ }
26695
+ squarePos(sq) {
26696
+ return { r: Number(sq.dataset.r), c: Number(sq.dataset.c) };
26697
+ }
26698
+ /** Help copy for the LAYOUT "?" badge. */
26699
+ tipNode() {
26700
+ const tip = this.doc.createElement("div");
26701
+ tip.className = "vela-lp-tip";
26702
+ tip.textContent = "Click a square to apply that columns \xD7 rows layout.";
26703
+ return tip;
26704
+ }
26705
+ /** Project the interaction state onto the DOM (the hover/current rectangle). */
26706
+ render() {
26707
+ const preview = this.hover ?? this.opts.shape();
26708
+ for (const sq of this.squares) {
26709
+ const { r, c } = this.squarePos(sq);
26710
+ const on = preview !== null && r < preview.rows && c < preview.cols;
26711
+ if (on) sq.dataset.on = "1";
26712
+ else delete sq.dataset.on;
26713
+ }
26714
+ }
26715
+ renderPresets() {
26716
+ const doc = this.doc;
26717
+ this.presetsEl.replaceChildren();
26718
+ const presets = this.opts.presets();
26719
+ this.presetsEl.style.display = presets.length > 0 ? "" : "none";
26720
+ for (const p of presets) {
26721
+ const b = doc.createElement("button");
26722
+ b.className = "vela-lp-preset";
26723
+ b.textContent = p.label;
26724
+ if (p.checked) b.dataset.checked = "1";
26725
+ b.addEventListener("click", () => {
26726
+ this.close();
26727
+ this.opts.onSelectPreset(p.id);
26728
+ });
26729
+ this.presetsEl.appendChild(b);
26730
+ }
26731
+ }
26732
+ renderSyncs() {
26733
+ const doc = this.doc;
26734
+ this.syncEl.replaceChildren();
26735
+ for (const s of this.opts.syncs()) {
26736
+ const row = doc.createElement("button");
26737
+ row.className = "vela-lp-sync-row";
26738
+ const label = doc.createElement("span");
26739
+ label.className = "vela-lp-label";
26740
+ label.textContent = s.label;
26741
+ const pill = doc.createElement("span");
26742
+ pill.className = "vela-lp-switch" + (s.checked ? " on" : "");
26743
+ pill.setAttribute("aria-hidden", "true");
26744
+ row.setAttribute("role", "switch");
26745
+ row.setAttribute("aria-checked", String(s.checked));
26746
+ row.append(label, pill);
26747
+ row.addEventListener("click", () => {
26748
+ this.opts.onToggleSync(s.id);
26749
+ this.renderSyncs();
26750
+ });
26751
+ this.syncEl.appendChild(row);
26752
+ }
26753
+ }
26754
+ /** Anchor under the trigger, clamped to the host's right edge. */
26755
+ position() {
26756
+ const hostRect = this.opts.host.getBoundingClientRect();
26757
+ const trigRect = this.opts.trigger.getBoundingClientRect();
26758
+ let left = trigRect.left - hostRect.left;
26759
+ const top = trigRect.bottom - hostRect.top + 4;
26760
+ const width = this.layer.offsetWidth;
26761
+ if (left + width > hostRect.width - 8) left = Math.max(8, hostRect.width - 8 - width);
26762
+ this.layer.style.left = `${left}px`;
26763
+ this.layer.style.top = `${top}px`;
26764
+ }
26765
+ };
26766
+
25842
26767
  // src/widget/contributions.ts
25843
26768
  var registry4 = /* @__PURE__ */ new Map();
25844
26769
  var attachments = /* @__PURE__ */ new Map();
@@ -25984,8 +26909,8 @@ function timeframeLabel(value) {
25984
26909
  }
25985
26910
 
25986
26911
  // src/widget/topbar.ts
25987
- var STYLE_ID6 = "vela-topbar";
25988
- var CSS3 = `
26912
+ var STYLE_ID7 = "vela-topbar";
26913
+ var CSS4 = `
25989
26914
  .vela-widget-topbar {
25990
26915
  display: flex;
25991
26916
  align-items: center;
@@ -26097,7 +27022,7 @@ function priceStyleIcon(id) {
26097
27022
  var Topbar = class {
26098
27023
  constructor(host, opts) {
26099
27024
  this.layoutButton = null;
26100
- this.layoutMenu = null;
27025
+ this.layoutPicker = null;
26101
27026
  this.layoutId = null;
26102
27027
  this.tooltips = [];
26103
27028
  this.panelBtns = /* @__PURE__ */ new Map();
@@ -26117,7 +27042,7 @@ var Topbar = class {
26117
27042
  this.timeframe = opts.timeframe;
26118
27043
  this.priceStyle = opts.priceStyle;
26119
27044
  const doc = host.ownerDocument;
26120
- injectStyles(STYLE_ID6, CSS3, doc);
27045
+ injectStyles(STYLE_ID7, CSS4, doc);
26121
27046
  this.el = doc.createElement("div");
26122
27047
  this.el.className = "vela-widget-topbar";
26123
27048
  this.symbolEl = doc.createElement("button");
@@ -26174,19 +27099,16 @@ var Topbar = class {
26174
27099
  this.tooltips.push(new Tooltip(this.styleButton, { content: "Chart style", triggerId: "vela-topbar-style", host }));
26175
27100
  if (this.layoutButton && opts.layout) {
26176
27101
  this.tooltips.push(new Tooltip(this.layoutButton, { content: "Layout", triggerId: "vela-topbar-layout", host }));
26177
- this.layoutMenu = new Menu({
27102
+ const layout = opts.layout;
27103
+ this.layoutPicker = new LayoutPicker({
26178
27104
  trigger: this.layoutButton,
26179
- triggerId: "vela-topbar-layout",
26180
27105
  host,
26181
- items: this.layoutItems(),
26182
- onSelect: (id) => {
26183
- if (id.startsWith("toggle:")) {
26184
- opts.layout.onToggle?.(id.slice("toggle:".length));
26185
- this.layoutMenu?.setItems(this.layoutItems());
26186
- return;
26187
- }
26188
- opts.layout.onSelect(id);
26189
- }
27106
+ shape: () => layout.shape(),
27107
+ presets: () => layout.presets().map((p) => ({ ...p, checked: p.id === this.layoutId })),
27108
+ onSelectGrid: (rows, cols) => layout.onSelectGrid(rows, cols),
27109
+ onSelectPreset: (id) => layout.onSelectPreset(id),
27110
+ syncs: () => layout.syncs(),
27111
+ onToggleSync: (id) => layout.onToggleSync(id)
26190
27112
  });
26191
27113
  }
26192
27114
  this.tfMenu = new Menu({
@@ -26225,7 +27147,7 @@ var Topbar = class {
26225
27147
  if (!this.layoutButton) return;
26226
27148
  this.layoutId = id;
26227
27149
  this.renderLayoutButton(this.layoutButton.ownerDocument);
26228
- this.layoutMenu?.setItems(this.layoutItems());
27150
+ this.layoutPicker?.refresh();
26229
27151
  }
26230
27152
  renderLayoutButton(doc) {
26231
27153
  if (!this.layoutButton) return;
@@ -26234,11 +27156,6 @@ var Topbar = class {
26234
27156
  else this.layoutButton.appendChild(doc.createTextNode(this.layoutId ?? ""));
26235
27157
  this.layoutButton.setAttribute("aria-label", `Layout \u2014 ${this.layoutId ?? ""}`);
26236
27158
  }
26237
- layoutItems() {
26238
- const presets = (this.opts.layout?.options() ?? []).map((l) => ({ id: l.id, label: l.label, checked: l.id === this.layoutId }));
26239
- const toggles = (this.opts.layout?.toggles?.() ?? []).map((t, i) => ({ id: `toggle:${t.id}`, label: t.label, checked: t.checked, toggle: true, separatorBefore: i === 0 }));
26240
- return [...presets, ...toggles];
26241
- }
26242
27159
  renderStyleButton(doc) {
26243
27160
  this.styleButton.replaceChildren();
26244
27161
  const icon2 = priceStyleIcon(this.priceStyle);
@@ -26303,7 +27220,7 @@ var Topbar = class {
26303
27220
  if (this.hairlineRaf) win?.cancelAnimationFrame(this.hairlineRaf);
26304
27221
  this.tfMenu.destroy();
26305
27222
  this.styleMenu.destroy();
26306
- this.layoutMenu?.destroy();
27223
+ this.layoutPicker?.destroy();
26307
27224
  for (const t of [...this.tooltips, ...this.panelTooltips]) t.destroy();
26308
27225
  this.el.remove();
26309
27226
  }
@@ -26405,8 +27322,8 @@ function tickerIconEl(doc, base, name, className) {
26405
27322
  }
26406
27323
 
26407
27324
  // src/widget/statusline.ts
26408
- var STYLE_ID7 = "vela-widget-statusline";
26409
- var CSS4 = `
27325
+ var STYLE_ID8 = "vela-widget-statusline";
27326
+ var CSS5 = `
26410
27327
  .vela-statusline {
26411
27328
  position: absolute;
26412
27329
  top: var(--vela-space-2);
@@ -26541,7 +27458,7 @@ var Statusline = class {
26541
27458
  /** 'ohlc' for bar-shaped styles; 'value' (the single plotted close) for line styles. */
26542
27459
  this.readout = "ohlc";
26543
27460
  const doc = host.ownerDocument;
26544
- injectStyles(STYLE_ID7, CSS4, doc);
27461
+ injectStyles(STYLE_ID8, CSS5, doc);
26545
27462
  host.classList.add("vela-has-statusline");
26546
27463
  this.el = doc.createElement("div");
26547
27464
  this.el.className = "vela-statusline";
@@ -26664,11 +27581,21 @@ var Statusline = class {
26664
27581
  };
26665
27582
 
26666
27583
  // src/widget/watermark.ts
26667
- var STYLE_ID8 = "vela-widget-watermark";
26668
- var CSS5 = `
27584
+ var MAX_FONT_PX = 72;
27585
+ var MIN_FONT_PX = 12;
27586
+ var FILL = 0.9;
27587
+ var STYLE_ID9 = "vela-widget-watermark";
27588
+ var CSS6 = `
26669
27589
  .vela-watermark {
26670
27590
  position: absolute;
26671
- inset: 0;
27591
+ /* Insets follow the renderer-published gutters (mount container), so the mark
27592
+ * centers and fits within the PLOT \u2014 never bleeding into the drawings toolbar
27593
+ * on the left or the price scale on the right (visible in small multi-chart
27594
+ * cells, where the scale is a large share of the width). */
27595
+ top: 0;
27596
+ bottom: 0;
27597
+ left: var(--vela-toolbar-gutter, 0px);
27598
+ right: var(--vela-scale-gutter, 0px);
26672
27599
  display: flex;
26673
27600
  align-items: center;
26674
27601
  justify-content: center;
@@ -26676,28 +27603,48 @@ var CSS5 = `
26676
27603
  z-index: 1;
26677
27604
  color: var(--vela-fg);
26678
27605
  opacity: 0.05;
26679
- font-size: clamp(28px, 7vw, 72px);
27606
+ font-size: ${MAX_FONT_PX}px;
26680
27607
  font-weight: 700;
26681
27608
  letter-spacing: 0.04em;
26682
27609
  user-select: none;
26683
27610
  white-space: nowrap;
26684
27611
  }
26685
27612
  `;
27613
+ function watermarkFontPx(availPx, textPxAtMax) {
27614
+ if (textPxAtMax <= 0) return MAX_FONT_PX;
27615
+ return Math.max(MIN_FONT_PX, Math.min(MAX_FONT_PX, Math.floor(MAX_FONT_PX * availPx * FILL / textPxAtMax)));
27616
+ }
26686
27617
  var Watermark = class {
26687
27618
  constructor(host, symbol, timeframe) {
26688
- injectStyles(STYLE_ID8, CSS5, host.ownerDocument);
27619
+ this.resizeObserver = null;
27620
+ injectStyles(STYLE_ID9, CSS6, host.ownerDocument);
26689
27621
  this.el = host.ownerDocument.createElement("div");
26690
27622
  this.el.className = "vela-watermark";
27623
+ this.text = host.ownerDocument.createElement("span");
27624
+ this.el.appendChild(this.text);
26691
27625
  host.appendChild(this.el);
27626
+ if (typeof ResizeObserver !== "undefined") {
27627
+ this.resizeObserver = new ResizeObserver(() => this.fit());
27628
+ this.resizeObserver.observe(this.el);
27629
+ }
26692
27630
  this.update(symbol, timeframe);
26693
27631
  }
26694
27632
  setVisible(visible) {
26695
27633
  this.el.style.display = visible ? "" : "none";
26696
27634
  }
26697
27635
  update(symbol, timeframe) {
26698
- this.el.textContent = symbol ? `${parseSymbol(symbol).ticker} \xB7 ${timeframeLabel(timeframe)}` : "";
27636
+ this.text.textContent = symbol ? `${parseSymbol(symbol).ticker} \xB7 ${timeframeLabel(timeframe)}` : "";
27637
+ this.fit();
27638
+ }
27639
+ /** Measure the text at the cap and shrink it to the chart's own width. */
27640
+ fit() {
27641
+ if (this.el.clientWidth <= 0 || !this.text.textContent) return;
27642
+ this.el.style.fontSize = `${MAX_FONT_PX}px`;
27643
+ const px = watermarkFontPx(this.el.clientWidth, this.text.getBoundingClientRect().width);
27644
+ if (px !== MAX_FONT_PX) this.el.style.fontSize = `${px}px`;
26699
27645
  }
26700
27646
  destroy() {
27647
+ this.resizeObserver?.disconnect();
26701
27648
  this.el.remove();
26702
27649
  }
26703
27650
  };
@@ -26723,8 +27670,8 @@ var RANGE_PRESETS = [
26723
27670
  { id: "ALL", tf: "W", preset: "ALL", bars: 5e3 }
26724
27671
  // everything the provider serves
26725
27672
  ];
26726
- var STYLE_ID9 = "vela-widget-bottombar";
26727
- var CSS6 = `
27673
+ var STYLE_ID10 = "vela-widget-bottombar";
27674
+ var CSS7 = `
26728
27675
  .vela-widget-bottombar {
26729
27676
  display: flex;
26730
27677
  align-items: center;
@@ -26799,7 +27746,7 @@ var Bottombar = class {
26799
27746
  this.timer = null;
26800
27747
  this.timezone = opts.timezone;
26801
27748
  const doc = host.ownerDocument;
26802
- injectStyles(STYLE_ID9, CSS6, doc);
27749
+ injectStyles(STYLE_ID10, CSS7, doc);
26803
27750
  this.el = doc.createElement("div");
26804
27751
  this.el.className = "vela-widget-bottombar";
26805
27752
  for (const preset of RANGE_PRESETS) {
@@ -26937,8 +27884,8 @@ function filterSymbols(list, query, limit = 100) {
26937
27884
  }
26938
27885
  return [...prefix, ...substr, ...desc, ...venue].slice(0, limit);
26939
27886
  }
26940
- var STYLE_ID10 = "vela-widget-symbolpicker";
26941
- var CSS7 = `
27887
+ var STYLE_ID11 = "vela-widget-symbolpicker";
27888
+ var CSS8 = `
26942
27889
  .vela-sp-searchrow {
26943
27890
  display: flex;
26944
27891
  align-items: center;
@@ -27028,7 +27975,7 @@ var SymbolPicker = class {
27028
27975
  this.seed = "";
27029
27976
  this.activeTab = "All";
27030
27977
  const doc = (opts.host ?? document.body).ownerDocument;
27031
- injectStyles(STYLE_ID10, CSS7, doc);
27978
+ injectStyles(STYLE_ID11, CSS8, doc);
27032
27979
  this.input = doc.createElement("input");
27033
27980
  this.input.className = "vela-sp-input";
27034
27981
  this.input.placeholder = "Search symbol\u2026";
@@ -27350,11 +28297,11 @@ function treeIsEmpty(panes) {
27350
28297
  }
27351
28298
 
27352
28299
  // src/widget/side-panel.ts
27353
- var STYLE_ID11 = "vela-widget-sidepanel";
28300
+ var STYLE_ID12 = "vela-widget-sidepanel";
27354
28301
  var DEFAULT_PANEL_WIDTH = 280;
27355
28302
  var DEFAULT_PANEL_MIN_WIDTH = 200;
27356
28303
  var DEFAULT_PANEL_MAX_WIDTH = 640;
27357
- var CSS8 = `
28304
+ var CSS9 = `
27358
28305
  .vela-panel[hidden] { display: none !important; }
27359
28306
  .vela-panel {
27360
28307
  position: relative;
@@ -27443,7 +28390,7 @@ var SidePanel = class {
27443
28390
  * a programmatic {@link setWidth}, so restoring a persisted width raises no change. */
27444
28391
  this.onWidthChange = null;
27445
28392
  const doc = host.ownerDocument;
27446
- injectStyles(STYLE_ID11, CSS8, doc);
28393
+ injectStyles(STYLE_ID12, CSS9, doc);
27447
28394
  this.minWidth = opts.resizable ? opts.minWidth ?? DEFAULT_PANEL_MIN_WIDTH : 1;
27448
28395
  this.maxWidth = opts.resizable ? opts.maxWidth ?? DEFAULT_PANEL_MAX_WIDTH : Number.POSITIVE_INFINITY;
27449
28396
  this.declaredWidth = clampPanelWidth(opts.width ?? DEFAULT_PANEL_WIDTH, this.minWidth, this.maxWidth);
@@ -27552,8 +28499,8 @@ var SidePanel = class {
27552
28499
  };
27553
28500
 
27554
28501
  // src/widget/object-tree.ts
27555
- var STYLE_ID12 = "vela-widget-objtree";
27556
- var CSS9 = `
28502
+ var STYLE_ID13 = "vela-widget-objtree";
28503
+ var CSS10 = `
27557
28504
  /* One pane's block. The transparent border reserves the drop-target outline. */
27558
28505
  .vela-ot-pane {
27559
28506
  border: 1px solid transparent;
@@ -27791,7 +28738,7 @@ var ObjectTree = class extends SidePanel {
27791
28738
  this.menu = null;
27792
28739
  this.menuActions = /* @__PURE__ */ new Map();
27793
28740
  this.drag = null;
27794
- injectStyles(STYLE_ID12, CSS9, host.ownerDocument);
28741
+ injectStyles(STYLE_ID13, CSS10, host.ownerDocument);
27795
28742
  this.body.addEventListener("contextmenu", (e) => this.onContextMenu(e));
27796
28743
  this.body.addEventListener("pointerdown", (e) => this.onPointerDown(e));
27797
28744
  }
@@ -28770,8 +29717,8 @@ var ObjectTree = class extends SidePanel {
28770
29717
  };
28771
29718
 
28772
29719
  // src/widget/data-window.ts
28773
- var STYLE_ID13 = "vela-widget-datawindow";
28774
- var CSS10 = `
29720
+ var STYLE_ID14 = "vela-widget-datawindow";
29721
+ var CSS11 = `
28775
29722
  .vela-dw-group {
28776
29723
  padding: 10px 8px 4px;
28777
29724
  margin-top: 4px;
@@ -28830,7 +29777,7 @@ var DataWindow = class extends SidePanel {
28830
29777
  super(host, "Data window", "vela-dw");
28831
29778
  this.chart = null;
28832
29779
  this.unsubs = [];
28833
- injectStyles(STYLE_ID13, CSS10, host.ownerDocument);
29780
+ injectStyles(STYLE_ID14, CSS11, host.ownerDocument);
28834
29781
  }
28835
29782
  toggle(open = this.el.hidden) {
28836
29783
  super.toggle(open);
@@ -29027,8 +29974,8 @@ var PanelDock = class {
29027
29974
  };
29028
29975
 
29029
29976
  // src/widget/shortcuts-help.ts
29030
- var STYLE_ID14 = "vela-widget-shortcuts";
29031
- var CSS11 = `
29977
+ var STYLE_ID15 = "vela-widget-shortcuts";
29978
+ var CSS12 = `
29032
29979
  .vela-sh-cat {
29033
29980
  color: var(--vela-fg-muted);
29034
29981
  font-size: var(--vela-font-size-sm);
@@ -29053,7 +30000,7 @@ var ShortcutsHelp = class {
29053
30000
  constructor(keymap, host, onOpenChange) {
29054
30001
  this.keymap = keymap;
29055
30002
  const doc = (host ?? document.body).ownerDocument;
29056
- injectStyles(STYLE_ID14, CSS11, doc);
30003
+ injectStyles(STYLE_ID15, CSS12, doc);
29057
30004
  this.list = doc.createElement("div");
29058
30005
  this.dialog = new Dialog({
29059
30006
  title: "Keyboard shortcuts",
@@ -29341,8 +30288,8 @@ var ChartContextMenu = class {
29341
30288
  };
29342
30289
 
29343
30290
  // src/widget/indicator-picker.ts
29344
- var STYLE_ID15 = "vela-widget-indpicker";
29345
- var CSS12 = `
30291
+ var STYLE_ID16 = "vela-widget-indpicker";
30292
+ var CSS13 = `
29346
30293
  .vela-ip-searchrow {
29347
30294
  display: flex;
29348
30295
  align-items: center;
@@ -29416,7 +30363,7 @@ var IndicatorPicker = class {
29416
30363
  this.isOpen = false;
29417
30364
  this.opts = opts;
29418
30365
  const doc = (opts.host ?? document.body).ownerDocument;
29419
- injectStyles(STYLE_ID15, CSS12, doc);
30366
+ injectStyles(STYLE_ID16, CSS13, doc);
29420
30367
  this.search = doc.createElement("input");
29421
30368
  this.search.className = "vela-ip-search";
29422
30369
  this.search.placeholder = "Search\u2026";
@@ -29544,8 +30491,8 @@ var IndicatorPicker = class {
29544
30491
  };
29545
30492
 
29546
30493
  // src/widget/timeframe-quick.ts
29547
- var STYLE_ID16 = "vela-widget-tfquick";
29548
- var CSS13 = `
30494
+ var STYLE_ID17 = "vela-widget-tfquick";
30495
+ var CSS14 = `
29549
30496
  .vela-tq-input {
29550
30497
  display: block;
29551
30498
  margin: 0 auto;
@@ -29568,7 +30515,7 @@ var CSS13 = `
29568
30515
  var TimeframeQuick = class {
29569
30516
  constructor(opts) {
29570
30517
  const doc = (opts.host ?? document.body).ownerDocument;
29571
- injectStyles(STYLE_ID16, CSS13, doc);
30518
+ injectStyles(STYLE_ID17, CSS14, doc);
29572
30519
  this.input = doc.createElement("input");
29573
30520
  this.input.className = "vela-tq-input";
29574
30521
  this.input.setAttribute("spellcheck", "false");
@@ -29765,7 +30712,7 @@ function sanitizeSync(raw) {
29765
30712
  if (raw == null || typeof raw !== "object") return null;
29766
30713
  const s = raw;
29767
30714
  const out = {};
29768
- for (const kind of ["viewport", "symbol", "timeframe", "crosshair"]) {
30715
+ for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) {
29769
30716
  const v = s[kind];
29770
30717
  if (v === true) out[kind] = true;
29771
30718
  else if (v != null && typeof v === "object") {
@@ -30004,8 +30951,8 @@ var WidgetHistory = class {
30004
30951
  };
30005
30952
 
30006
30953
  // src/widget/toast.ts
30007
- var STYLE_ID17 = "vela-widget-toast";
30008
- var CSS14 = `
30954
+ var STYLE_ID18 = "vela-widget-toast";
30955
+ var CSS15 = `
30009
30956
  .vela-toast {
30010
30957
  position: absolute;
30011
30958
  left: 50%;
@@ -30035,7 +30982,7 @@ var CSS14 = `
30035
30982
  var Toast = class {
30036
30983
  constructor(host) {
30037
30984
  this.timer = null;
30038
- injectStyles(STYLE_ID17, CSS14, host.ownerDocument);
30985
+ injectStyles(STYLE_ID18, CSS15, host.ownerDocument);
30039
30986
  this.el = host.ownerDocument.createElement("div");
30040
30987
  this.el.className = "vela-toast";
30041
30988
  host.appendChild(this.el);