@luxalgo/vela 0.6.5 → 0.6.6

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 (41) hide show
  1. package/README.md +16 -16
  2. package/dist/{DataProvider-DrN7ZIou.d.cts → DataProvider-DDUYw2qP.d.cts} +1 -1
  3. package/dist/{DataProvider-BmHkcwvJ.d.ts → DataProvider-JJq7M2it.d.ts} +1 -1
  4. package/dist/{history-CvOtsjT5.d.ts → bottombar-COnL2Sk4.d.ts} +110 -127
  5. package/dist/{history-BmyjaVir.d.cts → bottombar-DeDXbF_K.d.cts} +110 -127
  6. package/dist/{chunk-FQBT4NFM.js → chunk-JQFO2WMU.js} +2768 -2534
  7. package/dist/{chunk-BEJ57HP4.js → chunk-OEUGUXL7.js} +14 -1
  8. package/dist/{chunk-N6MBJHMV.js → chunk-TMXK2SJR.js} +5838 -3346
  9. package/dist/{contributions-2bEmCF9S.d.cts → contributions-BqCIsbaP.d.ts} +108 -4
  10. package/dist/{contributions-DatoAzMx.d.ts → contributions-DNMqrAuw.d.cts} +108 -4
  11. package/dist/index.cjs +2761 -2511
  12. package/dist/index.d.cts +40 -16
  13. package/dist/index.d.ts +40 -16
  14. package/dist/index.js +2 -2
  15. package/dist/{options-YdiaaVjt.d.cts → options-BaVTMXaO.d.cts} +48 -2
  16. package/dist/{options-YdiaaVjt.d.ts → options-BaVTMXaO.d.ts} +48 -2
  17. package/dist/{plugin-DO_DX0gp.d.cts → plugin-CN8U2__Q.d.cts} +6 -4
  18. package/dist/{plugin-Bcone6Vh.d.ts → plugin-WnvNNJOp.d.ts} +6 -4
  19. package/dist/plugin.cjs +16 -0
  20. package/dist/plugin.d.cts +4 -4
  21. package/dist/plugin.d.ts +4 -4
  22. package/dist/plugin.js +1 -1
  23. package/dist/providers/binance.d.cts +2 -2
  24. package/dist/providers/binance.d.ts +2 -2
  25. package/dist/providers/coinbase.d.cts +2 -2
  26. package/dist/providers/coinbase.d.ts +2 -2
  27. package/dist/providers/hyperliquid.d.cts +2 -2
  28. package/dist/providers/hyperliquid.d.ts +2 -2
  29. package/dist/ui.d.cts +1 -1
  30. package/dist/ui.d.ts +1 -1
  31. package/dist/vela.global.js +2762 -2512
  32. package/dist/vela.global.min.js +48 -48
  33. package/dist/widget.cjs +35560 -34095
  34. package/dist/widget.d.cts +34 -271
  35. package/dist/widget.d.ts +34 -271
  36. package/dist/widget.js +60 -1313
  37. package/dist/workspace.cjs +1438 -933
  38. package/dist/workspace.d.cts +111 -15
  39. package/dist/workspace.d.ts +111 -15
  40. package/dist/workspace.js +6 -2281
  41. package/package.json +1 -1
@@ -337,8 +337,12 @@ var BarStore = class {
337
337
  this.series = /* @__PURE__ */ new Map();
338
338
  /** Earliest bar-open time fetched for a series — what the cache actually covers. */
339
339
  this.coveredFrom = /* @__PURE__ */ new Map();
340
- /** Symbols protected from the current-symbol purge (multi-chart cells). Empty = legacy single-chart behavior. */
340
+ /** Symbols protected from the current-symbol purge (multi-chart cells) — the UNION
341
+ * of every owner's declaration. Empty = legacy single-chart behavior. */
341
342
  this.retained = /* @__PURE__ */ new Set();
343
+ /** Per-owner declarations behind {@link retained} — several shells on one page must
344
+ * not clobber (or, on destroy, evict) each other's protected symbols. */
345
+ this.retainedByOwner = /* @__PURE__ */ new Map();
342
346
  }
343
347
  get(key) {
344
348
  return this.series.get(key);
@@ -382,14 +386,21 @@ var BarStore = class {
382
386
  * Declare the set of symbols a multi-chart workspace is displaying (CANONICAL
383
387
  * tickers, post-registry resolution — `chart.data.resolve(sym).ticker`). These
384
388
  * survive every {@link retainSymbol} purge, so cells loading different symbols
385
- * stop evicting each other's history. Replaces the previous set and purges
386
- * anything now outside `symbols ∪ {currentSymbol}` immediately. An empty set
387
- * restores the legacy single-chart policy. Note: SECONDARY symbols a script
388
- * fetches (`request.security` cross-symbol) are not in this set and still drop
389
- * on cross-cell loads — correctness is unaffected (they re-fetch on demand).
389
+ * stop evicting each other's history. Replaces the previous set FOR THAT OWNER
390
+ * (pass the shell instance as `owner`; several shells on one page keep separate
391
+ * declarations, the effective set is their union) and purges anything now outside
392
+ * the union ∪ {currentSymbol} immediately. An empty set releases the owner's
393
+ * declaration — with no owners left, the legacy single-chart policy is back.
394
+ * Note: SECONDARY symbols a script fetches (`request.security` cross-symbol) are
395
+ * not in this set and still drop on cross-cell loads — correctness is unaffected
396
+ * (they re-fetch on demand).
390
397
  */
391
- retain(symbols) {
392
- this.retained = new Set(symbols);
398
+ retain(symbols, owner = "default") {
399
+ if (symbols.size === 0) this.retainedByOwner.delete(owner);
400
+ else this.retainedByOwner.set(owner, new Set(symbols));
401
+ const union = /* @__PURE__ */ new Set();
402
+ for (const set of this.retainedByOwner.values()) for (const s of set) union.add(s);
403
+ this.retained = union;
393
404
  this.purgeOutside(this.currentSymbol);
394
405
  }
395
406
  /** Drop every series whose symbol is neither `current` nor retained. */
@@ -4571,6 +4582,10 @@ function legendActionsProviderFor(chart, context) {
4571
4582
  return legendActions().filter((d) => !d.when || d.when(info)).map((d) => ({ id: d.id, icon: d.icon, tooltip: d.tooltip, run: () => d.run(context(), info) }));
4572
4583
  };
4573
4584
  }
4585
+ var stateHandlers = /* @__PURE__ */ new Map();
4586
+ function statePersistenceHandlers(scope) {
4587
+ return [...stateHandlers.values()].filter((h) => h.scope === scope);
4588
+ }
4574
4589
  var defaultEngines = /* @__PURE__ */ new Map();
4575
4590
  function resolveEngines(overrides) {
4576
4591
  return { ...Object.fromEntries(defaultEngines), ...overrides };
@@ -14212,10 +14227,11 @@ var MobileBar = class {
14212
14227
  const indicators = onIndicators ? item("vela-mb-indicators", "Indicators", onIndicators, "indicators") : null;
14213
14228
  this.actionsHost = doc.createElement("span");
14214
14229
  this.actionsHost.className = "vela-mb-actions";
14215
- const drawings = item("vela-mb-drawings", "Drawings", opts.onDrawingsClick, "pen");
14230
+ const onDrawings = opts.onDrawingsClick;
14231
+ const drawings = onDrawings ? item("vela-mb-drawings", "Drawings", onDrawings, "pen") : null;
14216
14232
  const more = item("vela-mb-more", "More", opts.onMoreClick, "kebab");
14217
14233
  const settings = item("vela-mb-settings", "Chart settings", opts.onSettingsClick, "gear");
14218
- this.el.append(this.symbolEl, this.tfEl, ...indicators ? [indicators] : [], this.actionsHost, drawings, more, settings);
14234
+ this.el.append(this.symbolEl, this.tfEl, ...indicators ? [indicators] : [], this.actionsHost, ...drawings ? [drawings] : [], more, settings);
14219
14235
  host.appendChild(this.el);
14220
14236
  this.renderActions();
14221
14237
  }
@@ -17030,6 +17046,35 @@ function rendererDefaults() {
17030
17046
  return Object.fromEntries(defaults);
17031
17047
  }
17032
17048
 
17049
+ // src/core/price-styles/heikin-ashi.ts
17050
+ function heikinAshiNext(raw, prevHa) {
17051
+ const haClose = (raw.open + raw.high + raw.low + raw.close) / 4;
17052
+ const haOpen = prevHa ? (prevHa.open + prevHa.close) / 2 : (raw.open + raw.close) / 2;
17053
+ return {
17054
+ time: raw.time,
17055
+ open: haOpen,
17056
+ high: Math.max(raw.high, haOpen, haClose),
17057
+ low: Math.min(raw.low, haOpen, haClose),
17058
+ close: haClose,
17059
+ ...raw.volume != null ? { volume: raw.volume } : {}
17060
+ };
17061
+ }
17062
+ function heikinAshiFull(raw) {
17063
+ const out = new Array(raw.length);
17064
+ let prev2;
17065
+ for (let i = 0; i < raw.length; i += 1) {
17066
+ prev2 = heikinAshiNext(raw[i], prev2);
17067
+ out[i] = prev2;
17068
+ }
17069
+ return out;
17070
+ }
17071
+
17072
+ // src/chart-types/builtins.ts
17073
+ var HEIKIN_ASHI = { full: heikinAshiFull, next: heikinAshiNext };
17074
+ function registerBuiltinChartTypes() {
17075
+ registerChartType({ id: "heikinashi", label: "Heikin Ashi", barTransform: HEIKIN_ASHI });
17076
+ }
17077
+
17033
17078
  // src/workspace/sync.ts
17034
17079
  function syncTargets(originId, setting, cellIds) {
17035
17080
  if (setting == null || setting === false) return [];
@@ -17089,8 +17134,18 @@ function sanitizeState(doc) {
17089
17134
  if (tracks) out.trackSizes = tracks;
17090
17135
  const panels2 = sanitizePanels(d.panels);
17091
17136
  if (panels2) out.panels = panels2;
17137
+ const ext = sanitizeExt(d.ext);
17138
+ if (ext) out.ext = ext;
17092
17139
  return out;
17093
17140
  }
17141
+ function sanitizeExt(raw) {
17142
+ if (raw == null || typeof raw !== "object" || Array.isArray(raw)) return null;
17143
+ const out = {};
17144
+ for (const [key, value] of Object.entries(raw)) {
17145
+ if (key.length > 0 && value !== void 0) out[key] = value;
17146
+ }
17147
+ return Object.keys(out).length > 0 ? out : null;
17148
+ }
17094
17149
  function sanitizeCell(raw) {
17095
17150
  if (raw == null || typeof raw !== "object") return null;
17096
17151
  const c = raw;
@@ -17112,6 +17167,8 @@ function sanitizeCell(raw) {
17112
17167
  const natives = Array.isArray(ind.natives) ? ind.natives.filter((n) => typeof n === "string") : [];
17113
17168
  out.indicators = { manifest, natives };
17114
17169
  }
17170
+ const ext = sanitizeExt(c.ext);
17171
+ if (ext) out.ext = ext;
17115
17172
  return out;
17116
17173
  }
17117
17174
  function sanitizeSync(raw) {
@@ -18444,7 +18501,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
18444
18501
  this.emitScriptRun(id, cause, first);
18445
18502
  },
18446
18503
  onAlert: (a) => {
18447
- this.events.emit("alert", a);
18504
+ const indicator = record.options?.title ?? record.prepared?.meta.title ?? record.title;
18505
+ this.events.emit("alert", { ...a, indicator });
18448
18506
  handle.emit("alert", { id: a.id, message: a.message, title: a.title, time: a.time });
18449
18507
  },
18450
18508
  onWarning: (w) => this.events.emit("warning", w),
@@ -18492,6 +18550,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
18492
18550
  overlay: d.overlay,
18493
18551
  paneHint: d.paneHint,
18494
18552
  native: { type: record.native.type },
18553
+ ...out.paneAxis != null ? { paneAxis: out.paneAxis } : {},
18495
18554
  series: out.series ?? [],
18496
18555
  fills: out.fills ?? [],
18497
18556
  backgrounds: out.backgrounds ?? [],
@@ -19086,6 +19145,24 @@ var RendererControl = class {
19086
19145
  this.renderer.setSettingsSections?.(sections);
19087
19146
  return this;
19088
19147
  }
19148
+ /**
19149
+ * Set the settings-dialog visibility policy: `hidden` lists setting ids to hide —
19150
+ * a tab (`'canvas'`), a group (`'canvas.grid'`), or a single row
19151
+ * (`'canvas.grid.vertical'`); an id hides its whole subtree, and a tab with nothing
19152
+ * left disappears from the rail. Presentation-only: hidden values keep being stored
19153
+ * and applied (e.g. hide `'advanced'` while forcing the widget's `bars` option).
19154
+ * Seeded from `VelaOptions.settings`; silent no-op without a settings dialog.
19155
+ */
19156
+ setSettingsVisibility(policy) {
19157
+ this.renderer.setSettingsVisibility?.(policy);
19158
+ return this;
19159
+ }
19160
+ /** Every addressable setting id of this chart (built-in tabs/groups/rows, chart-type
19161
+ * sections, host sections) — enumerate these to build a `hidden` list instead of
19162
+ * reading contributor source. Empty on a renderer without a settings dialog. */
19163
+ listSettingsIds() {
19164
+ return this.renderer.listSettingsIds?.() ?? [];
19165
+ }
19089
19166
  /** Tell the renderer's own chrome which size class the host shell is in —
19090
19167
  * `'mobile'` switches its dialogs/toolbars to the touch-first presentation.
19091
19168
  * Silent no-op on a renderer without adaptive chrome. */
@@ -21468,658 +21545,6 @@ function supportsWebGL2() {
21468
21545
  return webgl2Probe;
21469
21546
  }
21470
21547
 
21471
- // src/renderers/native/chrome/ticks.ts
21472
- function priceTicks(min, max, target = 6) {
21473
- if (!(max > min) || !Number.isFinite(min) || !Number.isFinite(max)) return [];
21474
- const raw = (max - min) / Math.max(1, target);
21475
- const mag = Math.pow(10, Math.floor(Math.log10(raw)));
21476
- const norm = raw / mag;
21477
- const step = (norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10) * mag;
21478
- const decimals = Math.max(0, -Math.floor(Math.log10(step)) + 1);
21479
- const out = [];
21480
- const start = Math.ceil(min / step) * step;
21481
- for (let v = start; v <= max + step * 1e-6; v += step) {
21482
- out.push(Number(v.toFixed(decimals)));
21483
- }
21484
- return out;
21485
- }
21486
- function priceDecimals(min, max, target = 6) {
21487
- if (!(max > min)) return 2;
21488
- const raw = (max - min) / Math.max(1, target);
21489
- const mag = Math.pow(10, Math.floor(Math.log10(raw)));
21490
- const norm = raw / mag;
21491
- const step = (norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10) * mag;
21492
- return Math.max(0, Math.min(8, -Math.floor(Math.log10(step)) + 1));
21493
- }
21494
- function logPriceTicks(min, max, target = 6) {
21495
- if (min <= 0 || !(max > min) || !Number.isFinite(min) || !Number.isFinite(max)) return [];
21496
- if (Math.log10(max) - Math.log10(min) < 1.1) return priceTicks(min, max, target);
21497
- const out = [];
21498
- const startExp = Math.floor(Math.log10(min));
21499
- const endExp = Math.ceil(Math.log10(max));
21500
- for (let e = startExp; e <= endExp; e += 1) {
21501
- for (const m of [1, 2, 5]) {
21502
- const v = m * Math.pow(10, e);
21503
- if (v >= min && v <= max) out.push(v);
21504
- }
21505
- }
21506
- return out;
21507
- }
21508
- function valueDecimals(v) {
21509
- const a = Math.abs(v);
21510
- if (a >= 100) return 0;
21511
- if (a >= 1) return 2;
21512
- if (a >= 0.01) return 4;
21513
- return 6;
21514
- }
21515
- function tickDecimals(tick) {
21516
- if (!(tick > 0) || !Number.isFinite(tick)) return 2;
21517
- for (let d = 0; d <= 8; d += 1) {
21518
- if (Math.abs(Number(tick.toFixed(d)) - tick) <= tick * 1e-6) return d;
21519
- }
21520
- return 8;
21521
- }
21522
- function axisDecimals(scale, heightPx, mintick) {
21523
- const d = mintick != null && mintick > 0 ? tickDecimals(mintick) : priceDecimals(scale.min, scale.max, tickCount(heightPx));
21524
- return d === 0 ? 2 : d;
21525
- }
21526
- function tickCount(paneHeightPx) {
21527
- return Math.max(2, Math.min(16, Math.round(paneHeightPx / 50)));
21528
- }
21529
- function paneTicks(scale, heightPx) {
21530
- return scale.log ? logPriceTicks(scale.min, scale.max, tickCount(heightPx)) : priceTicks(scale.min, scale.max, tickCount(heightPx));
21531
- }
21532
- function toPct(price, baseline) {
21533
- return (price / baseline - 1) * 100;
21534
- }
21535
- function toIndex(price, baseline) {
21536
- return price / baseline * 100;
21537
- }
21538
- function formatPct(pct) {
21539
- const sign = pct >= 0 ? "+" : "-";
21540
- return `${sign}${Math.abs(pct).toFixed(2)}%`;
21541
- }
21542
- function formatIndex(idx) {
21543
- return idx.toFixed(2);
21544
- }
21545
- function formatCompactValue(v) {
21546
- const a = Math.abs(v);
21547
- if (a >= 1e9) return `${trimZeros(v / 1e9)}B`;
21548
- if (a >= 1e6) return `${trimZeros(v / 1e6)}M`;
21549
- if (a >= 1e3) return `${trimZeros(v / 1e3)}K`;
21550
- return trimZeros(v);
21551
- }
21552
- function trimZeros(v) {
21553
- return Number(v.toFixed(2)).toString();
21554
- }
21555
- function paneAxisTicks(scale, heightPx, pct, mintick, format) {
21556
- if (pct) {
21557
- const { baseline, indexed } = pct;
21558
- const lo = Math.min(scale.min, scale.max);
21559
- const hi = Math.max(scale.min, scale.max);
21560
- if (indexed) {
21561
- const iLo = toIndex(lo, baseline);
21562
- const iHi = toIndex(hi, baseline);
21563
- return priceTicks(iLo, iHi, tickCount(heightPx)).map((idx) => ({ price: baseline * idx / 100, label: formatIndex(idx) }));
21564
- }
21565
- const pLo = toPct(lo, baseline);
21566
- const pHi = toPct(hi, baseline);
21567
- return priceTicks(pLo, pHi, tickCount(heightPx)).map((p) => ({ price: baseline * (1 + p / 100), label: formatPct(p) }));
21568
- }
21569
- if (format === "volume") {
21570
- return paneTicks(scale, heightPx).map((price) => ({ price, label: formatCompactValue(price) }));
21571
- }
21572
- return paneTicks(scale, heightPx).map((price) => ({ price, label: formatPriceLabel(scale, heightPx, price, mintick) }));
21573
- }
21574
- function formatAxisValue(scale, heightPx, value, pct, mintick, format) {
21575
- if (pct) return pct.indexed ? formatIndex(toIndex(value, pct.baseline)) : formatPct(toPct(value, pct.baseline));
21576
- if (format === "volume") return formatCompactValue(value);
21577
- return formatPriceLabel(scale, heightPx, value, mintick);
21578
- }
21579
- function formatPriceLabel(scale, heightPx, value, mintick) {
21580
- const wideLog = scale.log && Math.log10(scale.max) - Math.log10(scale.min) >= 1.1;
21581
- if (wideLog) {
21582
- const d = valueDecimals(value);
21583
- return value.toFixed(d === 0 ? 2 : d);
21584
- }
21585
- return value.toFixed(axisDecimals(scale, heightPx, mintick));
21586
- }
21587
- var SEC = 1e3;
21588
- var MIN = 60 * SEC;
21589
- var HOUR = 60 * MIN;
21590
- var DAY = 24 * HOUR;
21591
- var WEEK = 7 * DAY;
21592
- var MONTH = 30 * DAY;
21593
- var YEAR = 365 * DAY;
21594
- var STEP_LADDER = [
21595
- SEC,
21596
- 5 * SEC,
21597
- 15 * SEC,
21598
- 30 * SEC,
21599
- MIN,
21600
- 5 * MIN,
21601
- 15 * MIN,
21602
- 30 * MIN,
21603
- HOUR,
21604
- 2 * HOUR,
21605
- 4 * HOUR,
21606
- 6 * HOUR,
21607
- 12 * HOUR,
21608
- DAY,
21609
- 2 * DAY,
21610
- WEEK,
21611
- MONTH,
21612
- 3 * MONTH,
21613
- YEAR
21614
- ];
21615
- var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
21616
- var pad2 = (n) => n < 10 ? `0${n}` : String(n);
21617
- function pickStep(targetMs) {
21618
- for (const step of STEP_LADDER) if (step >= targetMs) return step;
21619
- return STEP_LADDER[STEP_LADDER.length - 1];
21620
- }
21621
- function timeTicks(fromMs, toMs, target = 8, offsetMs = 0) {
21622
- const span = toMs - fromMs;
21623
- if (!(span > 0)) return [];
21624
- const step = pickStep(span / Math.max(1, target));
21625
- const zFrom = fromMs + offsetMs;
21626
- const zTo = toMs + offsetMs;
21627
- const first = Math.ceil(zFrom / step) * step;
21628
- const out = [];
21629
- for (let zt = first; zt <= zTo; zt += step) {
21630
- const t = zt - offsetMs;
21631
- const d = new Date(zt);
21632
- let label;
21633
- let major = false;
21634
- if (step < DAY) {
21635
- const h = d.getUTCHours();
21636
- const m = d.getUTCMinutes();
21637
- if (h === 0 && m === 0) {
21638
- label = `${MONTHS[d.getUTCMonth()]} ${d.getUTCDate()}`;
21639
- major = true;
21640
- } else {
21641
- label = `${pad2(h)}:${pad2(m)}`;
21642
- }
21643
- } else if (step < YEAR) {
21644
- label = `${MONTHS[d.getUTCMonth()]} ${d.getUTCDate()}`;
21645
- if (d.getUTCDate() === 1) {
21646
- label = MONTHS[d.getUTCMonth()];
21647
- major = true;
21648
- }
21649
- } else {
21650
- label = String(d.getUTCFullYear());
21651
- major = true;
21652
- }
21653
- out.push({ time: t, label, major });
21654
- }
21655
- return out;
21656
- }
21657
-
21658
- // src/renderers/shared/trade-markers.ts
21659
- var TRADE_LONG_COLOR = TRADE_LONG;
21660
- var TRADE_SHORT_COLOR = TRADE_SHORT;
21661
- var TRADE_EXIT_COLOR = TRADE_EXIT;
21662
- function defaultTradeMarkersState() {
21663
- return {
21664
- visible: true,
21665
- labels: true,
21666
- qty: true,
21667
- colors: { long: TRADE_LONG_COLOR, short: TRADE_SHORT_COLOR, exit: TRADE_EXIT_COLOR }
21668
- };
21669
- }
21670
- function mergeTradeMarkersState(base, patch) {
21671
- const p = patch && typeof patch === "object" ? patch : {};
21672
- const c = p.colors && typeof p.colors === "object" ? p.colors : {};
21673
- const bool = (v, fb) => typeof v === "boolean" ? v : fb;
21674
- const color = (v, fb) => typeof v === "string" && v.trim().length > 0 ? v : fb;
21675
- return {
21676
- visible: bool(p.visible, base.visible),
21677
- labels: bool(p.labels, base.labels),
21678
- qty: bool(p.qty, base.qty),
21679
- colors: {
21680
- long: color(c.long, base.colors.long),
21681
- short: color(c.short, base.colors.short),
21682
- exit: color(c.exit, base.colors.exit)
21683
- }
21684
- };
21685
- }
21686
- var BAR_GAP = 10;
21687
- var ARROW_W = 9;
21688
- var HEAD_H = 6;
21689
- var ARROW_H = 14;
21690
- var STEM_W = 3;
21691
- var CAP_H = 2;
21692
- var CAP_GAP = 2;
21693
- var TEXT_GAP = 3;
21694
- var UNIT_GAP = 6;
21695
- var TICK_W = 6;
21696
- var TICK_H = 8;
21697
- function lineHeightOf(fontSize) {
21698
- return fontSize + 4;
21699
- }
21700
- function qtyText(exec) {
21701
- if (exec.qty == null || !Number.isFinite(exec.qty)) return null;
21702
- const magnitude = Number(Math.abs(exec.qty).toFixed(8));
21703
- return `${exec.side === "buy" ? "+" : "-"}${magnitude}`;
21704
- }
21705
- function unitOf(exec, state, lineH) {
21706
- const lines = [];
21707
- if (state.labels && exec.label) lines.push(exec.label);
21708
- if (state.qty) {
21709
- const q = qtyText(exec);
21710
- if (q) lines.push(q);
21711
- }
21712
- return { exec, lines, height: ARROW_H + (lines.length ? TEXT_GAP + lines.length * lineH : 0) };
21713
- }
21714
- function stacksFor(trades, state, deps, from, to, lineH) {
21715
- const byBar = /* @__PURE__ */ new Map();
21716
- for (const exec of trades) {
21717
- const logical = Math.round(deps.timeToLogical(exec.time));
21718
- if (logical < from || logical > to) continue;
21719
- let stack = byBar.get(logical);
21720
- if (!stack) {
21721
- stack = { logical, buys: [], sells: [] };
21722
- byBar.set(logical, stack);
21723
- }
21724
- (exec.side === "buy" ? stack.buys : stack.sells).push(unitOf(exec, state, lineH));
21725
- }
21726
- return [...byBar.values()];
21727
- }
21728
- function stackExtent(units) {
21729
- if (units.length === 0) return 0;
21730
- let px = BAR_GAP;
21731
- for (const u of units) px += u.height;
21732
- return px + (units.length - 1) * UNIT_GAP;
21733
- }
21734
- function tradesPriceHints(trades, state, deps, from, to, fontSize) {
21735
- if (trades.length === 0) return null;
21736
- const lineH = lineHeightOf(fontSize);
21737
- let min = Infinity;
21738
- let max = -Infinity;
21739
- let abovePx = 0;
21740
- let belowPx = 0;
21741
- for (const stack of stacksFor(trades, state, deps, Math.floor(from), Math.ceil(to), lineH)) {
21742
- const bar = deps.barAt(stack.logical);
21743
- if (!bar) continue;
21744
- if (bar.low < min) min = bar.low;
21745
- if (bar.high > max) max = bar.high;
21746
- belowPx = Math.max(belowPx, stackExtent(stack.buys));
21747
- abovePx = Math.max(abovePx, stackExtent(stack.sells));
21748
- }
21749
- if (!Number.isFinite(min) || !Number.isFinite(max)) return null;
21750
- return { min, max, abovePx, belowPx };
21751
- }
21752
- function renderTradeMarkers(ctx, trades, state, deps, xOf, yOf, text, width, barHalfPx) {
21753
- if (trades.length === 0) return;
21754
- const lineH = lineHeightOf(text.fontSize);
21755
- const stacks = stacksFor(trades, state, deps, -Infinity, Infinity, lineH);
21756
- if (stacks.length === 0) return;
21757
- ctx.save();
21758
- ctx.font = `${text.fontSize}px ${text.fontFamily}`;
21759
- ctx.textAlign = "center";
21760
- ctx.textBaseline = "middle";
21761
- for (const stack of stacks) {
21762
- const x = xOf(stack.logical);
21763
- if (x < -150 || x > width + 150) continue;
21764
- const bar = deps.barAt(stack.logical);
21765
- if (!bar) continue;
21766
- const yBottom = Math.max(yOf(bar.low), yOf(bar.high));
21767
- const yTop = Math.min(yOf(bar.low), yOf(bar.high));
21768
- let y = yBottom + BAR_GAP;
21769
- for (const unit of stack.buys) {
21770
- ctx.fillStyle = colorOf(unit.exec, state.colors);
21771
- drawArrowUp(ctx, x, y, unit.exec.kind === "exit");
21772
- drawFillTick(ctx, unit.exec.side, x, yOf(unit.exec.price), barHalfPx);
21773
- drawTextLines(ctx, unit.lines, x, y + ARROW_H + TEXT_GAP + lineH / 2, lineH, text.color);
21774
- y += unit.height + UNIT_GAP;
21775
- }
21776
- y = yTop - BAR_GAP;
21777
- for (const unit of stack.sells) {
21778
- ctx.fillStyle = colorOf(unit.exec, state.colors);
21779
- drawArrowDown(ctx, x, y, unit.exec.kind === "exit");
21780
- drawFillTick(ctx, unit.exec.side, x, yOf(unit.exec.price), barHalfPx);
21781
- drawTextLines(ctx, unit.lines, x, y - ARROW_H - TEXT_GAP - lineH / 2, -lineH, text.color);
21782
- y -= unit.height + UNIT_GAP;
21783
- }
21784
- }
21785
- ctx.restore();
21786
- }
21787
- function colorOf(exec, colors) {
21788
- if (exec.kind === "exit") return colors.exit;
21789
- return exec.side === "buy" ? colors.long : colors.short;
21790
- }
21791
- function drawArrowUp(ctx, x, yTip, capped) {
21792
- ctx.beginPath();
21793
- ctx.moveTo(x, yTip);
21794
- ctx.lineTo(x - ARROW_W / 2, yTip + HEAD_H);
21795
- ctx.lineTo(x + ARROW_W / 2, yTip + HEAD_H);
21796
- ctx.closePath();
21797
- ctx.fill();
21798
- ctx.fillRect(x - STEM_W / 2, yTip + HEAD_H, STEM_W, ARROW_H - HEAD_H);
21799
- if (capped) ctx.fillRect(x - ARROW_W / 2, yTip - CAP_GAP - CAP_H, ARROW_W, CAP_H);
21800
- }
21801
- function drawArrowDown(ctx, x, yTip, capped) {
21802
- ctx.beginPath();
21803
- ctx.moveTo(x, yTip);
21804
- ctx.lineTo(x - ARROW_W / 2, yTip - HEAD_H);
21805
- ctx.lineTo(x + ARROW_W / 2, yTip - HEAD_H);
21806
- ctx.closePath();
21807
- ctx.fill();
21808
- ctx.fillRect(x - STEM_W / 2, yTip - ARROW_H, STEM_W, ARROW_H - HEAD_H);
21809
- if (capped) ctx.fillRect(x - ARROW_W / 2, yTip + CAP_GAP, ARROW_W, CAP_H);
21810
- }
21811
- function drawFillTick(ctx, side, x, yFill, barHalfPx) {
21812
- const edge = side === "buy" ? x - barHalfPx : x + barHalfPx;
21813
- const back = side === "buy" ? edge - TICK_W : edge + TICK_W;
21814
- ctx.beginPath();
21815
- ctx.moveTo(edge, yFill);
21816
- ctx.lineTo(back, yFill - TICK_H / 2);
21817
- ctx.lineTo(back, yFill + TICK_H / 2);
21818
- ctx.closePath();
21819
- ctx.fill();
21820
- }
21821
- function drawTextLines(ctx, lines, x, firstY, step, color) {
21822
- if (lines.length === 0) return;
21823
- ctx.fillStyle = color;
21824
- for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], x, firstY + i * step);
21825
- }
21826
-
21827
- // src/renderers/native/core/SceneGraph.ts
21828
- var SceneGraph = class {
21829
- constructor() {
21830
- this.panes = /* @__PURE__ */ new Map();
21831
- this.indicators = /* @__PURE__ */ new Map();
21832
- this.bars = [];
21833
- /** Volume-layer config pushed by the volume native indicator (null ⇒ layer off). Ephemeral. */
21834
- this.volumeLayer = null;
21835
- /** Generic native-data channels for SDK renderer layers (`setNativeData(id, …)`). Ephemeral. */
21836
- this.nativeData = /* @__PURE__ */ new Map();
21837
- /** Loading ranges per channel (`setNativeData(id + '-pending', …)`). Ephemeral. */
21838
- this.nativePending = /* @__PURE__ */ new Map();
21839
- /** VPVR-layer config pushed by the VPVR native indicator (null ⇒ layer off). Ephemeral. */
21840
- this.vpvrLayer = null;
21841
- this.crosshair = null;
21842
- /** How the base price series is drawn on the price pane (candles by default). */
21843
- this.priceStyle = "candles";
21844
- /** Price-series base painting for the ACTIVE style (see ChartTypeDefinition.basePainting). */
21845
- this.basePainting = "candles";
21846
- /** The ACTIVE style's own candle cosmetics (`chartTypes.<id>.candle*`) when it is a
21847
- * candle-based plugin type; null ⇒ paint with the shared `style.candle` block. */
21848
- this.candleOverride = null;
21849
- /** Explicit baseline reference price for `priceStyle:'baseline'`; when null the
21850
- * baseline follows `style.baseline.baselineLevel` as a percent of the visible pane
21851
- * range (resolved per frame via `baselinePriceFor`). */
21852
- this.baselineValue = null;
21853
- /** Draw the dashed horizontal line at the latest price (price pane). Independent
21854
- * of the axis label chip (`showPriceLabel`) — either can show without the other. */
21855
- this.showPriceLine = true;
21856
- /** Draw the last-price label chip on the price axis. Independent of the line. */
21857
- this.showPriceLabel = true;
21858
- /** Draw the countdown-to-bar-close chip on the price axis. When the price label is
21859
- * also shown, the two merge into one stacked block (countdown under the label);
21860
- * when either shows alone it's centered on the latest price level. */
21861
- this.showCountdown = true;
21862
- /** Logarithmic price scale on the price pane. */
21863
- this.logScale = false;
21864
- /** Inverted price axis on the price pane (high at the bottom). Study panes carry their own. */
21865
- this.invertScale = false;
21866
- /** Exchange tick size for the active symbol (e.g. 0.01), when known. Drives the
21867
- * price-axis decimals — the instrument's true precision instead of the zoom-derived
21868
- * formula. Undefined until symbol metadata loads (the formula is the fallback). */
21869
- this.priceMintick = void 0;
21870
- /** Price-axis mode on the price pane: `'price'` (absolute) or `'percent'` (change
21871
- * vs `percentBaseline`). Gridlines, axis labels and crosshair chip all follow it. */
21872
- this.scaleMode = "price";
21873
- /** Reference price for percent mode (first visible bar's close); recomputed per frame. */
21874
- this.percentBaseline = 0;
21875
- /** IANA time zone for the time axis + crosshair/data-window stamps (`'UTC'` default). */
21876
- this.timezone = "UTC";
21877
- /** Draw the background gridlines (price + time). Master toggle (`gridlines`
21878
- * feature); per-axis visibility + colors live in `style.gridVert`/`gridHorz`. */
21879
- this.showGrid = true;
21880
- /** Comprehensive cosmetic config (item 15): grid colors, crosshair, candle
21881
- * border/wick, fonts, separators. Serialized via the renderer's `getConfig()`/
21882
- * `applyConfig()`; every draw layer reads its knobs from here, falling back to
21883
- * the theme for any value left at its inherit default. */
21884
- this.style = defaultChartStyle();
21885
- /** Draw the price/time axis tick labels. */
21886
- this.showAxisLabels = true;
21887
- /** Strategy trade-marker display (the `tradeMarkers` feature): master toggle, the
21888
- * two text lines, and the palette. Trade markers always paint on the price pane. */
21889
- this.tradeMarkers = defaultTradeMarkersState();
21890
- /** Renderer-owned shaded time bands (session highlighting), behind grid + data. */
21891
- this.highlights = [];
21892
- /** Pre/post-market bands pushed by the host (`sessionZones` feature); null ⇒ no sessions. */
21893
- this.sessionZones = null;
21894
- /** Draw-order key of the price candles, relative to indicator series z (see `seriesZ`).
21895
- * Indicators with z below this draw BEHIND the candles; at/above draw in front.
21896
- * Default 0 with indicators mounting at z < 0 ⇒ the price reads on top of every overlay,
21897
- * and user drawings (z ≥ 1 by default) on top of the price. */
21898
- this.candleZ = 0;
21899
- /** Hide the base price series (candles/bars/line/area) without removing it — overlay
21900
- * indicators keep drawing and the pane autoscales to them. Toggled from the object tree. */
21901
- this.candlesHidden = false;
21902
- /** Per-indicator foreground draw-order key (series layer), keyed by indicator id.
21903
- * Higher = drawn later (in front). Assigned on mount to the current BOTTOM of the stack,
21904
- * so each indicator arrives behind the candles (and behind older indicators);
21905
- * `setIndicatorZ`/`bringToFront`/`sendToBack` change it. */
21906
- this.seriesZ = /* @__PURE__ */ new Map();
21907
- /** Per-pane raster layers of user drawings interleaved into the series stack — each is a
21908
- * prepainted canvas the backend composites just before the series carrying `beforeZ`.
21909
- * Rebuilt by the renderer per data frame; empty when every drawing sits over the stack. */
21910
- this.drawingSlices = /* @__PURE__ */ new Map();
21911
- /** Per-model index offset: the chart bar index of the model's `anchorTime` — its
21912
- * index-aligned payloads (dense series arrays, `bar_index` drawings) count from that
21913
- * bar. Only nonzero for models computed over a SUFFIX of the bars (whole-chart models,
21914
- * the norm, aren't stored). Recomputed by the renderer on setBars + mount/patch. */
21915
- this.anchorOffsets = /* @__PURE__ */ new Map();
21916
- /** Per-indicator private price windows (merged indicators drawn on their own scale
21917
- * column). Populated per frame for models flagged `ownScale`; absent ⇒ the model
21918
- * shares its pane's master scale. */
21919
- this.indicatorScales = /* @__PURE__ */ new Map();
21920
- /** Cached sort of `panes` by order; invalidated on add/remove/reorder. */
21921
- this.orderedCache = null;
21922
- }
21923
- /** Panes sorted top-to-bottom by `order`. Cached — callers must NOT mutate the array. */
21924
- orderedPanes() {
21925
- if (!this.orderedCache) this.orderedCache = [...this.panes.values()].sort((a, b) => a.order - b.order);
21926
- return this.orderedCache;
21927
- }
21928
- /** The session zones resolved into colored bands (pre/post-market washes from the
21929
- * config's session colors) — consumed by the same painting path as {@link highlights}. */
21930
- sessionHighlightBands() {
21931
- if (!this.sessionZones) return [];
21932
- const out = [];
21933
- for (const [from, to] of this.sessionZones.pre) out.push({ from, to, color: this.style.sessions.premarketColor });
21934
- for (const [from, to] of this.sessionZones.post) out.push({ from, to, color: this.style.sessions.postmarketColor });
21935
- return out;
21936
- }
21937
- indicatorsForPane(paneId) {
21938
- const out = [];
21939
- for (const model of this.indicators.values()) if (model.paneId === paneId) out.push(model);
21940
- return out;
21941
- }
21942
- /** Merged (own-scale) indicators on a pane, ordered by z — one axis column each. */
21943
- ownScaleIndicatorsForPane(paneId) {
21944
- return this.orderedIndicatorsForPane(paneId).filter((m) => m.ownScale === true);
21945
- }
21946
- /** Ensure a merged indicator has a private scale slot (seeded from the pane if given). */
21947
- ensureIndicatorScale(id, seed) {
21948
- let s = this.indicatorScales.get(id);
21949
- if (!s) {
21950
- const base = seed ?? { min: 0, max: 1 };
21951
- s = { scale: { ...base }, scaleTarget: { ...base }, initialized: false, manualScale: null };
21952
- this.indicatorScales.set(id, s);
21953
- }
21954
- return s;
21955
- }
21956
- dropIndicatorScale(id) {
21957
- this.indicatorScales.delete(id);
21958
- }
21959
- /** The price window a model renders on: its own scale when merged (`ownScale`), else the pane's. */
21960
- scaleFor(model, pane) {
21961
- if (model.ownScale === true) {
21962
- const s = this.indicatorScales.get(model.id);
21963
- if (s) return s.scale;
21964
- }
21965
- return pane.scale;
21966
- }
21967
- /** Apply a new top-to-bottom pane order (ids not present are ignored). */
21968
- orderPanes(orderedIds) {
21969
- orderedIds.forEach((id, i) => {
21970
- const pane = this.panes.get(id);
21971
- if (pane) pane.order = i;
21972
- });
21973
- this.orderedCache = null;
21974
- }
21975
- /** Indicators on a pane sorted by foreground z (ascending). Array#sort is stable,
21976
- * so equal-z models keep their insertion order (the default). */
21977
- orderedIndicatorsForPane(paneId) {
21978
- return this.indicatorsForPane(paneId).sort((a, b) => this.zOf(a.id) - this.zOf(b.id));
21979
- }
21980
- /** The foreground draw-order key of an indicator (0 when never assigned). */
21981
- zOf(id) {
21982
- return this.seriesZ.get(id) ?? 0;
21983
- }
21984
- /** The model's index offset: chart bar index its index-aligned payloads count from (0 = whole-chart). */
21985
- offsetOf(id) {
21986
- return this.anchorOffsets.get(id) ?? 0;
21987
- }
21988
- /** Offsets are SIGNED. Positive: the model starts after the chart's first bar (it ran
21989
- * over a suffix) — readers skip its leading chart bars. Negative: the model starts
21990
- * BEFORE it (the chart's head moved forward under a mounted model) — readers skip the
21991
- * model's own leading points, `points[i - off]` reaching further in. Storing only the
21992
- * positive case silently pinned such a model at index 0, i.e. drew it shifted. */
21993
- setAnchorOffset(id, offset) {
21994
- if (offset !== 0 && Number.isFinite(offset)) this.anchorOffsets.set(id, offset);
21995
- else this.anchorOffsets.delete(id);
21996
- }
21997
- forgetAnchorOffset(id) {
21998
- this.anchorOffsets.delete(id);
21999
- }
22000
- /** Resolve the baseline reference price for the given pane window: the explicit
22001
- * `baselineValue` when set, else `style.baseline.baselineLevel` as the price that sits
22002
- * at that fraction of the pane height. Interpolated in the same space the pane renders
22003
- * in (log when `scale.log`, else linear) so `level%` always lands at `level%` of the
22004
- * height — matching `CoordinateSystem.yToPrice`. */
22005
- baselinePriceFor(scale) {
22006
- if (this.baselineValue != null) return this.baselineValue;
22007
- const t = this.style.baseline.baselineLevel / 100;
22008
- if (scale.log && scale.min > 0 && scale.max > scale.min) {
22009
- const lo = Math.log(scale.min);
22010
- return Math.exp(lo + t * (Math.log(scale.max) - lo));
22011
- }
22012
- return scale.min + (scale.max - scale.min) * t;
22013
- }
22014
- /** Assign a default z on mount: the current bottom of the stack, so a new indicator
22015
- * paints behind the candles and behind every indicator already there — the price stays
22016
- * the top of the pile until the user restacks it. No-op if the indicator already has one. */
22017
- assignIndicatorZ(id) {
22018
- if (!this.seriesZ.has(id)) this.seriesZ.set(id, this.bottomZ() - 1);
22019
- }
22020
- /** Mount-time default for a LAYER-BACKED native (it paints on a canvas stacked above the
22021
- * data canvas by default): top of the stack, so the recorded order tells the truth from
22022
- * the first frame. Keeps an existing key, so a restored stack survives the remount. */
22023
- assignIndicatorZTop(id) {
22024
- if (!this.seriesZ.has(id)) this.seriesZ.set(id, this.topZ() + 1);
22025
- }
22026
- forgetIndicatorZ(id) {
22027
- this.seriesZ.delete(id);
22028
- }
22029
- setIndicatorZ(id, z) {
22030
- this.seriesZ.set(id, z);
22031
- }
22032
- /** Snapshot of the current ordering for a UI/read API: `{ id, z }` sorted by z. */
22033
- indicatorZOrder() {
22034
- return [...this.seriesZ.entries()].map(([id, z]) => ({ id, z })).sort((a, b) => a.z - b.z);
22035
- }
22036
- /** The pane's series z keys (each indicator, plus the candles on the price pane), sorted
22037
- * ascending and de-duplicated — the boundaries a user drawing's z is slotted against. */
22038
- seriesBoundaries(paneId) {
22039
- const keys = /* @__PURE__ */ new Set();
22040
- if (paneId === "price") keys.add(this.candleZ);
22041
- for (const m of this.indicatorsForPane(paneId)) keys.add(this.zOf(m.id));
22042
- return [...keys].sort((a, b) => a - b);
22043
- }
22044
- /** Raise an indicator above every other layer (other indicators AND the candles). */
22045
- bringIndicatorToFront(id) {
22046
- this.seriesZ.set(id, this.topZ() + 1);
22047
- }
22048
- /** Drop an indicator below every other layer (other indicators AND the candles). */
22049
- sendIndicatorToBack(id) {
22050
- this.seriesZ.set(id, this.bottomZ() - 1);
22051
- }
22052
- topZ() {
22053
- let max = this.candleZ;
22054
- for (const z of this.seriesZ.values()) if (z > max) max = z;
22055
- return max;
22056
- }
22057
- bottomZ() {
22058
- let min = this.candleZ;
22059
- for (const z of this.seriesZ.values()) if (z < min) min = z;
22060
- return min;
22061
- }
22062
- ensurePane(id, kind, order, heightWeight) {
22063
- this.orderedCache = null;
22064
- const existing = this.panes.get(id);
22065
- if (existing) {
22066
- existing.order = order;
22067
- existing.heightWeight = heightWeight;
22068
- existing.kind = kind;
22069
- return existing;
22070
- }
22071
- const pane = { id, kind, order, heightWeight, bounds: { top: 0, height: 0 }, scale: { min: 0, max: 1 }, scaleTarget: { min: 0, max: 1 }, initialized: false, manualScale: null, collapsed: false, percentBaseline: 0 };
22072
- this.panes.set(id, pane);
22073
- return pane;
22074
- }
22075
- removePane(id) {
22076
- this.panes.delete(id);
22077
- this.orderedCache = null;
22078
- }
22079
- };
22080
- function paneScaleMode(scene, pane) {
22081
- return pane.kind === "price" ? scene.scaleMode : pane.scaleMode ?? "price";
22082
- }
22083
- function paneLogScale(scene, pane) {
22084
- return pane.kind === "price" ? scene.logScale : pane.logScale ?? false;
22085
- }
22086
- function paneInvert(scene, pane) {
22087
- return pane.kind === "price" ? scene.invertScale : pane.invert ?? false;
22088
- }
22089
- function percentScaleFor(scene, pane) {
22090
- const mode = paneScaleMode(scene, pane);
22091
- if (mode !== "percent" && mode !== "indexed") return void 0;
22092
- const baseline = pane.percentBaseline;
22093
- if (!Number.isFinite(baseline) || baseline === 0) return void 0;
22094
- return { baseline, indexed: mode === "indexed" };
22095
- }
22096
-
22097
- // src/renderers/native/chrome/tz.ts
22098
- function tzOffsetMs(ms, timeZone) {
22099
- if (!timeZone || timeZone === "UTC") return 0;
22100
- try {
22101
- const dtf = new Intl.DateTimeFormat("en-US", {
22102
- timeZone,
22103
- hourCycle: "h23",
22104
- year: "numeric",
22105
- month: "2-digit",
22106
- day: "2-digit",
22107
- hour: "2-digit",
22108
- minute: "2-digit",
22109
- second: "2-digit"
22110
- });
22111
- const parts = dtf.formatToParts(new Date(ms));
22112
- const get = (t) => Number(parts.find((p) => p.type === t)?.value);
22113
- const asUTC = Date.UTC(get("year"), get("month") - 1, get("day"), get("hour") % 24, get("minute"), get("second"));
22114
- return asUTC - ms;
22115
- } catch {
22116
- return 0;
22117
- }
22118
- }
22119
- function zonedDate(ms, timeZone) {
22120
- return new Date(ms + tzOffsetMs(ms, timeZone));
22121
- }
22122
-
22123
21548
  // src/renderers/native/backend/candle-lod.ts
22124
21549
  var CANDLE_BODY_MIN_SPACING = 3;
22125
21550
  var CANDLE_WICK_W = 1.5;
@@ -22409,11 +21834,6 @@ var DASH = {
22409
21834
  dashed: [6, 4],
22410
21835
  dotted: [2, 3]
22411
21836
  };
22412
- function crispHairline(cssCenter, dpr) {
22413
- const w = Math.max(1, Math.round(dpr));
22414
- const edge = Math.round(cssCenter * dpr - w / 2);
22415
- return { pos: edge / dpr, size: w / dpr };
22416
- }
22417
21837
  function joinSegments(hw) {
22418
21838
  return Math.max(6, Math.min(20, Math.round(hw * 4)));
22419
21839
  }
@@ -22423,7 +21843,6 @@ var WebGL2Backend = class {
22423
21843
  this.modelAlpha = 1;
22424
21844
  this.candleBodyAlpha = 1;
22425
21845
  this.candleStructureAlpha = 1;
22426
- this.gridAlpha = 1;
22427
21846
  this.candleBodyScale = 1;
22428
21847
  /** Set by the renderer; invoked after a context restore to request a repaint. */
22429
21848
  this.onNeedsRedraw = null;
@@ -22653,11 +22072,7 @@ var WebGL2Backend = class {
22653
22072
  }
22654
22073
  };
22655
22074
  b.alpha = 1;
22656
- if (pane.collapsed) {
22657
- this.emitGrid(b, coords, theme, dataW, pane, scene);
22658
- } else {
22659
- this.emitHighlights(b, scene, pane, coords);
22660
- this.emitGrid(b, coords, theme, dataW, pane, scene);
22075
+ if (!pane.collapsed) {
22661
22076
  const models = scene.orderedIndicatorsForPane(pane.id);
22662
22077
  const isPrice = pane.kind === "price";
22663
22078
  const effPane = (m) => {
@@ -22842,49 +22257,6 @@ var WebGL2Backend = class {
22842
22257
  this.screenProgram = null;
22843
22258
  }
22844
22259
  // ── geometry emit (mirrors Canvas2dBackend, in CSS-px space) ──
22845
- emitGrid(b, coords, theme, dataW, pane, scene) {
22846
- const top = pane.bounds.top;
22847
- const bot = pane.bounds.top + pane.bounds.height;
22848
- const { gridVert, gridHorz } = scene.style;
22849
- const dpr = coords.dpr;
22850
- b.alpha = this.gridAlpha;
22851
- if (scene.showGrid && gridVert.visible && !pane.collapsed) {
22852
- const grid = parseColor(gridVert.color ?? theme.gridColor);
22853
- const tr = coords.visibleTimeRange();
22854
- const offset = tzOffsetMs((tr.from + tr.to) / 2, scene.timezone);
22855
- for (const tick of timeTicks(tr.from, tr.to, 8, offset)) {
22856
- const x = coords.timeToX(tick.time);
22857
- if (x < 0 || x > dataW) continue;
22858
- const g = crispHairline(x, dpr);
22859
- b.rect(g.pos, top, g.size, bot - top, grid);
22860
- }
22861
- }
22862
- if (scene.showGrid && gridHorz.visible && !pane.collapsed) {
22863
- const grid = parseColor(gridHorz.color ?? theme.gridColor);
22864
- const pct = percentScaleFor(scene, pane);
22865
- for (const t of paneAxisTicks(pane.scale, pane.bounds.height, pct)) {
22866
- const y = coords.priceToY(t.price, pane.scale, pane.bounds);
22867
- if (y < top || y > bot) continue;
22868
- const g = crispHairline(y, dpr);
22869
- b.rect(0, g.pos, dataW, g.size, grid);
22870
- }
22871
- }
22872
- }
22873
- /** Renderer-owned session highlight bands, clipped per-pane (scissor reconstructs full height).
22874
- * Session-zone washes (pre/post-market) paint first, host highlights on top. */
22875
- emitHighlights(b, scene, pane, coords) {
22876
- const bands = [...scene.sessionHighlightBands(), ...scene.highlights];
22877
- if (bands.length === 0) return;
22878
- for (const band of bands) {
22879
- const x1 = coords.timeToX(band.from);
22880
- const x2 = coords.timeToX(band.to);
22881
- if (x2 < 0 || x1 > coords.width || x2 <= x1) continue;
22882
- const cx = Math.max(0, x1);
22883
- const cw = Math.min(coords.width, x2) - cx;
22884
- if (cw <= 0) continue;
22885
- b.rect(cx, pane.bounds.top, cw, pane.bounds.height, parseColor(band.color));
22886
- }
22887
- }
22888
22260
  emitBackground(b, bg, pane, coords) {
22889
22261
  const x1 = coords.timeToX(bg.from);
22890
22262
  const x2 = coords.timeToX(bg.to);
@@ -24207,6 +23579,445 @@ var KeyboardController = class {
24207
23579
  }
24208
23580
  };
24209
23581
 
23582
+ // src/renderers/shared/trade-markers.ts
23583
+ var TRADE_LONG_COLOR = TRADE_LONG;
23584
+ var TRADE_SHORT_COLOR = TRADE_SHORT;
23585
+ var TRADE_EXIT_COLOR = TRADE_EXIT;
23586
+ function defaultTradeMarkersState() {
23587
+ return {
23588
+ visible: true,
23589
+ labels: true,
23590
+ qty: true,
23591
+ colors: { long: TRADE_LONG_COLOR, short: TRADE_SHORT_COLOR, exit: TRADE_EXIT_COLOR }
23592
+ };
23593
+ }
23594
+ function mergeTradeMarkersState(base, patch) {
23595
+ const p = patch && typeof patch === "object" ? patch : {};
23596
+ const c = p.colors && typeof p.colors === "object" ? p.colors : {};
23597
+ const bool = (v, fb) => typeof v === "boolean" ? v : fb;
23598
+ const color = (v, fb) => typeof v === "string" && v.trim().length > 0 ? v : fb;
23599
+ return {
23600
+ visible: bool(p.visible, base.visible),
23601
+ labels: bool(p.labels, base.labels),
23602
+ qty: bool(p.qty, base.qty),
23603
+ colors: {
23604
+ long: color(c.long, base.colors.long),
23605
+ short: color(c.short, base.colors.short),
23606
+ exit: color(c.exit, base.colors.exit)
23607
+ }
23608
+ };
23609
+ }
23610
+ var BAR_GAP = 10;
23611
+ var ARROW_W = 9;
23612
+ var HEAD_H = 6;
23613
+ var ARROW_H = 14;
23614
+ var STEM_W = 3;
23615
+ var CAP_H = 2;
23616
+ var CAP_GAP = 2;
23617
+ var TEXT_GAP = 3;
23618
+ var UNIT_GAP = 6;
23619
+ var TICK_W = 6;
23620
+ var TICK_H = 8;
23621
+ function lineHeightOf(fontSize) {
23622
+ return fontSize + 4;
23623
+ }
23624
+ function qtyText(exec) {
23625
+ if (exec.qty == null || !Number.isFinite(exec.qty)) return null;
23626
+ const magnitude = Number(Math.abs(exec.qty).toFixed(8));
23627
+ return `${exec.side === "buy" ? "+" : "-"}${magnitude}`;
23628
+ }
23629
+ function unitOf(exec, state, lineH) {
23630
+ const lines = [];
23631
+ if (state.labels && exec.label) lines.push(exec.label);
23632
+ if (state.qty) {
23633
+ const q = qtyText(exec);
23634
+ if (q) lines.push(q);
23635
+ }
23636
+ return { exec, lines, height: ARROW_H + (lines.length ? TEXT_GAP + lines.length * lineH : 0) };
23637
+ }
23638
+ function stacksFor(trades, state, deps, from, to, lineH) {
23639
+ const byBar = /* @__PURE__ */ new Map();
23640
+ for (const exec of trades) {
23641
+ const logical = Math.round(deps.timeToLogical(exec.time));
23642
+ if (logical < from || logical > to) continue;
23643
+ let stack = byBar.get(logical);
23644
+ if (!stack) {
23645
+ stack = { logical, buys: [], sells: [] };
23646
+ byBar.set(logical, stack);
23647
+ }
23648
+ (exec.side === "buy" ? stack.buys : stack.sells).push(unitOf(exec, state, lineH));
23649
+ }
23650
+ return [...byBar.values()];
23651
+ }
23652
+ function stackExtent(units) {
23653
+ if (units.length === 0) return 0;
23654
+ let px = BAR_GAP;
23655
+ for (const u of units) px += u.height;
23656
+ return px + (units.length - 1) * UNIT_GAP;
23657
+ }
23658
+ function tradesPriceHints(trades, state, deps, from, to, fontSize) {
23659
+ if (trades.length === 0) return null;
23660
+ const lineH = lineHeightOf(fontSize);
23661
+ let min = Infinity;
23662
+ let max = -Infinity;
23663
+ let abovePx = 0;
23664
+ let belowPx = 0;
23665
+ for (const stack of stacksFor(trades, state, deps, Math.floor(from), Math.ceil(to), lineH)) {
23666
+ const bar = deps.barAt(stack.logical);
23667
+ if (!bar) continue;
23668
+ if (bar.low < min) min = bar.low;
23669
+ if (bar.high > max) max = bar.high;
23670
+ belowPx = Math.max(belowPx, stackExtent(stack.buys));
23671
+ abovePx = Math.max(abovePx, stackExtent(stack.sells));
23672
+ }
23673
+ if (!Number.isFinite(min) || !Number.isFinite(max)) return null;
23674
+ return { min, max, abovePx, belowPx };
23675
+ }
23676
+ function renderTradeMarkers(ctx, trades, state, deps, xOf, yOf, text, width, barHalfPx) {
23677
+ if (trades.length === 0) return;
23678
+ const lineH = lineHeightOf(text.fontSize);
23679
+ const stacks = stacksFor(trades, state, deps, -Infinity, Infinity, lineH);
23680
+ if (stacks.length === 0) return;
23681
+ ctx.save();
23682
+ ctx.font = `${text.fontSize}px ${text.fontFamily}`;
23683
+ ctx.textAlign = "center";
23684
+ ctx.textBaseline = "middle";
23685
+ for (const stack of stacks) {
23686
+ const x = xOf(stack.logical);
23687
+ if (x < -150 || x > width + 150) continue;
23688
+ const bar = deps.barAt(stack.logical);
23689
+ if (!bar) continue;
23690
+ const yBottom = Math.max(yOf(bar.low), yOf(bar.high));
23691
+ const yTop = Math.min(yOf(bar.low), yOf(bar.high));
23692
+ let y = yBottom + BAR_GAP;
23693
+ for (const unit of stack.buys) {
23694
+ ctx.fillStyle = colorOf(unit.exec, state.colors);
23695
+ drawArrowUp(ctx, x, y, unit.exec.kind === "exit");
23696
+ drawFillTick(ctx, unit.exec.side, x, yOf(unit.exec.price), barHalfPx);
23697
+ drawTextLines(ctx, unit.lines, x, y + ARROW_H + TEXT_GAP + lineH / 2, lineH, text.color);
23698
+ y += unit.height + UNIT_GAP;
23699
+ }
23700
+ y = yTop - BAR_GAP;
23701
+ for (const unit of stack.sells) {
23702
+ ctx.fillStyle = colorOf(unit.exec, state.colors);
23703
+ drawArrowDown(ctx, x, y, unit.exec.kind === "exit");
23704
+ drawFillTick(ctx, unit.exec.side, x, yOf(unit.exec.price), barHalfPx);
23705
+ drawTextLines(ctx, unit.lines, x, y - ARROW_H - TEXT_GAP - lineH / 2, -lineH, text.color);
23706
+ y -= unit.height + UNIT_GAP;
23707
+ }
23708
+ }
23709
+ ctx.restore();
23710
+ }
23711
+ function colorOf(exec, colors) {
23712
+ if (exec.kind === "exit") return colors.exit;
23713
+ return exec.side === "buy" ? colors.long : colors.short;
23714
+ }
23715
+ function drawArrowUp(ctx, x, yTip, capped) {
23716
+ ctx.beginPath();
23717
+ ctx.moveTo(x, yTip);
23718
+ ctx.lineTo(x - ARROW_W / 2, yTip + HEAD_H);
23719
+ ctx.lineTo(x + ARROW_W / 2, yTip + HEAD_H);
23720
+ ctx.closePath();
23721
+ ctx.fill();
23722
+ ctx.fillRect(x - STEM_W / 2, yTip + HEAD_H, STEM_W, ARROW_H - HEAD_H);
23723
+ if (capped) ctx.fillRect(x - ARROW_W / 2, yTip - CAP_GAP - CAP_H, ARROW_W, CAP_H);
23724
+ }
23725
+ function drawArrowDown(ctx, x, yTip, capped) {
23726
+ ctx.beginPath();
23727
+ ctx.moveTo(x, yTip);
23728
+ ctx.lineTo(x - ARROW_W / 2, yTip - HEAD_H);
23729
+ ctx.lineTo(x + ARROW_W / 2, yTip - HEAD_H);
23730
+ ctx.closePath();
23731
+ ctx.fill();
23732
+ ctx.fillRect(x - STEM_W / 2, yTip - ARROW_H, STEM_W, ARROW_H - HEAD_H);
23733
+ if (capped) ctx.fillRect(x - ARROW_W / 2, yTip + CAP_GAP, ARROW_W, CAP_H);
23734
+ }
23735
+ function drawFillTick(ctx, side, x, yFill, barHalfPx) {
23736
+ const edge = side === "buy" ? x - barHalfPx : x + barHalfPx;
23737
+ const back = side === "buy" ? edge - TICK_W : edge + TICK_W;
23738
+ ctx.beginPath();
23739
+ ctx.moveTo(edge, yFill);
23740
+ ctx.lineTo(back, yFill - TICK_H / 2);
23741
+ ctx.lineTo(back, yFill + TICK_H / 2);
23742
+ ctx.closePath();
23743
+ ctx.fill();
23744
+ }
23745
+ function drawTextLines(ctx, lines, x, firstY, step, color) {
23746
+ if (lines.length === 0) return;
23747
+ ctx.fillStyle = color;
23748
+ for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], x, firstY + i * step);
23749
+ }
23750
+
23751
+ // src/renderers/native/core/SceneGraph.ts
23752
+ var SceneGraph = class {
23753
+ constructor() {
23754
+ this.panes = /* @__PURE__ */ new Map();
23755
+ this.indicators = /* @__PURE__ */ new Map();
23756
+ this.bars = [];
23757
+ /** Volume-layer config pushed by the volume native indicator (null ⇒ layer off). Ephemeral. */
23758
+ this.volumeLayer = null;
23759
+ /** Generic native-data channels for SDK renderer layers (`setNativeData(id, …)`). Ephemeral. */
23760
+ this.nativeData = /* @__PURE__ */ new Map();
23761
+ /** Loading ranges per channel (`setNativeData(id + '-pending', …)`). Ephemeral. */
23762
+ this.nativePending = /* @__PURE__ */ new Map();
23763
+ /** VPVR-layer config pushed by the VPVR native indicator (null ⇒ layer off). Ephemeral. */
23764
+ this.vpvrLayer = null;
23765
+ this.crosshair = null;
23766
+ /** How the base price series is drawn on the price pane (candles by default). */
23767
+ this.priceStyle = "candles";
23768
+ /** Price-series base painting for the ACTIVE style (see ChartTypeDefinition.basePainting). */
23769
+ this.basePainting = "candles";
23770
+ /** The ACTIVE style's own candle cosmetics (`chartTypes.<id>.candle*`) when it is a
23771
+ * candle-based plugin type; null ⇒ paint with the shared `style.candle` block. */
23772
+ this.candleOverride = null;
23773
+ /** Explicit baseline reference price for `priceStyle:'baseline'`; when null the
23774
+ * baseline follows `style.baseline.baselineLevel` as a percent of the visible pane
23775
+ * range (resolved per frame via `baselinePriceFor`). */
23776
+ this.baselineValue = null;
23777
+ /** Draw the dashed horizontal line at the latest price (price pane). Independent
23778
+ * of the axis label chip (`showPriceLabel`) — either can show without the other. */
23779
+ this.showPriceLine = true;
23780
+ /** Draw the last-price label chip on the price axis. Independent of the line. */
23781
+ this.showPriceLabel = true;
23782
+ /** Draw the countdown-to-bar-close chip on the price axis. When the price label is
23783
+ * also shown, the two merge into one stacked block (countdown under the label);
23784
+ * when either shows alone it's centered on the latest price level. */
23785
+ this.showCountdown = true;
23786
+ /** Logarithmic price scale on the price pane. */
23787
+ this.logScale = false;
23788
+ /** Inverted price axis on the price pane (high at the bottom). Study panes carry their own. */
23789
+ this.invertScale = false;
23790
+ /** Exchange tick size for the active symbol (e.g. 0.01), when known. Drives the
23791
+ * price-axis decimals — the instrument's true precision instead of the zoom-derived
23792
+ * formula. Undefined until symbol metadata loads (the formula is the fallback). */
23793
+ this.priceMintick = void 0;
23794
+ /** Price-axis mode on the price pane: `'price'` (absolute) or `'percent'` (change
23795
+ * vs `percentBaseline`). Gridlines, axis labels and crosshair chip all follow it. */
23796
+ this.scaleMode = "price";
23797
+ /** Reference price for percent mode (first visible bar's close); recomputed per frame. */
23798
+ this.percentBaseline = 0;
23799
+ /** IANA time zone for the time axis + crosshair/data-window stamps (`'UTC'` default). */
23800
+ this.timezone = "UTC";
23801
+ /** Draw the background gridlines (price + time). Master toggle (`gridlines`
23802
+ * feature); per-axis visibility + colors live in `style.gridVert`/`gridHorz`. */
23803
+ this.showGrid = true;
23804
+ /** Comprehensive cosmetic config (item 15): grid colors, crosshair, candle
23805
+ * border/wick, fonts, separators. Serialized via the renderer's `getConfig()`/
23806
+ * `applyConfig()`; every draw layer reads its knobs from here, falling back to
23807
+ * the theme for any value left at its inherit default. */
23808
+ this.style = defaultChartStyle();
23809
+ /** Draw the price/time axis tick labels. */
23810
+ this.showAxisLabels = true;
23811
+ /** Strategy trade-marker display (the `tradeMarkers` feature): master toggle, the
23812
+ * two text lines, and the palette. Trade markers always paint on the price pane. */
23813
+ this.tradeMarkers = defaultTradeMarkersState();
23814
+ /** Renderer-owned shaded time bands (session highlighting), behind grid + data. */
23815
+ this.highlights = [];
23816
+ /** Pre/post-market bands pushed by the host (`sessionZones` feature); null ⇒ no sessions. */
23817
+ this.sessionZones = null;
23818
+ /** Draw-order key of the price candles, relative to indicator series z (see `seriesZ`).
23819
+ * Indicators with z below this draw BEHIND the candles; at/above draw in front.
23820
+ * Default 0 with indicators mounting at z < 0 ⇒ the price reads on top of every overlay,
23821
+ * and user drawings (z ≥ 1 by default) on top of the price. */
23822
+ this.candleZ = 0;
23823
+ /** Hide the base price series (candles/bars/line/area) without removing it — overlay
23824
+ * indicators keep drawing and the pane autoscales to them. Toggled from the object tree. */
23825
+ this.candlesHidden = false;
23826
+ /** Per-indicator foreground draw-order key (series layer), keyed by indicator id.
23827
+ * Higher = drawn later (in front). Assigned on mount to the current BOTTOM of the stack,
23828
+ * so each indicator arrives behind the candles (and behind older indicators);
23829
+ * `setIndicatorZ`/`bringToFront`/`sendToBack` change it. */
23830
+ this.seriesZ = /* @__PURE__ */ new Map();
23831
+ /** Per-pane raster layers of user drawings interleaved into the series stack — each is a
23832
+ * prepainted canvas the backend composites just before the series carrying `beforeZ`.
23833
+ * Rebuilt by the renderer per data frame; empty when every drawing sits over the stack. */
23834
+ this.drawingSlices = /* @__PURE__ */ new Map();
23835
+ /** Per-model index offset: the chart bar index of the model's `anchorTime` — its
23836
+ * index-aligned payloads (dense series arrays, `bar_index` drawings) count from that
23837
+ * bar. Only nonzero for models computed over a SUFFIX of the bars (whole-chart models,
23838
+ * the norm, aren't stored). Recomputed by the renderer on setBars + mount/patch. */
23839
+ this.anchorOffsets = /* @__PURE__ */ new Map();
23840
+ /** Per-indicator private price windows (merged indicators drawn on their own scale
23841
+ * column). Populated per frame for models flagged `ownScale`; absent ⇒ the model
23842
+ * shares its pane's master scale. */
23843
+ this.indicatorScales = /* @__PURE__ */ new Map();
23844
+ /** Cached sort of `panes` by order; invalidated on add/remove/reorder. */
23845
+ this.orderedCache = null;
23846
+ }
23847
+ /** Panes sorted top-to-bottom by `order`. Cached — callers must NOT mutate the array. */
23848
+ orderedPanes() {
23849
+ if (!this.orderedCache) this.orderedCache = [...this.panes.values()].sort((a, b) => a.order - b.order);
23850
+ return this.orderedCache;
23851
+ }
23852
+ /** The session zones resolved into colored bands (pre/post-market washes from the
23853
+ * config's session colors) — consumed by the same painting path as {@link highlights}. */
23854
+ sessionHighlightBands() {
23855
+ if (!this.sessionZones) return [];
23856
+ const out = [];
23857
+ for (const [from, to] of this.sessionZones.pre) out.push({ from, to, color: this.style.sessions.premarketColor });
23858
+ for (const [from, to] of this.sessionZones.post) out.push({ from, to, color: this.style.sessions.postmarketColor });
23859
+ return out;
23860
+ }
23861
+ indicatorsForPane(paneId) {
23862
+ const out = [];
23863
+ for (const model of this.indicators.values()) if (model.paneId === paneId) out.push(model);
23864
+ return out;
23865
+ }
23866
+ /** Merged (own-scale) indicators on a pane, ordered by z — one axis column each. */
23867
+ ownScaleIndicatorsForPane(paneId) {
23868
+ return this.orderedIndicatorsForPane(paneId).filter((m) => m.ownScale === true);
23869
+ }
23870
+ /** Ensure a merged indicator has a private scale slot (seeded from the pane if given). */
23871
+ ensureIndicatorScale(id, seed) {
23872
+ let s = this.indicatorScales.get(id);
23873
+ if (!s) {
23874
+ const base = seed ?? { min: 0, max: 1 };
23875
+ s = { scale: { ...base }, scaleTarget: { ...base }, initialized: false, manualScale: null };
23876
+ this.indicatorScales.set(id, s);
23877
+ }
23878
+ return s;
23879
+ }
23880
+ dropIndicatorScale(id) {
23881
+ this.indicatorScales.delete(id);
23882
+ }
23883
+ /** The price window a model renders on: its own scale when merged (`ownScale`), else the pane's. */
23884
+ scaleFor(model, pane) {
23885
+ if (model.ownScale === true) {
23886
+ const s = this.indicatorScales.get(model.id);
23887
+ if (s) return s.scale;
23888
+ }
23889
+ return pane.scale;
23890
+ }
23891
+ /** Apply a new top-to-bottom pane order (ids not present are ignored). */
23892
+ orderPanes(orderedIds) {
23893
+ orderedIds.forEach((id, i) => {
23894
+ const pane = this.panes.get(id);
23895
+ if (pane) pane.order = i;
23896
+ });
23897
+ this.orderedCache = null;
23898
+ }
23899
+ /** Indicators on a pane sorted by foreground z (ascending). Array#sort is stable,
23900
+ * so equal-z models keep their insertion order (the default). */
23901
+ orderedIndicatorsForPane(paneId) {
23902
+ return this.indicatorsForPane(paneId).sort((a, b) => this.zOf(a.id) - this.zOf(b.id));
23903
+ }
23904
+ /** The foreground draw-order key of an indicator (0 when never assigned). */
23905
+ zOf(id) {
23906
+ return this.seriesZ.get(id) ?? 0;
23907
+ }
23908
+ /** The model's index offset: chart bar index its index-aligned payloads count from (0 = whole-chart). */
23909
+ offsetOf(id) {
23910
+ return this.anchorOffsets.get(id) ?? 0;
23911
+ }
23912
+ /** Offsets are SIGNED. Positive: the model starts after the chart's first bar (it ran
23913
+ * over a suffix) — readers skip its leading chart bars. Negative: the model starts
23914
+ * BEFORE it (the chart's head moved forward under a mounted model) — readers skip the
23915
+ * model's own leading points, `points[i - off]` reaching further in. Storing only the
23916
+ * positive case silently pinned such a model at index 0, i.e. drew it shifted. */
23917
+ setAnchorOffset(id, offset) {
23918
+ if (offset !== 0 && Number.isFinite(offset)) this.anchorOffsets.set(id, offset);
23919
+ else this.anchorOffsets.delete(id);
23920
+ }
23921
+ forgetAnchorOffset(id) {
23922
+ this.anchorOffsets.delete(id);
23923
+ }
23924
+ /** Resolve the baseline reference price for the given pane window: the explicit
23925
+ * `baselineValue` when set, else `style.baseline.baselineLevel` as the price that sits
23926
+ * at that fraction of the pane height. Interpolated in the same space the pane renders
23927
+ * in (log when `scale.log`, else linear) so `level%` always lands at `level%` of the
23928
+ * height — matching `CoordinateSystem.yToPrice`. */
23929
+ baselinePriceFor(scale) {
23930
+ if (this.baselineValue != null) return this.baselineValue;
23931
+ const t = this.style.baseline.baselineLevel / 100;
23932
+ if (scale.log && scale.min > 0 && scale.max > scale.min) {
23933
+ const lo = Math.log(scale.min);
23934
+ return Math.exp(lo + t * (Math.log(scale.max) - lo));
23935
+ }
23936
+ return scale.min + (scale.max - scale.min) * t;
23937
+ }
23938
+ /** Assign a default z on mount: the current bottom of the stack, so a new indicator
23939
+ * paints behind the candles and behind every indicator already there — the price stays
23940
+ * the top of the pile until the user restacks it. No-op if the indicator already has one. */
23941
+ assignIndicatorZ(id) {
23942
+ if (!this.seriesZ.has(id)) this.seriesZ.set(id, this.bottomZ() - 1);
23943
+ }
23944
+ /** Mount-time default for a LAYER-BACKED native (it paints on a canvas stacked above the
23945
+ * data canvas by default): top of the stack, so the recorded order tells the truth from
23946
+ * the first frame. Keeps an existing key, so a restored stack survives the remount. */
23947
+ assignIndicatorZTop(id) {
23948
+ if (!this.seriesZ.has(id)) this.seriesZ.set(id, this.topZ() + 1);
23949
+ }
23950
+ forgetIndicatorZ(id) {
23951
+ this.seriesZ.delete(id);
23952
+ }
23953
+ setIndicatorZ(id, z) {
23954
+ this.seriesZ.set(id, z);
23955
+ }
23956
+ /** Snapshot of the current ordering for a UI/read API: `{ id, z }` sorted by z. */
23957
+ indicatorZOrder() {
23958
+ return [...this.seriesZ.entries()].map(([id, z]) => ({ id, z })).sort((a, b) => a.z - b.z);
23959
+ }
23960
+ /** The pane's series z keys (each indicator, plus the candles on the price pane), sorted
23961
+ * ascending and de-duplicated — the boundaries a user drawing's z is slotted against. */
23962
+ seriesBoundaries(paneId) {
23963
+ const keys = /* @__PURE__ */ new Set();
23964
+ if (paneId === "price") keys.add(this.candleZ);
23965
+ for (const m of this.indicatorsForPane(paneId)) keys.add(this.zOf(m.id));
23966
+ return [...keys].sort((a, b) => a - b);
23967
+ }
23968
+ /** Raise an indicator above every other layer (other indicators AND the candles). */
23969
+ bringIndicatorToFront(id) {
23970
+ this.seriesZ.set(id, this.topZ() + 1);
23971
+ }
23972
+ /** Drop an indicator below every other layer (other indicators AND the candles). */
23973
+ sendIndicatorToBack(id) {
23974
+ this.seriesZ.set(id, this.bottomZ() - 1);
23975
+ }
23976
+ topZ() {
23977
+ let max = this.candleZ;
23978
+ for (const z of this.seriesZ.values()) if (z > max) max = z;
23979
+ return max;
23980
+ }
23981
+ bottomZ() {
23982
+ let min = this.candleZ;
23983
+ for (const z of this.seriesZ.values()) if (z < min) min = z;
23984
+ return min;
23985
+ }
23986
+ ensurePane(id, kind, order, heightWeight) {
23987
+ this.orderedCache = null;
23988
+ const existing = this.panes.get(id);
23989
+ if (existing) {
23990
+ existing.order = order;
23991
+ existing.heightWeight = heightWeight;
23992
+ existing.kind = kind;
23993
+ return existing;
23994
+ }
23995
+ const pane = { id, kind, order, heightWeight, bounds: { top: 0, height: 0 }, scale: { min: 0, max: 1 }, scaleTarget: { min: 0, max: 1 }, initialized: false, manualScale: null, collapsed: false, percentBaseline: 0 };
23996
+ this.panes.set(id, pane);
23997
+ return pane;
23998
+ }
23999
+ removePane(id) {
24000
+ this.panes.delete(id);
24001
+ this.orderedCache = null;
24002
+ }
24003
+ };
24004
+ function paneScaleMode(scene, pane) {
24005
+ return pane.kind === "price" ? scene.scaleMode : pane.scaleMode ?? "price";
24006
+ }
24007
+ function paneLogScale(scene, pane) {
24008
+ return pane.kind === "price" ? scene.logScale : pane.logScale ?? false;
24009
+ }
24010
+ function paneInvert(scene, pane) {
24011
+ return pane.kind === "price" ? scene.invertScale : pane.invert ?? false;
24012
+ }
24013
+ function percentScaleFor(scene, pane) {
24014
+ const mode = paneScaleMode(scene, pane);
24015
+ if (mode !== "percent" && mode !== "indexed") return void 0;
24016
+ const baseline = pane.percentBaseline;
24017
+ if (!Number.isFinite(baseline) || baseline === 0) return void 0;
24018
+ return { baseline, indexed: mode === "indexed" };
24019
+ }
24020
+
24210
24021
  // src/renderers/native/backend/Canvas2dBackend.ts
24211
24022
  var Canvas2dBackend = class {
24212
24023
  constructor() {
@@ -24214,7 +24025,6 @@ var Canvas2dBackend = class {
24214
24025
  this.modelAlpha = 1;
24215
24026
  this.candleBodyAlpha = 1;
24216
24027
  this.candleStructureAlpha = 1;
24217
- this.gridAlpha = 1;
24218
24028
  this.candleBodyScale = 1;
24219
24029
  this.canvas = null;
24220
24030
  this.ctx = null;
@@ -24241,8 +24051,6 @@ var Canvas2dBackend = class {
24241
24051
  if (i1 < i0) return;
24242
24052
  const barColorMap = mergeBarColors2(scene.indicators);
24243
24053
  const panes = scene.orderedPanes();
24244
- this.drawHighlights(ctx, scene, coords);
24245
- this.drawGrid(ctx, scene, coords, theme, dataW);
24246
24054
  for (const pane of panes) {
24247
24055
  if (pane.collapsed) continue;
24248
24056
  const models = scene.orderedIndicatorsForPane(pane.id);
@@ -24849,22 +24657,6 @@ var Canvas2dBackend = class {
24849
24657
  ctx.fillStyle = bg.color;
24850
24658
  ctx.fillRect(x1, pane.bounds.top, x2 - x1, pane.bounds.height);
24851
24659
  }
24852
- /** Renderer-owned session highlight bands: full-height (all panes), behind grid + data.
24853
- * Session-zone washes (pre/post-market) paint first, host highlights on top. */
24854
- drawHighlights(ctx, scene, coords) {
24855
- const bands = [...scene.sessionHighlightBands(), ...scene.highlights];
24856
- if (bands.length === 0) return;
24857
- for (const band of bands) {
24858
- const x1 = coords.timeToX(band.from);
24859
- const x2 = coords.timeToX(band.to);
24860
- if (x2 < 0 || x1 > coords.width || x2 <= x1) continue;
24861
- const cx = Math.max(0, x1);
24862
- const cw = Math.min(coords.width, x2) - cx;
24863
- if (cw <= 0) continue;
24864
- ctx.fillStyle = band.color;
24865
- ctx.fillRect(cx, 0, cw, coords.height);
24866
- }
24867
- }
24868
24660
  drawHline(ctx, pl, pane, coords, dataW, theme) {
24869
24661
  const y = Math.round(coords.priceToY(pl.price, pane.scale, pane.bounds)) + 0.5;
24870
24662
  if (y < pane.bounds.top || y > pane.bounds.top + pane.bounds.height) return;
@@ -24877,46 +24669,6 @@ var Canvas2dBackend = class {
24877
24669
  ctx.stroke();
24878
24670
  setDash(ctx, "solid");
24879
24671
  }
24880
- // ── grid (L0, behind data) ── vert/horz gate on `scene.showGrid` AND their own
24881
- // per-axis visibility (style); each uses its own color. Pane separators are drawn on the
24882
- // chrome layer (full-width, above the data) so series never overpaint them.
24883
- drawGrid(ctx, scene, coords, theme, dataW) {
24884
- const panes = scene.orderedPanes();
24885
- const { gridVert, gridHorz } = scene.style;
24886
- const vertColor = gridVert.color ?? theme.gridColor;
24887
- const horzColor = gridHorz.color ?? theme.gridColor;
24888
- ctx.lineWidth = 1;
24889
- if (scene.showGrid && gridVert.visible) {
24890
- ctx.globalAlpha = this.gridAlpha;
24891
- ctx.strokeStyle = vertColor;
24892
- const tr = coords.visibleTimeRange();
24893
- const offset = tzOffsetMs((tr.from + tr.to) / 2, scene.timezone);
24894
- ctx.beginPath();
24895
- for (const tick of timeTicks(tr.from, tr.to, 8, offset)) {
24896
- const x = Math.round(coords.timeToX(tick.time)) + 0.5;
24897
- if (x < 0 || x > dataW) continue;
24898
- ctx.moveTo(x, 0);
24899
- ctx.lineTo(x, coords.height);
24900
- }
24901
- ctx.stroke();
24902
- }
24903
- for (const pane of panes) {
24904
- if (scene.showGrid && gridHorz.visible && !pane.collapsed) {
24905
- ctx.globalAlpha = this.gridAlpha;
24906
- ctx.strokeStyle = horzColor;
24907
- const pct = percentScaleFor(scene, pane);
24908
- ctx.beginPath();
24909
- for (const t of paneAxisTicks(pane.scale, pane.bounds.height, pct)) {
24910
- const y = Math.round(coords.priceToY(t.price, pane.scale, pane.bounds)) + 0.5;
24911
- if (y < pane.bounds.top || y > pane.bounds.top + pane.bounds.height) continue;
24912
- ctx.moveTo(0, y);
24913
- ctx.lineTo(dataW, y);
24914
- }
24915
- ctx.stroke();
24916
- }
24917
- }
24918
- ctx.globalAlpha = 1;
24919
- }
24920
24672
  };
24921
24673
  function setDash(ctx, style) {
24922
24674
  if (style === "dashed") ctx.setLineDash([6, 4]);
@@ -25628,6 +25380,221 @@ var DrawingSceneRenderer = class {
25628
25380
  }
25629
25381
  };
25630
25382
 
25383
+ // src/renderers/native/chrome/ticks.ts
25384
+ function priceTicks(min, max, target = 6) {
25385
+ if (!(max > min) || !Number.isFinite(min) || !Number.isFinite(max)) return [];
25386
+ const raw = (max - min) / Math.max(1, target);
25387
+ const mag = Math.pow(10, Math.floor(Math.log10(raw)));
25388
+ const norm = raw / mag;
25389
+ const step = (norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10) * mag;
25390
+ const decimals = Math.max(0, -Math.floor(Math.log10(step)) + 1);
25391
+ const out = [];
25392
+ const start = Math.ceil(min / step) * step;
25393
+ for (let v = start; v <= max + step * 1e-6; v += step) {
25394
+ out.push(Number(v.toFixed(decimals)));
25395
+ }
25396
+ return out;
25397
+ }
25398
+ function priceDecimals(min, max, target = 6) {
25399
+ if (!(max > min)) return 2;
25400
+ const raw = (max - min) / Math.max(1, target);
25401
+ const mag = Math.pow(10, Math.floor(Math.log10(raw)));
25402
+ const norm = raw / mag;
25403
+ const step = (norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10) * mag;
25404
+ return Math.max(0, Math.min(8, -Math.floor(Math.log10(step)) + 1));
25405
+ }
25406
+ function logPriceTicks(min, max, target = 6) {
25407
+ if (min <= 0 || !(max > min) || !Number.isFinite(min) || !Number.isFinite(max)) return [];
25408
+ if (Math.log10(max) - Math.log10(min) < 1.1) return priceTicks(min, max, target);
25409
+ const out = [];
25410
+ const startExp = Math.floor(Math.log10(min));
25411
+ const endExp = Math.ceil(Math.log10(max));
25412
+ for (let e = startExp; e <= endExp; e += 1) {
25413
+ for (const m of [1, 2, 5]) {
25414
+ const v = m * Math.pow(10, e);
25415
+ if (v >= min && v <= max) out.push(v);
25416
+ }
25417
+ }
25418
+ return out;
25419
+ }
25420
+ function valueDecimals(v) {
25421
+ const a = Math.abs(v);
25422
+ if (a >= 100) return 0;
25423
+ if (a >= 1) return 2;
25424
+ if (a >= 0.01) return 4;
25425
+ return 6;
25426
+ }
25427
+ function tickDecimals(tick) {
25428
+ if (!(tick > 0) || !Number.isFinite(tick)) return 2;
25429
+ for (let d = 0; d <= 8; d += 1) {
25430
+ if (Math.abs(Number(tick.toFixed(d)) - tick) <= tick * 1e-6) return d;
25431
+ }
25432
+ return 8;
25433
+ }
25434
+ function axisDecimals(scale, heightPx, mintick) {
25435
+ const d = mintick != null && mintick > 0 ? tickDecimals(mintick) : priceDecimals(scale.min, scale.max, tickCount(heightPx));
25436
+ return d === 0 ? 2 : d;
25437
+ }
25438
+ function tickCount(paneHeightPx) {
25439
+ return Math.max(2, Math.min(16, Math.round(paneHeightPx / 50)));
25440
+ }
25441
+ function paneTicks(scale, heightPx) {
25442
+ return scale.log ? logPriceTicks(scale.min, scale.max, tickCount(heightPx)) : priceTicks(scale.min, scale.max, tickCount(heightPx));
25443
+ }
25444
+ function toPct(price, baseline) {
25445
+ return (price / baseline - 1) * 100;
25446
+ }
25447
+ function toIndex(price, baseline) {
25448
+ return price / baseline * 100;
25449
+ }
25450
+ function formatPct(pct) {
25451
+ const sign = pct >= 0 ? "+" : "-";
25452
+ return `${sign}${Math.abs(pct).toFixed(2)}%`;
25453
+ }
25454
+ function formatIndex(idx) {
25455
+ return idx.toFixed(2);
25456
+ }
25457
+ function formatCompactValue(v) {
25458
+ const a = Math.abs(v);
25459
+ if (a >= 1e9) return `${trimZeros(v / 1e9)}B`;
25460
+ if (a >= 1e6) return `${trimZeros(v / 1e6)}M`;
25461
+ if (a >= 1e3) return `${trimZeros(v / 1e3)}K`;
25462
+ return trimZeros(v);
25463
+ }
25464
+ function trimZeros(v) {
25465
+ return Number(v.toFixed(2)).toString();
25466
+ }
25467
+ function paneAxisTicks(scale, heightPx, pct, mintick, format) {
25468
+ if (format === "none") return [];
25469
+ if (pct) {
25470
+ const { baseline, indexed } = pct;
25471
+ const lo = Math.min(scale.min, scale.max);
25472
+ const hi = Math.max(scale.min, scale.max);
25473
+ if (indexed) {
25474
+ const iLo = toIndex(lo, baseline);
25475
+ const iHi = toIndex(hi, baseline);
25476
+ return priceTicks(iLo, iHi, tickCount(heightPx)).map((idx) => ({ price: baseline * idx / 100, label: formatIndex(idx) }));
25477
+ }
25478
+ const pLo = toPct(lo, baseline);
25479
+ const pHi = toPct(hi, baseline);
25480
+ return priceTicks(pLo, pHi, tickCount(heightPx)).map((p) => ({ price: baseline * (1 + p / 100), label: formatPct(p) }));
25481
+ }
25482
+ if (format === "volume") {
25483
+ return paneTicks(scale, heightPx).map((price) => ({ price, label: formatCompactValue(price) }));
25484
+ }
25485
+ return paneTicks(scale, heightPx).map((price) => ({ price, label: formatPriceLabel(scale, heightPx, price, mintick) }));
25486
+ }
25487
+ function formatAxisValue(scale, heightPx, value, pct, mintick, format) {
25488
+ if (format === "none") return "";
25489
+ if (pct) return pct.indexed ? formatIndex(toIndex(value, pct.baseline)) : formatPct(toPct(value, pct.baseline));
25490
+ if (format === "volume") return formatCompactValue(value);
25491
+ return formatPriceLabel(scale, heightPx, value, mintick);
25492
+ }
25493
+ function formatPriceLabel(scale, heightPx, value, mintick) {
25494
+ const wideLog = scale.log && Math.log10(scale.max) - Math.log10(scale.min) >= 1.1;
25495
+ if (wideLog) {
25496
+ const d = valueDecimals(value);
25497
+ return value.toFixed(d === 0 ? 2 : d);
25498
+ }
25499
+ return value.toFixed(axisDecimals(scale, heightPx, mintick));
25500
+ }
25501
+ var SEC = 1e3;
25502
+ var MIN = 60 * SEC;
25503
+ var HOUR = 60 * MIN;
25504
+ var DAY = 24 * HOUR;
25505
+ var WEEK = 7 * DAY;
25506
+ var MONTH = 30 * DAY;
25507
+ var YEAR = 365 * DAY;
25508
+ var STEP_LADDER = [
25509
+ SEC,
25510
+ 5 * SEC,
25511
+ 15 * SEC,
25512
+ 30 * SEC,
25513
+ MIN,
25514
+ 5 * MIN,
25515
+ 15 * MIN,
25516
+ 30 * MIN,
25517
+ HOUR,
25518
+ 2 * HOUR,
25519
+ 4 * HOUR,
25520
+ 6 * HOUR,
25521
+ 12 * HOUR,
25522
+ DAY,
25523
+ 2 * DAY,
25524
+ WEEK,
25525
+ MONTH,
25526
+ 3 * MONTH,
25527
+ YEAR
25528
+ ];
25529
+ var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
25530
+ var pad2 = (n) => n < 10 ? `0${n}` : String(n);
25531
+ function pickStep(targetMs) {
25532
+ for (const step of STEP_LADDER) if (step >= targetMs) return step;
25533
+ return STEP_LADDER[STEP_LADDER.length - 1];
25534
+ }
25535
+ function timeTicks(fromMs, toMs, target = 8, offsetMs = 0) {
25536
+ const span = toMs - fromMs;
25537
+ if (!(span > 0)) return [];
25538
+ const step = pickStep(span / Math.max(1, target));
25539
+ const zFrom = fromMs + offsetMs;
25540
+ const zTo = toMs + offsetMs;
25541
+ const first = Math.ceil(zFrom / step) * step;
25542
+ const out = [];
25543
+ for (let zt = first; zt <= zTo; zt += step) {
25544
+ const t = zt - offsetMs;
25545
+ const d = new Date(zt);
25546
+ let label;
25547
+ let major = false;
25548
+ if (step < DAY) {
25549
+ const h = d.getUTCHours();
25550
+ const m = d.getUTCMinutes();
25551
+ if (h === 0 && m === 0) {
25552
+ label = `${MONTHS[d.getUTCMonth()]} ${d.getUTCDate()}`;
25553
+ major = true;
25554
+ } else {
25555
+ label = `${pad2(h)}:${pad2(m)}`;
25556
+ }
25557
+ } else if (step < YEAR) {
25558
+ label = `${MONTHS[d.getUTCMonth()]} ${d.getUTCDate()}`;
25559
+ if (d.getUTCDate() === 1) {
25560
+ label = MONTHS[d.getUTCMonth()];
25561
+ major = true;
25562
+ }
25563
+ } else {
25564
+ label = String(d.getUTCFullYear());
25565
+ major = true;
25566
+ }
25567
+ out.push({ time: t, label, major });
25568
+ }
25569
+ return out;
25570
+ }
25571
+
25572
+ // src/renderers/native/chrome/tz.ts
25573
+ function tzOffsetMs(ms, timeZone) {
25574
+ if (!timeZone || timeZone === "UTC") return 0;
25575
+ try {
25576
+ const dtf = new Intl.DateTimeFormat("en-US", {
25577
+ timeZone,
25578
+ hourCycle: "h23",
25579
+ year: "numeric",
25580
+ month: "2-digit",
25581
+ day: "2-digit",
25582
+ hour: "2-digit",
25583
+ minute: "2-digit",
25584
+ second: "2-digit"
25585
+ });
25586
+ const parts = dtf.formatToParts(new Date(ms));
25587
+ const get = (t) => Number(parts.find((p) => p.type === t)?.value);
25588
+ const asUTC = Date.UTC(get("year"), get("month") - 1, get("day"), get("hour") % 24, get("minute"), get("second"));
25589
+ return asUTC - ms;
25590
+ } catch {
25591
+ return 0;
25592
+ }
25593
+ }
25594
+ function zonedDate(ms, timeZone) {
25595
+ return new Date(ms + tzOffsetMs(ms, timeZone));
25596
+ }
25597
+
25631
25598
  // src/renderers/native/chrome/ChromeRenderer.ts
25632
25599
  var ChromeRenderer = class {
25633
25600
  constructor() {
@@ -25807,6 +25774,13 @@ var ChromeRenderer = class {
25807
25774
  if (y < pane.bounds.top + 6 || y > pane.bounds.top + pane.bounds.height - 4) continue;
25808
25775
  ctx.fillText(t.label, dataW + 6, y);
25809
25776
  }
25777
+ if (pane.axisBands) {
25778
+ for (const b of pane.axisBands) {
25779
+ const y = pane.bounds.top + b.frac * pane.bounds.height;
25780
+ if (y < pane.bounds.top + 6 || y > pane.bounds.top + pane.bounds.height - 4) continue;
25781
+ ctx.fillText(b.label, dataW + 6, y);
25782
+ }
25783
+ }
25810
25784
  }
25811
25785
  ctx.textAlign = "start";
25812
25786
  }
@@ -26048,7 +26022,7 @@ var CrosshairRenderer = class {
26048
26022
  }
26049
26023
  }
26050
26024
  const chipBg = cs.labelBackground ?? theme.borderColor;
26051
- if (pane) {
26025
+ if (pane && pane.axisFormat !== "none") {
26052
26026
  const price = coords.yToPrice(ch.y, pane.scale, pane.bounds);
26053
26027
  this.chip(ctx, dataW + 1, ch.y, formatAxisValue(pane.scale, pane.bounds.height, price, percentScaleFor(scene, pane), scene.priceMintick, pane.axisFormat), chipBg, "left", false, theme.background);
26054
26028
  }
@@ -26096,7 +26070,7 @@ var CrosshairRenderer = class {
26096
26070
  break;
26097
26071
  }
26098
26072
  }
26099
- if (pane) {
26073
+ if (pane && pane.axisFormat !== "none") {
26100
26074
  this.chip(ctx, dataW + 1, ext.y, formatAxisValue(pane.scale, pane.bounds.height, ext.price, percentScaleFor(scene, pane), scene.priceMintick, pane.axisFormat), chipBg, "left", false, theme.background);
26101
26075
  }
26102
26076
  }
@@ -26139,6 +26113,156 @@ function formatStamp(ms, timeZone) {
26139
26113
  return `${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}`;
26140
26114
  }
26141
26115
 
26116
+ // src/renderers/native/chrome/settings-visibility.ts
26117
+ function settingsIdSlug(label) {
26118
+ return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
26119
+ }
26120
+ function settingsIdHidden(id, hidden) {
26121
+ if (hidden.size === 0) return false;
26122
+ let path = id;
26123
+ for (; ; ) {
26124
+ if (hidden.has(path)) return true;
26125
+ const cut = path.lastIndexOf(".");
26126
+ if (cut < 0) return false;
26127
+ path = path.slice(0, cut);
26128
+ }
26129
+ }
26130
+ function settingsRowId(r) {
26131
+ if (r.kind === "heading" || r.kind === "header") return settingsIdSlug(r.label);
26132
+ const n = normalizeSettingsRow(r);
26133
+ if (n.toggle) return n.toggle.key;
26134
+ for (const c of n.controls) {
26135
+ if (c.kind !== "hint") return c.key;
26136
+ }
26137
+ return settingsIdSlug(n.label);
26138
+ }
26139
+ function filterHiddenRows(rows, scope, hidden) {
26140
+ if (hidden.size === 0) return [...rows];
26141
+ const out = [];
26142
+ let skipGroup = false;
26143
+ let skipSub = false;
26144
+ for (const r of rows) {
26145
+ const id = `${scope}.${settingsRowId(r)}`;
26146
+ if (r.kind === "heading") {
26147
+ skipGroup = settingsIdHidden(id, hidden);
26148
+ skipSub = false;
26149
+ if (!skipGroup) out.push(r);
26150
+ continue;
26151
+ }
26152
+ if (r.kind === "header") {
26153
+ if (skipGroup) continue;
26154
+ skipSub = settingsIdHidden(id, hidden);
26155
+ if (!skipSub) out.push(r);
26156
+ continue;
26157
+ }
26158
+ if (skipGroup || skipSub || settingsIdHidden(id, hidden)) continue;
26159
+ out.push(r);
26160
+ }
26161
+ return out;
26162
+ }
26163
+ function hostSectionId(s) {
26164
+ return s.id ?? settingsIdSlug(s.title);
26165
+ }
26166
+ function hostRowId(r) {
26167
+ return r.id ?? settingsIdSlug(r.label);
26168
+ }
26169
+ function filterHiddenHostRows(rows, scope, hidden) {
26170
+ if (hidden.size === 0) return [...rows];
26171
+ const out = [];
26172
+ let skipGroup = false;
26173
+ for (const r of rows) {
26174
+ const id = `${scope}.${hostRowId(r)}`;
26175
+ if (r.kind === "heading") {
26176
+ skipGroup = settingsIdHidden(id, hidden);
26177
+ if (!skipGroup) out.push(r);
26178
+ continue;
26179
+ }
26180
+ if (skipGroup || settingsIdHidden(id, hidden)) continue;
26181
+ out.push(r);
26182
+ }
26183
+ return out;
26184
+ }
26185
+ var BUILTIN_SETTINGS_IDS = [
26186
+ "symbol",
26187
+ "symbol.type",
26188
+ "symbol.style.candles",
26189
+ "symbol.style.candles.body",
26190
+ "symbol.style.candles.borders",
26191
+ "symbol.style.candles.wick",
26192
+ "symbol.style.candles.spacing",
26193
+ "symbol.style.bars",
26194
+ "symbol.style.bars.up-color",
26195
+ "symbol.style.bars.down-color",
26196
+ "symbol.style.bars.spacing",
26197
+ "symbol.style.line",
26198
+ "symbol.style.line.color",
26199
+ "symbol.style.line.width",
26200
+ "symbol.style.area",
26201
+ "symbol.style.area.line-color",
26202
+ "symbol.style.area.width",
26203
+ "symbol.style.area.top-fill",
26204
+ "symbol.style.area.bottom-fill",
26205
+ "symbol.style.baseline",
26206
+ "symbol.style.baseline.top-line",
26207
+ "symbol.style.baseline.bottom-line",
26208
+ "symbol.style.baseline.fill-top",
26209
+ "symbol.style.baseline.fill-bottom",
26210
+ "symbol.style.baseline.base-level",
26211
+ "symbol.style.baseline.width",
26212
+ "symbol.timezone",
26213
+ "scales",
26214
+ "scales.price-scale",
26215
+ "scales.price-scale.mode",
26216
+ "scales.price-scale.invert",
26217
+ "scales.price-scale.last-price-line",
26218
+ "scales.price-scale.last-price-label",
26219
+ "scales.price-scale.countdown",
26220
+ "scales.price-scale.axis-labels",
26221
+ "scales.price-scale.border-color",
26222
+ "scales.crosshair",
26223
+ "scales.crosshair.color",
26224
+ "scales.crosshair.width",
26225
+ "scales.crosshair.style",
26226
+ "canvas",
26227
+ "canvas.background",
26228
+ "canvas.background.color",
26229
+ "canvas.background.text-color",
26230
+ "canvas.background.text-size",
26231
+ "canvas.background.pane-separator",
26232
+ "canvas.grid",
26233
+ "canvas.grid.vertical",
26234
+ "canvas.grid.horizontal",
26235
+ "canvas.theme"
26236
+ ];
26237
+ function settingsIdCatalog(hostSections) {
26238
+ const ids = new Set(BUILTIN_SETTINGS_IDS);
26239
+ for (const def of chartTypes()) {
26240
+ if (hasOwnCandlePaint(def.id)) {
26241
+ const style = `symbol.style.${def.id}`;
26242
+ for (const leaf of ["", ".body", ".borders", ".wick", ".spacing"]) ids.add(style + leaf);
26243
+ }
26244
+ const section = def.settings;
26245
+ if (!section) continue;
26246
+ const scope = `type:${def.id}`;
26247
+ ids.add(scope);
26248
+ const addRows = (rows) => {
26249
+ for (const r of rows) ids.add(`${scope}.${settingsRowId(r)}`);
26250
+ };
26251
+ if (section.rows) addRows(section.rows);
26252
+ for (const inst of section.instances ?? []) addRows(inst.rows);
26253
+ for (const sub of section.subsections ?? []) {
26254
+ ids.add(`${scope}.${settingsIdSlug(sub.title)}`);
26255
+ addRows(sub.rows);
26256
+ }
26257
+ }
26258
+ for (const hs of hostSections) {
26259
+ const scope = hostSectionId(hs);
26260
+ ids.add(scope);
26261
+ for (const r of hs.rows) ids.add(`${scope}.${hostRowId(r)}`);
26262
+ }
26263
+ return [...ids];
26264
+ }
26265
+
26142
26266
  // src/renderers/native/chrome/SettingsDialog.ts
26143
26267
  var BUILTIN_STYLE_LABELS2 = {
26144
26268
  candles: "Candles",
@@ -26257,12 +26381,21 @@ var SettingsDialog = class {
26257
26381
  this.activeSection = null;
26258
26382
  /** Mobile chrome: fullscreen card, burger-opened section sidebar, TOC as top tabs. */
26259
26383
  this.mobileLayout = false;
26384
+ /** The visibility policy: setting ids hidden by the host (subtree semantics). */
26385
+ this.hiddenSettings = /* @__PURE__ */ new Set();
26260
26386
  if (getComputedStyle(container).position === "static") container.style.position = "relative";
26261
26387
  }
26262
26388
  /** Host-app sections (e.g. the widget's Status line tab) — re-shown on next open. */
26263
26389
  setHostSections(sections) {
26264
26390
  this.hostSections = sections;
26265
26391
  }
26392
+ /** Replace the visibility policy — an open dialog rebuilds in place to honor it. */
26393
+ setHiddenSettings(ids) {
26394
+ const next = new Set(ids);
26395
+ const same = next.size === this.hiddenSettings.size && [...next].every((id) => this.hiddenSettings.has(id));
26396
+ this.hiddenSettings = next;
26397
+ if (!same) this.reopenIfLive();
26398
+ }
26266
26399
  /** Configure the Canvas → Theme row (see {@link themeControl}); null hides the row. */
26267
26400
  setThemeControl(current, onSelect) {
26268
26401
  this.themeControl = { current, onSelect };
@@ -26330,8 +26463,12 @@ var SettingsDialog = class {
26330
26463
  }
26331
26464
  const body = document.createElement("div");
26332
26465
  body.style.cssText = "display:flex;flex-direction:column;gap:0;";
26333
- body.append(this.section("Symbol"));
26334
- body.append(this.sectionTitle("Chart type"));
26466
+ const sid = (el, id) => {
26467
+ el.dataset.sdId = id;
26468
+ return el;
26469
+ };
26470
+ body.append(sid(this.section("Symbol"), "symbol"));
26471
+ body.append(sid(this.sectionTitle("Chart type"), "symbol.type"));
26335
26472
  const groups = {};
26336
26473
  const showActive = (style) => {
26337
26474
  const active = style === "heikinashi" ? "candles" : style;
@@ -26340,64 +26477,64 @@ var SettingsDialog = class {
26340
26477
  }
26341
26478
  };
26342
26479
  body.append(
26343
- this.selectRowLabeled("Type", config.series.style, priceStyleIds().map((id) => [id, styleLabel(id)]), (v) => {
26480
+ sid(this.selectRowLabeled("Type", config.series.style, priceStyleIds().map((id) => [id, styleLabel(id)]), (v) => {
26344
26481
  this.emit({ series: { style: v } });
26345
26482
  showActive(v);
26346
26483
  this.syncTypeTabs?.(v);
26347
- })
26484
+ }), "symbol.type")
26348
26485
  );
26349
- const candles = this.group();
26486
+ const candles = sid(this.group(), "symbol.style.candles");
26350
26487
  candles.append(this.sectionTitle("Candles"));
26351
- candles.append(this.toggleRow("Body", config.candles.bodyVisible, (v) => this.emit({ candles: { bodyVisible: v } }), [
26488
+ candles.append(sid(this.toggleRow("Body", config.candles.bodyVisible, (v) => this.emit({ candles: { bodyVisible: v } }), [
26352
26489
  this.swatch(config.candles.upColor, (v) => this.emit({ candles: { upColor: v } })),
26353
26490
  this.swatch(config.candles.downColor, (v) => this.emit({ candles: { downColor: v } }))
26354
- ]));
26355
- candles.append(this.toggleRow("Borders", config.candles.borderVisible, (v) => this.emit({ candles: { borderVisible: v } }), [
26491
+ ]), "symbol.style.candles.body"));
26492
+ candles.append(sid(this.toggleRow("Borders", config.candles.borderVisible, (v) => this.emit({ candles: { borderVisible: v } }), [
26356
26493
  this.swatch(config.candles.borderUpColor, (v) => this.emit({ candles: { borderUpColor: v } })),
26357
26494
  this.swatch(config.candles.borderDownColor, (v) => this.emit({ candles: { borderDownColor: v } }))
26358
- ]));
26359
- candles.append(this.toggleRow("Wick", config.candles.wickVisible, (v) => this.emit({ candles: { wickVisible: v } }), [
26495
+ ]), "symbol.style.candles.borders"));
26496
+ candles.append(sid(this.toggleRow("Wick", config.candles.wickVisible, (v) => this.emit({ candles: { wickVisible: v } }), [
26360
26497
  this.swatch(config.candles.wickUpColor, (v) => this.emit({ candles: { wickUpColor: v } })),
26361
26498
  this.swatch(config.candles.wickDownColor, (v) => this.emit({ candles: { wickDownColor: v } }))
26362
- ]));
26363
- candles.append(this.numberRow("Spacing", config.series.spacing, 0.1, 10, 0.1, (v) => this.emit({ series: { spacing: v } })));
26499
+ ]), "symbol.style.candles.wick"));
26500
+ candles.append(sid(this.numberRow("Spacing", config.series.spacing, 0.1, 10, 0.1, (v) => this.emit({ series: { spacing: v } })), "symbol.style.candles.spacing"));
26364
26501
  groups.candles = candles;
26365
26502
  body.append(candles);
26366
- const bars = this.group();
26503
+ const bars = sid(this.group(), "symbol.style.bars");
26367
26504
  bars.append(this.sectionTitle("Bars"));
26368
- bars.append(this.colorRow("Color Up", config.bars.upColor, (v) => this.emit({ bars: { upColor: v } })));
26369
- bars.append(this.colorRow("Color Down", config.bars.downColor, (v) => this.emit({ bars: { downColor: v } })));
26370
- bars.append(this.numberRow("Spacing", config.series.spacing, 0.1, 10, 0.1, (v) => this.emit({ series: { spacing: v } })));
26505
+ bars.append(sid(this.colorRow("Color Up", config.bars.upColor, (v) => this.emit({ bars: { upColor: v } })), "symbol.style.bars.up-color"));
26506
+ bars.append(sid(this.colorRow("Color Down", config.bars.downColor, (v) => this.emit({ bars: { downColor: v } })), "symbol.style.bars.down-color"));
26507
+ bars.append(sid(this.numberRow("Spacing", config.series.spacing, 0.1, 10, 0.1, (v) => this.emit({ series: { spacing: v } })), "symbol.style.bars.spacing"));
26371
26508
  groups.bars = bars;
26372
26509
  body.append(bars);
26373
- const line = this.group();
26510
+ const line = sid(this.group(), "symbol.style.line");
26374
26511
  line.append(this.sectionTitle("Line"));
26375
- line.append(this.colorRow("Color", config.line.color, (v) => this.emit({ line: { color: v } })));
26376
- line.append(this.numberRow("Width", config.line.width, 1, 10, 1, (v) => this.emit({ line: { width: v } })));
26512
+ line.append(sid(this.colorRow("Color", config.line.color, (v) => this.emit({ line: { color: v } })), "symbol.style.line.color"));
26513
+ line.append(sid(this.numberRow("Width", config.line.width, 1, 10, 1, (v) => this.emit({ line: { width: v } })), "symbol.style.line.width"));
26377
26514
  groups.line = line;
26378
26515
  body.append(line);
26379
- const area = this.group();
26516
+ const area = sid(this.group(), "symbol.style.area");
26380
26517
  area.append(this.sectionTitle("Area"));
26381
- area.append(this.colorRow("Line color", config.area.lineColor, (v) => this.emit({ area: { lineColor: v } })));
26382
- area.append(this.numberRow("Width", config.area.width, 1, 10, 1, (v) => this.emit({ area: { width: v } })));
26383
- area.append(this.colorRow("Top fill", config.area.topColor, (v) => this.emit({ area: { topColor: v } })));
26384
- area.append(this.colorRow("Bottom fill", config.area.bottomColor, (v) => this.emit({ area: { bottomColor: v } })));
26518
+ area.append(sid(this.colorRow("Line color", config.area.lineColor, (v) => this.emit({ area: { lineColor: v } })), "symbol.style.area.line-color"));
26519
+ area.append(sid(this.numberRow("Width", config.area.width, 1, 10, 1, (v) => this.emit({ area: { width: v } })), "symbol.style.area.width"));
26520
+ area.append(sid(this.colorRow("Top fill", config.area.topColor, (v) => this.emit({ area: { topColor: v } })), "symbol.style.area.top-fill"));
26521
+ area.append(sid(this.colorRow("Bottom fill", config.area.bottomColor, (v) => this.emit({ area: { bottomColor: v } })), "symbol.style.area.bottom-fill"));
26385
26522
  groups.area = area;
26386
26523
  body.append(area);
26387
- const baseline = this.group();
26524
+ const baseline = sid(this.group(), "symbol.style.baseline");
26388
26525
  baseline.append(this.sectionTitle("Baseline"));
26389
- baseline.append(this.rowWith("Top line", [this.swatch(config.baseline.topLineColor, (v) => this.emit({ baseline: { topLineColor: v } }))]));
26390
- baseline.append(this.rowWith("Bottom line", [this.swatch(config.baseline.bottomLineColor, (v) => this.emit({ baseline: { bottomLineColor: v } }))]));
26391
- baseline.append(this.rowWith("Fill top area", [
26526
+ baseline.append(sid(this.rowWith("Top line", [this.swatch(config.baseline.topLineColor, (v) => this.emit({ baseline: { topLineColor: v } }))]), "symbol.style.baseline.top-line"));
26527
+ baseline.append(sid(this.rowWith("Bottom line", [this.swatch(config.baseline.bottomLineColor, (v) => this.emit({ baseline: { bottomLineColor: v } }))]), "symbol.style.baseline.bottom-line"));
26528
+ baseline.append(sid(this.rowWith("Fill top area", [
26392
26529
  this.swatch(config.baseline.topFillColor, (v) => this.emit({ baseline: { topFillColor: v } })),
26393
26530
  this.swatch(config.baseline.topFillColor2, (v) => this.emit({ baseline: { topFillColor2: v } }))
26394
- ]));
26395
- baseline.append(this.rowWith("Fill bottom area", [
26531
+ ]), "symbol.style.baseline.fill-top"));
26532
+ baseline.append(sid(this.rowWith("Fill bottom area", [
26396
26533
  this.swatch(config.baseline.bottomFillColor2, (v) => this.emit({ baseline: { bottomFillColor2: v } })),
26397
26534
  this.swatch(config.baseline.bottomFillColor, (v) => this.emit({ baseline: { bottomFillColor: v } }))
26398
- ]));
26399
- baseline.append(this.numberRow("Base level %", config.baseline.baselineLevel, 0, 100, 1, (v) => this.emit({ baseline: { baselineLevel: v } })));
26400
- baseline.append(this.numberRow("Width", config.baseline.width, 1, 10, 1, (v) => this.emit({ baseline: { width: v } })));
26535
+ ]), "symbol.style.baseline.fill-bottom"));
26536
+ baseline.append(sid(this.numberRow("Base level %", config.baseline.baselineLevel, 0, 100, 1, (v) => this.emit({ baseline: { baselineLevel: v } })), "symbol.style.baseline.base-level"));
26537
+ baseline.append(sid(this.numberRow("Width", config.baseline.width, 1, 10, 1, (v) => this.emit({ baseline: { width: v } })), "symbol.style.baseline.width"));
26401
26538
  groups.baseline = baseline;
26402
26539
  body.append(baseline);
26403
26540
  for (const def of chartTypes()) {
@@ -26405,32 +26542,36 @@ var SettingsDialog = class {
26405
26542
  const bag = config.chartTypes[def.id] ?? {};
26406
26543
  const colorOf2 = (key, shared) => typeof bag[key] === "string" && bag[key] !== "" ? bag[key] : shared;
26407
26544
  const boolOf = (key, shared) => typeof bag[key] === "boolean" ? bag[key] : shared;
26408
- const g = this.group();
26545
+ const g = sid(this.group(), `symbol.style.${def.id}`);
26409
26546
  g.append(this.sectionTitle("Candles"));
26410
- g.append(this.toggleRow("Body", boolOf("candleBodyVisible", config.candles.bodyVisible), (v) => this.emitType(def.id, "candleBodyVisible", v), [
26547
+ g.append(sid(this.toggleRow("Body", boolOf("candleBodyVisible", config.candles.bodyVisible), (v) => this.emitType(def.id, "candleBodyVisible", v), [
26411
26548
  this.swatch(colorOf2("candleUpColor", config.candles.upColor), (v) => this.emitType(def.id, "candleUpColor", v)),
26412
26549
  this.swatch(colorOf2("candleDownColor", config.candles.downColor), (v) => this.emitType(def.id, "candleDownColor", v))
26413
- ]));
26414
- g.append(this.toggleRow("Borders", boolOf("candleBorderVisible", config.candles.borderVisible), (v) => this.emitType(def.id, "candleBorderVisible", v), [
26550
+ ]), `symbol.style.${def.id}.body`));
26551
+ g.append(sid(this.toggleRow("Borders", boolOf("candleBorderVisible", config.candles.borderVisible), (v) => this.emitType(def.id, "candleBorderVisible", v), [
26415
26552
  this.swatch(colorOf2("candleBorderUpColor", config.candles.borderUpColor), (v) => this.emitType(def.id, "candleBorderUpColor", v)),
26416
26553
  this.swatch(colorOf2("candleBorderDownColor", config.candles.borderDownColor), (v) => this.emitType(def.id, "candleBorderDownColor", v))
26417
- ]));
26418
- g.append(this.toggleRow("Wick", boolOf("candleWickVisible", config.candles.wickVisible), (v) => this.emitType(def.id, "candleWickVisible", v), [
26554
+ ]), `symbol.style.${def.id}.borders`));
26555
+ g.append(sid(this.toggleRow("Wick", boolOf("candleWickVisible", config.candles.wickVisible), (v) => this.emitType(def.id, "candleWickVisible", v), [
26419
26556
  this.swatch(colorOf2("candleWickUpColor", config.candles.wickUpColor), (v) => this.emitType(def.id, "candleWickUpColor", v)),
26420
26557
  this.swatch(colorOf2("candleWickDownColor", config.candles.wickDownColor), (v) => this.emitType(def.id, "candleWickDownColor", v))
26421
- ]));
26422
- g.append(this.numberRow("Spacing", config.series.spacing, 0.1, 10, 0.1, (v) => this.emit({ series: { spacing: v } })));
26558
+ ]), `symbol.style.${def.id}.wick`));
26559
+ g.append(sid(this.numberRow("Spacing", config.series.spacing, 0.1, 10, 0.1, (v) => this.emit({ series: { spacing: v } })), `symbol.style.${def.id}.spacing`));
26423
26560
  groups[def.id] = g;
26424
26561
  body.append(g);
26425
26562
  }
26426
26563
  showActive(config.series.style);
26427
- body.append(this.sectionTitle("Time zone"));
26428
- body.append(this.selectRowLabeled("Time zone", normalizeTimezone(config.timeScale.timezone), timezoneOptions(config.timeScale.timezone), (v) => this.emit({ timeScale: { timezone: v } })));
26564
+ body.append(sid(this.sectionTitle("Time zone"), "symbol.timezone"));
26565
+ body.append(sid(this.selectRowLabeled("Time zone", normalizeTimezone(config.timeScale.timezone), timezoneOptions(config.timeScale.timezone), (v) => this.emit({ timeScale: { timezone: v } })), "symbol.timezone"));
26429
26566
  const renderHostSections = (placement) => {
26430
26567
  for (const hs of this.hostSections) {
26431
26568
  if ((hs.placement ?? "after-symbol") !== placement) continue;
26569
+ const scope = hostSectionId(hs);
26570
+ if (settingsIdHidden(scope, this.hiddenSettings)) continue;
26571
+ const rows = filterHiddenHostRows(hs.rows, scope, this.hiddenSettings);
26572
+ if (rows.length === 0) continue;
26432
26573
  body.append(placement === "symbol" ? this.sectionTitle(hs.title) : this.section(hs.title));
26433
- for (const hr of hs.rows) {
26574
+ for (const hr of rows) {
26434
26575
  if (hr.kind === "heading") body.append(this.sectionTitle(hr.label));
26435
26576
  else if (hr.kind === "toggle") body.append(this.boolRow(hr.label, hr.get(), (v) => hr.set(v)));
26436
26577
  else if (hr.kind === "color") body.append(this.colorRow(hr.label, hr.get(), (v) => hr.set(v)));
@@ -26443,53 +26584,69 @@ var SettingsDialog = class {
26443
26584
  const typeSettings = def.settings;
26444
26585
  if (!typeSettings) continue;
26445
26586
  if ((typeSettings.placement ?? "end") !== placement) continue;
26587
+ if (settingsIdHidden(`type:${def.id}`, this.hiddenSettings)) continue;
26446
26588
  this.chartTypeSection(def.id, typeSettings, config, body);
26447
26589
  }
26448
26590
  };
26449
26591
  renderHostSections("symbol");
26450
26592
  renderChartTypeSections("after-symbol");
26451
26593
  renderHostSections("after-symbol");
26452
- body.append(this.section("Scales and lines"));
26453
- body.append(this.sectionTitle("Price scale"));
26594
+ body.append(sid(this.section("Scales and lines"), "scales"));
26595
+ body.append(sid(this.sectionTitle("Price scale"), "scales.price-scale"));
26454
26596
  body.append(
26455
- this.selectRowLabeled(
26597
+ sid(this.selectRowLabeled(
26456
26598
  "Mode",
26457
26599
  config.priceScale.log ? "log" : config.priceScale.mode,
26458
26600
  [["price", "Regular"], ["percent", "Percent"], ["indexed", "Indexed to 100"], ["log", "Logarithmic"]],
26459
26601
  (v) => this.emit({ priceScale: v === "log" ? { mode: "price", log: true } : { mode: v, log: false } })
26460
- )
26602
+ ), "scales.price-scale.mode")
26461
26603
  );
26462
- body.append(this.boolRow("Invert scale", config.priceScale.invert, (v) => this.emit({ priceScale: { invert: v } })));
26463
- body.append(this.separator());
26464
- body.append(this.boolRow("Last Price Line", config.priceScale.currentPriceLine, (v) => this.emit({ priceScale: { currentPriceLine: v } })));
26465
- body.append(this.boolRow("Last price label", config.priceScale.priceLabel, (v) => this.emit({ priceScale: { priceLabel: v } })));
26466
- body.append(this.boolRow("Countdown to bar close", config.priceScale.countdown, (v) => this.emit({ priceScale: { countdown: v } })));
26467
- body.append(this.boolRow("Axis labels", config.priceScale.labelsVisible, (v) => this.emit({ priceScale: { labelsVisible: v } })));
26468
- body.append(this.colorRow("Scale border color", config.priceScale.borderColor, (v) => this.emit({ priceScale: { borderColor: v } })));
26469
- body.append(this.sectionTitle("Crosshair"));
26470
- body.append(this.colorRow("Color", config.crosshair.color, (v) => this.emit({ crosshair: { color: v } })));
26471
- body.append(this.numberRow("Width", config.crosshair.width, 0.5, 8, 0.5, (v) => this.emit({ crosshair: { width: v } })));
26472
- body.append(this.selectRowLabeled("Style", config.crosshair.style, [["solid", "Solid"], ["dashed", "Dashed"], ["dotted", "Dotted"]], (v) => this.emit({ crosshair: { style: v } })));
26473
- body.append(this.section("Canvas"));
26474
- body.append(this.sectionTitle("Background & text"));
26475
- body.append(this.colorRow("Background", config.layout.background, (v) => this.emit({ layout: { background: v } })));
26476
- body.append(this.colorRow("Text color", config.layout.textColor, (v) => this.emit({ layout: { textColor: v } })));
26477
- body.append(this.numberRow("Text size", config.layout.fontSize, 6, 32, 1, (v) => this.emit({ layout: { fontSize: v } })));
26478
- body.append(this.colorRow("Pane separator color", config.panes.separatorColor, (v) => this.emit({ panes: { separatorColor: v } })));
26479
- body.append(this.sectionTitle("Grid"));
26480
- body.append(this.toggleRow("Vertical", config.grid.vertLines.visible, (v) => this.emit({ grid: { vertLines: { visible: v } } }), [
26604
+ body.append(sid(this.boolRow("Invert scale", config.priceScale.invert, (v) => this.emit({ priceScale: { invert: v } })), "scales.price-scale.invert"));
26605
+ body.append(sid(this.separator(), "scales.price-scale"));
26606
+ body.append(sid(this.boolRow("Last Price Line", config.priceScale.currentPriceLine, (v) => this.emit({ priceScale: { currentPriceLine: v } })), "scales.price-scale.last-price-line"));
26607
+ body.append(sid(this.boolRow("Last price label", config.priceScale.priceLabel, (v) => this.emit({ priceScale: { priceLabel: v } })), "scales.price-scale.last-price-label"));
26608
+ body.append(sid(this.boolRow("Countdown to bar close", config.priceScale.countdown, (v) => this.emit({ priceScale: { countdown: v } })), "scales.price-scale.countdown"));
26609
+ body.append(sid(this.boolRow("Axis labels", config.priceScale.labelsVisible, (v) => this.emit({ priceScale: { labelsVisible: v } })), "scales.price-scale.axis-labels"));
26610
+ body.append(sid(this.colorRow("Scale border color", config.priceScale.borderColor, (v) => this.emit({ priceScale: { borderColor: v } })), "scales.price-scale.border-color"));
26611
+ body.append(sid(this.sectionTitle("Crosshair"), "scales.crosshair"));
26612
+ body.append(sid(this.colorRow("Color", config.crosshair.color, (v) => this.emit({ crosshair: { color: v } })), "scales.crosshair.color"));
26613
+ body.append(sid(this.numberRow("Width", config.crosshair.width, 0.5, 8, 0.5, (v) => this.emit({ crosshair: { width: v } })), "scales.crosshair.width"));
26614
+ body.append(sid(this.selectRowLabeled("Style", config.crosshair.style, [["solid", "Solid"], ["dashed", "Dashed"], ["dotted", "Dotted"]], (v) => this.emit({ crosshair: { style: v } })), "scales.crosshair.style"));
26615
+ body.append(sid(this.section("Canvas"), "canvas"));
26616
+ body.append(sid(this.sectionTitle("Background & text"), "canvas.background"));
26617
+ body.append(sid(this.colorRow("Background", config.layout.background, (v) => this.emit({ layout: { background: v } })), "canvas.background.color"));
26618
+ body.append(sid(this.colorRow("Text color", config.layout.textColor, (v) => this.emit({ layout: { textColor: v } })), "canvas.background.text-color"));
26619
+ body.append(sid(this.numberRow("Text size", config.layout.fontSize, 6, 32, 1, (v) => this.emit({ layout: { fontSize: v } })), "canvas.background.text-size"));
26620
+ body.append(sid(this.colorRow("Pane separator color", config.panes.separatorColor, (v) => this.emit({ panes: { separatorColor: v } })), "canvas.background.pane-separator"));
26621
+ body.append(sid(this.sectionTitle("Grid"), "canvas.grid"));
26622
+ body.append(sid(this.toggleRow("Vertical", config.grid.vertLines.visible, (v) => this.emit({ grid: { vertLines: { visible: v } } }), [
26481
26623
  this.swatch(config.grid.vertLines.color, (v) => this.emit({ grid: { vertLines: { color: v } } }))
26482
- ]));
26483
- body.append(this.toggleRow("Horizontal", config.grid.horzLines.visible, (v) => this.emit({ grid: { horzLines: { visible: v } } }), [
26624
+ ]), "canvas.grid.vertical"));
26625
+ body.append(sid(this.toggleRow("Horizontal", config.grid.horzLines.visible, (v) => this.emit({ grid: { horzLines: { visible: v } } }), [
26484
26626
  this.swatch(config.grid.horzLines.color, (v) => this.emit({ grid: { horzLines: { color: v } } }))
26485
- ]));
26627
+ ]), "canvas.grid.horizontal"));
26486
26628
  if (this.themeControl) {
26487
26629
  const tc = this.themeControl;
26488
- body.append(this.sectionTitle("Theme"));
26489
- body.append(this.selectRow("Color theme", tc.current === "dark" ? "Dark" : "Light", ["Dark", "Light"], (v) => tc.onSelect(v === "Dark" ? "dark" : "light")));
26630
+ body.append(sid(this.sectionTitle("Theme"), "canvas.theme"));
26631
+ body.append(sid(this.selectRow("Color theme", tc.current === "dark" ? "Dark" : "Light", ["Dark", "Light"], (v) => tc.onSelect(v === "Dark" ? "dark" : "light")), "canvas.theme"));
26490
26632
  }
26491
26633
  renderChartTypeSections("end");
26492
26634
  renderHostSections("end");
26635
+ if (this.hiddenSettings.size > 0) {
26636
+ let skipTab = false;
26637
+ for (const child of [...body.children]) {
26638
+ if (child.dataset.sdTab !== void 0) {
26639
+ skipTab = child.dataset.sdId !== void 0 && settingsIdHidden(child.dataset.sdId, this.hiddenSettings);
26640
+ }
26641
+ if (skipTab || child.dataset.sdId !== void 0 && settingsIdHidden(child.dataset.sdId, this.hiddenSettings)) {
26642
+ child.remove();
26643
+ continue;
26644
+ }
26645
+ for (const el of [...child.querySelectorAll("[data-sd-id]")]) {
26646
+ if (settingsIdHidden(el.dataset.sdId, this.hiddenSettings)) el.remove();
26647
+ }
26648
+ }
26649
+ }
26493
26650
  const shell = document.createElement("div");
26494
26651
  shell.style.cssText = mobile ? "display:flex;min-height:0;flex:1 1 auto;position:relative;overflow:hidden;" : "display:flex;min-height:360px;max-height:calc(70vh - 100px);flex:1 1 auto;";
26495
26652
  const rail = document.createElement("div");
@@ -26527,6 +26684,9 @@ var SettingsDialog = class {
26527
26684
  }
26528
26685
  if (current) current.appendChild(child);
26529
26686
  }
26687
+ for (let i = panes.length - 1; i >= 0; i--) {
26688
+ if (panes[i].el.childElementCount === 0) panes.splice(i, 1);
26689
+ }
26530
26690
  this.syncTypeTabs = (active) => {
26531
26691
  let hidActive = false;
26532
26692
  panes.forEach((pn, idx) => {
@@ -26666,19 +26826,25 @@ var SettingsDialog = class {
26666
26826
  this.emitType(typeId, key, v);
26667
26827
  for (const r of refreshers) r();
26668
26828
  };
26829
+ const scope = `type:${typeId}`;
26669
26830
  if (section.instances && section.instances.length > 0) {
26670
- body.append(this.instancesBlock(typeId, section.instances, bag, put, refreshers));
26831
+ const instances = section.instances.map((inst) => ({ ...inst, rows: filterHiddenRows(inst.rows, scope, this.hiddenSettings) }));
26832
+ body.append(this.instancesBlock(typeId, instances, bag, put, refreshers));
26671
26833
  } else if (section.rows) {
26672
- if (section.layout === "grouped") body.append(this.groupedRows(`${typeId}/rows`, section.rows, bag, put, refreshers));
26673
- else this.flatTypeRows(section.rows, bag, put, refreshers, body);
26834
+ const rows = filterHiddenRows(section.rows, scope, this.hiddenSettings);
26835
+ if (section.layout === "grouped") body.append(this.groupedRows(`${typeId}/rows`, rows, bag, put, refreshers));
26836
+ else this.flatTypeRows(rows, bag, put, refreshers, body);
26674
26837
  }
26675
26838
  for (const sub of section.subsections ?? []) {
26839
+ if (settingsIdHidden(`${scope}.${settingsIdSlug(sub.title)}`, this.hiddenSettings)) continue;
26840
+ const rows = filterHiddenRows(sub.rows, scope, this.hiddenSettings);
26841
+ if (rows.length === 0) continue;
26676
26842
  const subMarker = this.section(sub.title);
26677
26843
  subMarker.dataset.sdStyle = typeId;
26678
26844
  subMarker.dataset.sdVisibility = section.visibility ?? "active";
26679
26845
  subMarker.dataset.sdSub = "1";
26680
26846
  body.append(subMarker);
26681
- body.append(this.groupedRows(`${typeId}/${sub.title}`, sub.rows, bag, put, refreshers, sub.enableKey));
26847
+ body.append(this.groupedRows(`${typeId}/${sub.title}`, rows, bag, put, refreshers, sub.enableKey));
26682
26848
  }
26683
26849
  for (const r of refreshers) r();
26684
26850
  }
@@ -30968,6 +31134,94 @@ function resizeSplit(split, dyTotal, minPx = MIN_PANE_PX) {
30968
31134
  return { above, below: combinedWeight - above };
30969
31135
  }
30970
31136
 
31137
+ // src/renderers/native/backdrop/BackdropRenderer.ts
31138
+ var BackdropRenderer = class {
31139
+ constructor() {
31140
+ this.canvas = null;
31141
+ this.ctx = null;
31142
+ }
31143
+ mount(canvas) {
31144
+ this.canvas = canvas;
31145
+ this.ctx = canvas.getContext("2d");
31146
+ }
31147
+ destroy() {
31148
+ this.canvas = null;
31149
+ this.ctx = null;
31150
+ }
31151
+ /** Paint one frame: highlight bands first, gridlines on top (the order they had inside
31152
+ * the data canvas). `gridAlpha` fades the gridlines as a reveal-under layer opens. */
31153
+ render(scene, coords, theme, gridAlpha) {
31154
+ const ctx = this.ctx;
31155
+ const canvas = this.canvas;
31156
+ if (!ctx || !canvas) return;
31157
+ const dpr = coords.dpr;
31158
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
31159
+ ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
31160
+ const n = coords.barCount;
31161
+ if (n === 0) return;
31162
+ const vr = coords.visibleLogicalRange();
31163
+ if (Math.min(n - 1, Math.ceil(vr.to)) < Math.max(0, Math.floor(vr.from))) return;
31164
+ this.drawHighlights(ctx, scene, coords);
31165
+ this.drawGrid(ctx, scene, coords, theme, coords.width, gridAlpha);
31166
+ }
31167
+ /** Renderer-owned session highlight bands: full-height (all panes), behind the grid.
31168
+ * Session-zone washes (pre/post-market) paint first, host highlights on top. */
31169
+ drawHighlights(ctx, scene, coords) {
31170
+ const bands = [...scene.sessionHighlightBands(), ...scene.highlights];
31171
+ if (bands.length === 0) return;
31172
+ for (const band of bands) {
31173
+ const x1 = coords.timeToX(band.from);
31174
+ const x2 = coords.timeToX(band.to);
31175
+ if (x2 < 0 || x1 > coords.width || x2 <= x1) continue;
31176
+ const cx = Math.max(0, x1);
31177
+ const cw = Math.min(coords.width, x2) - cx;
31178
+ if (cw <= 0) continue;
31179
+ ctx.fillStyle = band.color;
31180
+ ctx.fillRect(cx, 0, cw, coords.height);
31181
+ }
31182
+ }
31183
+ // ── grid ── vert/horz gate on `scene.showGrid` AND their own per-axis visibility
31184
+ // (style); each uses its own color. Pane separators are drawn on the chrome layer
31185
+ // (full-width, above the data) so series never overpaint them.
31186
+ drawGrid(ctx, scene, coords, theme, dataW, gridAlpha) {
31187
+ const panes = scene.orderedPanes();
31188
+ const { gridVert, gridHorz } = scene.style;
31189
+ const vertColor = gridVert.color ?? theme.gridColor;
31190
+ const horzColor = gridHorz.color ?? theme.gridColor;
31191
+ ctx.lineWidth = 1;
31192
+ if (scene.showGrid && gridVert.visible) {
31193
+ ctx.globalAlpha = gridAlpha;
31194
+ ctx.strokeStyle = vertColor;
31195
+ const tr = coords.visibleTimeRange();
31196
+ const offset = tzOffsetMs((tr.from + tr.to) / 2, scene.timezone);
31197
+ ctx.beginPath();
31198
+ for (const tick of timeTicks(tr.from, tr.to, 8, offset)) {
31199
+ const x = Math.round(coords.timeToX(tick.time)) + 0.5;
31200
+ if (x < 0 || x > dataW) continue;
31201
+ ctx.moveTo(x, 0);
31202
+ ctx.lineTo(x, coords.height);
31203
+ }
31204
+ ctx.stroke();
31205
+ }
31206
+ for (const pane of panes) {
31207
+ if (scene.showGrid && gridHorz.visible && !pane.collapsed) {
31208
+ ctx.globalAlpha = gridAlpha;
31209
+ ctx.strokeStyle = horzColor;
31210
+ const pct = percentScaleFor(scene, pane);
31211
+ ctx.beginPath();
31212
+ for (const t of paneAxisTicks(pane.scale, pane.bounds.height, pct, void 0, pane.axisFormat)) {
31213
+ const y = Math.round(coords.priceToY(t.price, pane.scale, pane.bounds)) + 0.5;
31214
+ if (y < pane.bounds.top || y > pane.bounds.top + pane.bounds.height) continue;
31215
+ ctx.moveTo(0, y);
31216
+ ctx.lineTo(dataW, y);
31217
+ }
31218
+ ctx.stroke();
31219
+ }
31220
+ }
31221
+ ctx.globalAlpha = 1;
31222
+ }
31223
+ };
31224
+
30971
31225
  // src/renderers/native/volume/paintVolume.ts
30972
31226
  var VOLUME_FILL_ALPHA = 0.5;
30973
31227
  function paintVolume(ctx, bars, geom, colors) {
@@ -31443,6 +31697,7 @@ var NativeRenderer = class {
31443
31697
  // px reserved on the left for the docked drawings toolbar (0 when hidden)
31444
31698
  this.mountContainer = null;
31445
31699
  this.userDrawings = null;
31700
+ this.backdropRenderer = new BackdropRenderer();
31446
31701
  this.volumeRenderer = new VolumeRenderer();
31447
31702
  /** SDK renderer layers instantiated at mount ({@link registerRendererLayer}). */
31448
31703
  this.extLayers = [];
@@ -31570,6 +31825,9 @@ var NativeRenderer = class {
31570
31825
  // bar under the crosshair; null when off a bar
31571
31826
  // ── settings dialog (rich, serializable config — item 15) ──
31572
31827
  this.settingsDialog = null;
31828
+ /** The host's visibility policy (setting ids hidden from the dialog) — instance
31829
+ * state, never part of the persisted config. */
31830
+ this.hiddenSettings = [];
31573
31831
  /** Where modal dialogs mount — a HOST override (multi-chart shells pass their root
31574
31832
  * so dialogs center globally instead of clipping inside one cell). Null = the plot. */
31575
31833
  this.dialogHost = null;
@@ -32242,6 +32500,7 @@ var NativeRenderer = class {
32242
32500
  }
32243
32501
  this.settingsDialog.setTheme(this.theme);
32244
32502
  this.settingsDialog.setHostSections(this.hostSettingsSections);
32503
+ this.settingsDialog.setHiddenSettings(this.hiddenSettings);
32245
32504
  this.syncThemeControl();
32246
32505
  this.settingsDialog.toggle(
32247
32506
  this.getConfig(),
@@ -32489,6 +32748,8 @@ var NativeRenderer = class {
32489
32748
  this.wrapper = document.createElement("div");
32490
32749
  Object.assign(this.wrapper.style, { position: "relative", width: "100%", height: "100%", overflow: "hidden", cursor: "crosshair", userSelect: "none", webkitUserSelect: "none" });
32491
32750
  applyChromeTokens(this.wrapper, this.chromeTheme());
32751
+ this.backdropCanvas = document.createElement("canvas");
32752
+ Object.assign(this.backdropCanvas.style, { position: "absolute", inset: "0", width: "100%", height: "100%", pointerEvents: "none" });
32492
32753
  this.volumeCanvas = document.createElement("canvas");
32493
32754
  Object.assign(this.volumeCanvas.style, { position: "absolute", inset: "0", width: "100%", height: "100%", pointerEvents: "none" });
32494
32755
  this.dataCanvas = this.createGeometryBackend();
@@ -32511,7 +32772,7 @@ var NativeRenderer = class {
32511
32772
  });
32512
32773
  const below = this.extLayers.filter((l) => l.def.placement === "below-data").map((l) => l.canvas);
32513
32774
  const above = this.extLayers.filter((l) => l.def.placement !== "below-data").map((l) => l.canvas);
32514
- this.plot.append(...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above, this.chromeCanvas, this.drawingsCanvas, this.cursorCanvas, this.overlayRoot);
32775
+ this.plot.append(this.backdropCanvas, ...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above, this.chromeCanvas, this.drawingsCanvas, this.cursorCanvas, this.overlayRoot);
32515
32776
  this.layerOrderSig = "";
32516
32777
  this.wrapper.appendChild(this.plot);
32517
32778
  this.factoryConfig = this.getConfig();
@@ -32521,6 +32782,7 @@ var NativeRenderer = class {
32521
32782
  this.wrapper.appendChild(this.attributionEl);
32522
32783
  container.appendChild(this.wrapper);
32523
32784
  this.applyBackground();
32785
+ this.backdropRenderer.mount(this.backdropCanvas);
32524
32786
  this.volumeRenderer.mount(this.volumeCanvas);
32525
32787
  this.vpvrRenderer.mount(this.vpvrCanvas);
32526
32788
  for (const l of this.extLayers) l.instance.mount(l.canvas);
@@ -32810,6 +33072,7 @@ var NativeRenderer = class {
32810
33072
  this.scrollButton = null;
32811
33073
  for (const l of this.extLayers) l.instance.destroy?.();
32812
33074
  this.extLayers = [];
33075
+ this.backdropRenderer.destroy();
32813
33076
  this.volumeRenderer.destroy();
32814
33077
  this.vpvrRenderer.destroy();
32815
33078
  this.backend.destroy();
@@ -33189,6 +33452,13 @@ var NativeRenderer = class {
33189
33452
  this.hostSettingsSections = sections;
33190
33453
  this.settingsDialog?.setHostSections(sections);
33191
33454
  }
33455
+ setSettingsVisibility(policy) {
33456
+ this.hiddenSettings = [...policy.hidden ?? []];
33457
+ this.settingsDialog?.setHiddenSettings(this.hiddenSettings);
33458
+ }
33459
+ listSettingsIds() {
33460
+ return settingsIdCatalog(this.hostSettingsSections);
33461
+ }
33192
33462
  onChartTypeSettingsChange(cb) {
33193
33463
  this.chartTypeSettingsCbs.add(cb);
33194
33464
  return () => this.chartTypeSettingsCbs.delete(cb);
@@ -33895,7 +34165,7 @@ var NativeRenderer = class {
33895
34165
  this.backend.modelAlpha = this.modelAlpha;
33896
34166
  this.backend.candleBodyAlpha = this.candleBodyAlpha;
33897
34167
  this.backend.candleStructureAlpha = this.candleStructureAlpha;
33898
- this.backend.gridAlpha = 1;
34168
+ let gridAlpha = 1;
33899
34169
  const candleBodyScale = 1;
33900
34170
  this.backend.candleBodyScale = candleBodyScale;
33901
34171
  const pane = this.scene.panes.get(PRICE_PANE_ID2);
@@ -33934,7 +34204,7 @@ var NativeRenderer = class {
33934
34204
  if (folded) {
33935
34205
  if (folded.candleBodyScale != null) this.backend.candleBodyScale = clamp012(folded.candleBodyScale) || 0.01;
33936
34206
  if (folded.candleBodyAlpha != null) this.backend.candleBodyAlpha = clamp012(folded.candleBodyAlpha) * this.candleBodyAlpha;
33937
- if (folded.gridAlpha != null) this.backend.gridAlpha = clamp012(folded.gridAlpha);
34207
+ if (folded.gridAlpha != null) gridAlpha = clamp012(folded.gridAlpha);
33938
34208
  }
33939
34209
  }
33940
34210
  const li = this.bars.length - 1;
@@ -33942,6 +34212,7 @@ var NativeRenderer = class {
33942
34212
  const easeLive = !!liveActual && this.liveEaseTime === liveActual.time && (liveActual.high !== this.liveEaseHigh || liveActual.low !== this.liveEaseLow || liveActual.close !== this.liveEaseClose);
33943
34213
  if (easeLive && liveActual) this.bars[li] = { ...liveActual, high: this.liveEaseHigh, low: this.liveEaseLow, close: this.liveEaseClose };
33944
34214
  this.scene.drawingSlices = this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map();
34215
+ this.backdropRenderer.render(this.scene, this.coords, this.theme, gridAlpha);
33945
34216
  this.backend.render(this.scene, this.coords, this.theme);
33946
34217
  this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
33947
34218
  this.userDrawings?.render();
@@ -34084,6 +34355,7 @@ var NativeRenderer = class {
34084
34355
  pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
34085
34356
  pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
34086
34357
  pane.axisFormat = void 0;
34358
+ pane.axisBands = void 0;
34087
34359
  if (this.volumeOwnsPane(pane, masterModels)) {
34088
34360
  const maxVol = this.maxVisibleVolume(i0, i1);
34089
34361
  if (maxVol > 0) {
@@ -34093,6 +34365,11 @@ var NativeRenderer = class {
34093
34365
  } else if (this.layerNativesOwnPane(pane, masterModels)) {
34094
34366
  pane.scaleTarget = computePaneScale([], this.bars, true, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
34095
34367
  pane.percentBaseline = this.bars[i0]?.close ?? 0;
34368
+ if (masterModels.every((m) => m.paneAxis != null)) {
34369
+ pane.axisFormat = "none";
34370
+ const banded = masterModels.find((m) => typeof m.paneAxis === "object");
34371
+ pane.axisBands = banded ? banded.paneAxis.bands : void 0;
34372
+ }
34096
34373
  }
34097
34374
  if (pane === pricePane && this.scene.tradeMarkers.visible && pane.bounds.height > 0) {
34098
34375
  const th = this.tradesScaleHints(vr);
@@ -34213,11 +34490,11 @@ var NativeRenderer = class {
34213
34490
  );
34214
34491
  return { below: below.map((id) => byId.get(id)), above: above.map((id) => byId.get(id)) };
34215
34492
  }
34216
- /** The full canvas pile in paint order (layers + data/volume/vpvr) — what the DOM
34217
- * stacking and the screenshot compositor must both follow. */
34493
+ /** The full canvas pile in paint order (backdrop + layers + data/volume/vpvr) — what
34494
+ * the DOM stacking and the screenshot compositor must both follow. */
34218
34495
  canvasPile() {
34219
34496
  const { below, above } = this.orderedLayerCanvases();
34220
- return [...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above];
34497
+ return [this.backdropCanvas, ...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above];
34221
34498
  }
34222
34499
  /** Re-slot the SDK layer canvases in the plot when the computed order changed (a z
34223
34500
  * write, a restored config, an indicator mount/remove/restack). Runs at the top of
@@ -34534,6 +34811,8 @@ var NativeRenderer = class {
34534
34811
  const ph = h;
34535
34812
  this.dataCanvas.width = Math.round(pw * dpr);
34536
34813
  this.dataCanvas.height = Math.round(ph * dpr);
34814
+ this.backdropCanvas.width = this.dataCanvas.width;
34815
+ this.backdropCanvas.height = this.dataCanvas.height;
34537
34816
  this.volumeCanvas.width = this.dataCanvas.width;
34538
34817
  this.volumeCanvas.height = this.dataCanvas.height;
34539
34818
  for (const l of this.extLayers) {
@@ -34675,35 +34954,6 @@ function normalizeBars(bars) {
34675
34954
  return out;
34676
34955
  }
34677
34956
 
34678
- // src/core/price-styles/heikin-ashi.ts
34679
- function heikinAshiNext(raw, prevHa) {
34680
- const haClose = (raw.open + raw.high + raw.low + raw.close) / 4;
34681
- const haOpen = prevHa ? (prevHa.open + prevHa.close) / 2 : (raw.open + raw.close) / 2;
34682
- return {
34683
- time: raw.time,
34684
- open: haOpen,
34685
- high: Math.max(raw.high, haOpen, haClose),
34686
- low: Math.min(raw.low, haOpen, haClose),
34687
- close: haClose,
34688
- ...raw.volume != null ? { volume: raw.volume } : {}
34689
- };
34690
- }
34691
- function heikinAshiFull(raw) {
34692
- const out = new Array(raw.length);
34693
- let prev2;
34694
- for (let i = 0; i < raw.length; i += 1) {
34695
- prev2 = heikinAshiNext(raw[i], prev2);
34696
- out[i] = prev2;
34697
- }
34698
- return out;
34699
- }
34700
-
34701
- // src/chart-types/builtins.ts
34702
- var HEIKIN_ASHI = { full: heikinAshiFull, next: heikinAshiNext };
34703
- function registerBuiltinChartTypes() {
34704
- registerChartType({ id: "heikinashi", label: "Heikin Ashi", barTransform: HEIKIN_ASHI });
34705
- }
34706
-
34707
34957
  // src/core/native-indicators/volume/VolumeIndicator.ts
34708
34958
  var DEFAULT_UP = BULLISH;
34709
34959
  var DEFAULT_DOWN = BEARISH;
@@ -34900,6 +35150,7 @@ var Vela = class {
34900
35150
  // the volume indicator is on by default
34901
35151
  };
34902
35152
  this.rendererControl = new RendererControl(renderer);
35153
+ if (options.settings) this.rendererControl.setSettingsVisibility(options.settings);
34903
35154
  this.dataControl = new DataControl(feed);
34904
35155
  this.orchestrator = new EngineOrchestrator(element, renderer, feed, engines, config, this.dataControl);
34905
35156
  const defaults2 = rendererDefaults();
@@ -36030,8 +36281,8 @@ function seedDefaults(opts) {
36030
36281
  };
36031
36282
  }
36032
36283
  function cellChartDefaults(opts) {
36033
- const { renderer, defaultLanguage, currentPriceLine, logScale, animations, glow, upColor, downColor, drawings } = opts;
36034
- return { renderer, defaultLanguage, currentPriceLine, logScale, animations, glow, upColor, downColor, drawings };
36284
+ const { renderer, defaultLanguage, currentPriceLine, logScale, animations, glow, upColor, downColor, drawings, settings } = opts;
36285
+ return { renderer, defaultLanguage, currentPriceLine, logScale, animations, glow, upColor, downColor, drawings, settings };
36035
36286
  }
36036
36287
  function cellDrawings(opt) {
36037
36288
  if (opt === false) return false;
@@ -36044,7 +36295,11 @@ var ChartCell = class {
36044
36295
  this.deps = deps;
36045
36296
  /** This cell's unified app+drawings undo timeline (the shared Ctrl+Z routes here). */
36046
36297
  this.history = new WidgetHistory(() => this.inner);
36047
- /** Live manifest-indicator instances on this cell (the SAME entry may repeat). */
36298
+ /** Live manifest-indicator instances on this cell (the SAME entry may repeat).
36299
+ * `external` marks instances added through the public seam (`ctx.addIndicator`)
36300
+ * rather than the shell manifest — they share the undo/redo and picker plumbing
36301
+ * but stay OUT of the persisted ledger (their names would never resolve against
36302
+ * the manifest); persisting them is their plugin's job (`registerStatePersistence`). */
36048
36303
  this.instances = [];
36049
36304
  /** The native-indicator catalog with this cell's live supported/present flags. */
36050
36305
  this.nativeCatalog = [];
@@ -36069,6 +36324,14 @@ var ChartCell = class {
36069
36324
  this.presentNatives = [];
36070
36325
  this.rangeBars = 0;
36071
36326
  this.pendingRange = null;
36327
+ /** Last symbol we toasted "no provider serves this" for — once per symbol (the
36328
+ * core re-reports on every provider-index settle). */
36329
+ this.unresolvedToasted = null;
36330
+ /** The cell's third-party state bag (`ext` of the persisted per-chart state) —
36331
+ * seeded from the boot/restored document, refreshed by handler `serialize` calls at
36332
+ * dehydrate time. Entries with no registered handler this session ride along
36333
+ * verbatim, so a document never loses a plugin's state in the plugin's absence. */
36334
+ this.extState = {};
36072
36335
  /** Indicator titles (this cell's in-chart legend rows) shown. */
36073
36336
  this.indicatorTitlesOn = true;
36074
36337
  /** Plot values beside this cell's legend titles shown. */
@@ -36135,9 +36398,16 @@ var ChartCell = class {
36135
36398
  this.pendingManifestNames = [...seed.indicators.manifest];
36136
36399
  }
36137
36400
  this.volumeIntent = seed.indicators ? seed.indicators.natives.includes("volume") : deps.volume;
36401
+ this.extState = { ...seed.ext ?? {} };
36138
36402
  this.inner.on("load:end", () => {
36139
36403
  this.volumeMayBePending = false;
36140
36404
  });
36405
+ this.inner.on("data:unresolved", ({ symbol: symbol2, providers }) => {
36406
+ if (this.unresolvedToasted === symbol2) return;
36407
+ this.unresolvedToasted = symbol2;
36408
+ const list = providers.length > 0 ? providers.join(", ") : "none";
36409
+ this.deps.toast(`No registered provider serves "${symbol2}" (registered: ${list})`, "error", 6e3);
36410
+ });
36141
36411
  this.inner.on("load:start", () => this.watermark?.setLoading(true));
36142
36412
  this.inner.on("load:end", () => this.watermark?.setLoading(false));
36143
36413
  const tz = deps.timezone();
@@ -36298,11 +36568,13 @@ var ChartCell = class {
36298
36568
  const eth = "Extended hours (ETH)";
36299
36569
  const sessionSection = {
36300
36570
  title: "Trading session",
36571
+ id: "trading-session",
36301
36572
  placement: "symbol",
36302
36573
  rows: [
36303
36574
  {
36304
36575
  kind: "select",
36305
36576
  label: "Session",
36577
+ id: "session",
36306
36578
  options: [rth, eth],
36307
36579
  get: () => this.session === "extended" ? eth : rth,
36308
36580
  set: (v) => this.setSession(v === eth ? "extended" : "regular")
@@ -36310,12 +36582,14 @@ var ChartCell = class {
36310
36582
  {
36311
36583
  kind: "color",
36312
36584
  label: "Pre-market",
36585
+ id: "premarket-color",
36313
36586
  get: () => this.sessionShadeColor("premarketColor"),
36314
36587
  set: (v) => this.setSessionShadeColor("premarketColor", v)
36315
36588
  },
36316
36589
  {
36317
36590
  kind: "color",
36318
36591
  label: "Post-market",
36592
+ id: "postmarket-color",
36319
36593
  get: () => this.sessionShadeColor("postmarketColor"),
36320
36594
  set: (v) => this.setSessionShadeColor("postmarketColor", v)
36321
36595
  }
@@ -36323,11 +36597,13 @@ var ChartCell = class {
36323
36597
  };
36324
36598
  const advanced = {
36325
36599
  title: "Advanced",
36600
+ id: "advanced",
36326
36601
  placement: "end",
36327
36602
  rows: [
36328
36603
  {
36329
36604
  kind: "select",
36330
36605
  label: "Bars to fetch",
36606
+ id: "bars",
36331
36607
  options: ["500", "1000", "2000", "5000", "10000", "20000"],
36332
36608
  get: () => String(this.state.bars ?? 1e3),
36333
36609
  set: (v) => {
@@ -36340,11 +36616,13 @@ var ChartCell = class {
36340
36616
  };
36341
36617
  const watermarkSection = {
36342
36618
  title: "Watermark",
36619
+ id: "watermark",
36343
36620
  placement: "symbol",
36344
36621
  rows: [
36345
36622
  {
36346
36623
  kind: "toggle",
36347
36624
  label: "Symbol watermark",
36625
+ id: "visible",
36348
36626
  get: () => this.watermarkOn,
36349
36627
  set: (v) => this.setWatermarkVisible(v)
36350
36628
  }
@@ -36355,22 +36633,25 @@ var ChartCell = class {
36355
36633
  const sl = this.statusline;
36356
36634
  sections.push({
36357
36635
  title: "Status line",
36636
+ id: "status-line",
36358
36637
  rows: [
36359
- { kind: "heading", label: "Status line" },
36360
- { kind: "toggle", label: "Symbol name", get: () => sl.partVisible("name"), set: (v) => sl.setPartVisible("name", v) },
36361
- { kind: "toggle", label: "Market status", get: () => sl.partVisible("market"), set: (v) => sl.setPartVisible("market", v) },
36362
- { kind: "toggle", label: "OHLC values", get: () => sl.partVisible("ohlc"), set: (v) => sl.setPartVisible("ohlc", v) },
36363
- { kind: "toggle", label: "Bar change values", get: () => sl.partVisible("change"), set: (v) => sl.setPartVisible("change", v) },
36364
- { kind: "heading", label: "Indicators" },
36638
+ { kind: "heading", label: "Status line", id: "parts" },
36639
+ { kind: "toggle", label: "Symbol name", id: "name", get: () => sl.partVisible("name"), set: (v) => sl.setPartVisible("name", v) },
36640
+ { kind: "toggle", label: "Market status", id: "market", get: () => sl.partVisible("market"), set: (v) => sl.setPartVisible("market", v) },
36641
+ { kind: "toggle", label: "OHLC values", id: "ohlc", get: () => sl.partVisible("ohlc"), set: (v) => sl.setPartVisible("ohlc", v) },
36642
+ { kind: "toggle", label: "Bar change values", id: "change", get: () => sl.partVisible("change"), set: (v) => sl.setPartVisible("change", v) },
36643
+ { kind: "heading", label: "Indicators", id: "indicators" },
36365
36644
  {
36366
36645
  kind: "toggle",
36367
36646
  label: "Titles",
36647
+ id: "indicator-titles",
36368
36648
  get: () => this.indicatorTitlesOn,
36369
36649
  set: (v) => this.setIndicatorTitlesVisible(v)
36370
36650
  },
36371
36651
  {
36372
36652
  kind: "toggle",
36373
36653
  label: "Values",
36654
+ id: "indicator-values",
36374
36655
  get: () => this.indicatorValuesOn,
36375
36656
  set: (v) => this.setIndicatorValuesVisible(v)
36376
36657
  }
@@ -36423,6 +36704,7 @@ var ChartCell = class {
36423
36704
  /** Switch this cell's market in place (the chart instance survives). */
36424
36705
  setSymbol(symbol) {
36425
36706
  if (!this.inner || symbol === this.symbol) return;
36707
+ this.unresolvedToasted = null;
36426
36708
  void this.inner.setMarket({ symbol });
36427
36709
  }
36428
36710
  setTimeframe(timeframe) {
@@ -36513,6 +36795,41 @@ var ChartCell = class {
36513
36795
  for (const entry of list) if (entry.enabled) this.addManifestInstance(entry, { record: false });
36514
36796
  }
36515
36797
  }
36798
+ /**
36799
+ * Replace the indicator ledger: natives converge to the listed set (volume
36800
+ * included — removing it sticks, the core's auto-add respects the opt-out), and
36801
+ * manifest instances are re-created by name, held until the shared manifest
36802
+ * resolves. Convergence is state application, not user edits — nothing enters the
36803
+ * undo timeline.
36804
+ */
36805
+ applyIndicatorLedger(led) {
36806
+ const chart = this.inner;
36807
+ if (!chart) return;
36808
+ this.volumeIntent = led.natives.includes("volume");
36809
+ this.history.silently(() => {
36810
+ const present = chart.presentNativeIndicators();
36811
+ for (const type of led.natives) {
36812
+ if (!present.includes(type)) chart.addNativeIndicator(type);
36813
+ }
36814
+ for (const type of present) {
36815
+ if (!led.natives.includes(type)) chart.addNativeIndicator(type).remove();
36816
+ }
36817
+ for (const it of [...this.instances]) this.dropInstance(it);
36818
+ if (this.manifest.length > 0) {
36819
+ for (const name of led.manifest) {
36820
+ const entry = this.manifest.find((e) => e.name === name);
36821
+ if (entry) this.addManifestInstance(entry, { record: false });
36822
+ }
36823
+ this.pendingManifestNames = null;
36824
+ } else if (!this.deps.manifestSettled()) {
36825
+ this.pendingManifestNames = [...led.manifest];
36826
+ } else {
36827
+ this.pendingManifestNames = null;
36828
+ }
36829
+ });
36830
+ this.syncPresentNatives();
36831
+ this.refreshNativeCatalog();
36832
+ }
36516
36833
  /** The picker's library rows: supported natives first, then the manifest. */
36517
36834
  libraryRows() {
36518
36835
  return [
@@ -36542,10 +36859,20 @@ var ChartCell = class {
36542
36859
  if (index < present.length) this.removeNative(present[index].type);
36543
36860
  else this.removeInstance(index - present.length);
36544
36861
  }
36862
+ /**
36863
+ * Add a script indicator through the PUBLIC seam (`ctx.addIndicator`) — same undo/
36864
+ * redo and picker plumbing as a manifest entry, but flagged `external` so the
36865
+ * persisted ledger never records a name the manifest can't resolve (the plugin owns
36866
+ * persistence via `registerStatePersistence`). Recording follows the ambient mute:
36867
+ * a persistence handler's `restore` runs silently, a user-driven call records.
36868
+ */
36869
+ addExternalIndicator(entry) {
36870
+ this.addManifestInstance({ ...entry, enabled: true }, { external: true });
36871
+ }
36545
36872
  /** Add ONE instance of a manifest entry (repeatable — duplicates are legitimate). */
36546
36873
  addManifestInstance(entry, opts = {}) {
36547
36874
  if (this.destroyed) return;
36548
- const it = { entry, handle: this.addToChart(entry) };
36875
+ const it = { entry, handle: this.addToChart(entry), ...opts.external ? { external: true } : {} };
36549
36876
  this.instances.push(it);
36550
36877
  this.deps.onIndicatorsChanged(this.id);
36551
36878
  if (opts.record === false) return;
@@ -36626,12 +36953,97 @@ var ChartCell = class {
36626
36953
  return null;
36627
36954
  }
36628
36955
  }
36956
+ // ── third-party state (the `ext` seam) ──
36957
+ /** The cell-bound surface persistence handlers work against (built per call — the
36958
+ * widget-context rule; nothing here may be cached by a handler). Its add methods
36959
+ * are ALWAYS muted — a `restore` that fetches before adding escapes the sync mute
36960
+ * of {@link restorePersistedExt}, and a state application must never enter the
36961
+ * undo timeline, however late its continuation lands. */
36962
+ stateContext() {
36963
+ return {
36964
+ cellId: this.id,
36965
+ chart: this.chart,
36966
+ addIndicator: (entry) => this.history.silently(() => this.addExternalIndicator(entry)),
36967
+ addNativeIndicator: (type) => this.history.silently(() => this.addNative(type))
36968
+ };
36969
+ }
36970
+ /**
36971
+ * Run the registered cell-scope `restore` handlers against the cell's restored
36972
+ * `ext` bag — the workspace calls this AFTER the core state is in place (chart
36973
+ * alive and wired, indicator ledger converged). Muted: nothing a restore does
36974
+ * enters the undo timeline. Handlers only see keys the document carries; a failing
36975
+ * handler is contained (one broken plugin must not take the cell down).
36976
+ */
36977
+ restorePersistedExt() {
36978
+ if (this.destroyed) return;
36979
+ for (const h of statePersistenceHandlers("cell")) {
36980
+ if (!(h.key in this.extState)) continue;
36981
+ try {
36982
+ this.history.silently(() => h.restore(this.extState[h.key], this.stateContext()));
36983
+ } catch (err) {
36984
+ console.warn(`[vela] state persistence "${h.key}" restore failed:`, err);
36985
+ }
36986
+ }
36987
+ }
36988
+ /** Assemble the cell's `ext` bag: fresh handler snapshots merged OVER the preserved
36989
+ * entries — a key with no handler this session rides along verbatim; a registered
36990
+ * handler returning `undefined` withdraws its entry. */
36991
+ dehydrateExt() {
36992
+ const ext = { ...this.extState };
36993
+ for (const h of statePersistenceHandlers("cell")) {
36994
+ try {
36995
+ const value = h.serialize(this.stateContext());
36996
+ if (value === void 0) delete ext[h.key];
36997
+ else ext[h.key] = value;
36998
+ } catch (err) {
36999
+ console.warn(`[vela] state persistence "${h.key}" serialize failed:`, err);
37000
+ }
37001
+ }
37002
+ this.extState = ext;
37003
+ return Object.keys(ext).length > 0 ? ext : void 0;
37004
+ }
36629
37005
  // ── lifecycle ──
37006
+ /**
37007
+ * Apply a restored cell state IN PLACE — the chart instance survives (the market
37008
+ * switches via `setMarket`) while cosmetics, renderer config, drawings, and the
37009
+ * indicator ledger converge to the document. The workspace takes this path when a
37010
+ * state document lands on a grid of the same shape (async-storage boot, host
37011
+ * `applyState`), so chart references, indicator handles, event subscriptions, and
37012
+ * the cell host all stay valid.
37013
+ */
37014
+ rehydrate(cs) {
37015
+ if (!this.inner || this.destroyed) return;
37016
+ if (cs.priceStyle && cs.priceStyle !== this.priceStyle) this.setPriceStyle(cs.priceStyle);
37017
+ if (cs.watermark !== void 0 && cs.watermark !== this.watermarkOn) this.setWatermarkVisible(cs.watermark);
37018
+ if (cs.indicatorTitles !== void 0 && cs.indicatorTitles !== this.indicatorTitlesOn) this.setIndicatorTitlesVisible(cs.indicatorTitles);
37019
+ if (cs.indicatorValues !== void 0 && cs.indicatorValues !== this.indicatorValuesOn) this.setIndicatorValuesVisible(cs.indicatorValues);
37020
+ if (cs.rendererConfig != null) this.inner.renderer.applyConfig(cs.rendererConfig);
37021
+ if (cs.drawings != null) this.inner.drawings.fromJSON(cs.drawings);
37022
+ if (cs.indicators) this.applyIndicatorLedger(cs.indicators);
37023
+ this.extState = { ...cs.ext ?? {} };
37024
+ this.restorePersistedExt();
37025
+ const symbol = prefixedSymbol(cs);
37026
+ const session = normalizeSession(cs.session) ?? "regular";
37027
+ const bars = typeof cs.bars === "number" && Number.isFinite(cs.bars) && cs.bars > 0 ? cs.bars : 0;
37028
+ const next = {};
37029
+ if (symbol && symbol !== this.symbol) next.symbol = symbol;
37030
+ if (cs.timeframe && cs.timeframe !== this.timeframe) next.timeframe = cs.timeframe;
37031
+ if (session !== this.session) {
37032
+ this.state.session = session;
37033
+ next.session = session;
37034
+ }
37035
+ if (bars > 0 && bars !== this.state.bars) {
37036
+ this.state.bars = bars;
37037
+ next.bars = Math.max(bars, this.rangeBars);
37038
+ }
37039
+ if (Object.keys(next).length > 0) void this.inner.setMarket(next);
37040
+ }
36630
37041
  /** Snapshot everything the pool needs to restore this slot later. The market fields
36631
37042
  * come from the LIVE config (`chart.market`) — the requested identity — so a switch
36632
37043
  * still loading when the snapshot is taken (persist-on-close) is not lost. */
36633
37044
  dehydrate() {
36634
37045
  const live = this.inner?.market;
37046
+ const ext = this.inner ? this.dehydrateExt() : Object.keys(this.extState).length > 0 ? { ...this.extState } : void 0;
36635
37047
  return {
36636
37048
  ...this.state,
36637
37049
  ...live ? { symbol: live.symbol, provider: live.provider, timeframe: live.timeframe } : {},
@@ -36644,14 +37056,17 @@ var ChartCell = class {
36644
37056
  // Natives from the chart's SYNC registry read — an async catalog mirror here
36645
37057
  // lost unload-time saves, and the old empty-set fallbacks resurrected removed
36646
37058
  // indicators. Manifest names fall back to the restored ledger only until the
36647
- // shared manifest settles. See {@link indicatorLedger}.
37059
+ // shared manifest settles. See {@link indicatorLedger}. External instances
37060
+ // (`ctx.addIndicator`) stay out: their names would never resolve against the
37061
+ // manifest — their plugin persists them via the `ext` seam instead.
36648
37062
  indicators: indicatorLedger({
36649
37063
  present: this.inner ? this.inner.presentNativeIndicators() : [],
36650
- instanceNames: this.instances.map((it) => it.entry.name),
37064
+ instanceNames: this.instances.filter((it) => !it.external).map((it) => it.entry.name),
36651
37065
  pendingManifest: this.pendingManifestNames,
36652
37066
  manifestSettled: this.deps.manifestSettled(),
36653
37067
  volumePending: this.volumeMayBePending && this.volumeIntent
36654
- })
37068
+ }),
37069
+ ...ext ? { ext } : {}
36655
37070
  };
36656
37071
  }
36657
37072
  destroy() {
@@ -36688,6 +37103,9 @@ function buildContext(host) {
36688
37103
  togglePanel: (id, open2) => host.togglePanel(id, open2),
36689
37104
  host: host.root,
36690
37105
  toast: (message, kind) => host.toast(message, kind),
37106
+ addIndicator: (entry) => host.active()?.addExternalIndicator(entry),
37107
+ addNativeIndicator: (type) => host.active()?.addNative(type),
37108
+ stateChanged: () => host.stateDirty(),
36691
37109
  cells: host.cells().map((c) => ({ id: c.id, chart: c.chart, symbol: c.symbol, timeframe: c.timeframe })),
36692
37110
  activeCellId: active?.id ?? "",
36693
37111
  setActiveCell: (id) => host.setActiveCell(id)
@@ -37049,6 +37467,10 @@ var VelaWorkspace = class {
37049
37467
  /** Plot-local y of the last price-axis long-press (targets the pane under the finger). */
37050
37468
  this.priceScalePressY = 0;
37051
37469
  this.attachmentDisposers = /* @__PURE__ */ new Map();
37470
+ /** The document-level third-party state bag (`state.ext`) — seeded from the restored
37471
+ * document, refreshed by global-scope handler `serialize` calls at snapshot time.
37472
+ * Entries with no registered handler this session ride along verbatim. */
37473
+ this.extState = {};
37052
37474
  /** The single grid-wide attribution mark — re-inked on a live theme swap. */
37053
37475
  this.attributionMark = null;
37054
37476
  this.onRootKeydown = (ev) => this.routeTyping(ev);
@@ -37063,6 +37485,7 @@ var VelaWorkspace = class {
37063
37485
  state: () => ({ ...this.syncOpts })
37064
37486
  };
37065
37487
  registerBuiltinLayouts();
37488
+ registerBuiltinChartTypes();
37066
37489
  const hostEl = typeof container === "string" ? document.querySelector(container) : container;
37067
37490
  if (!hostEl) throw new Error(`VelaWorkspace: container not found: ${String(container)}`);
37068
37491
  this.opts = opts;
@@ -37086,8 +37509,12 @@ var VelaWorkspace = class {
37086
37509
  for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) {
37087
37510
  this.applySyncSetting(kind, sync?.[kind]);
37088
37511
  }
37089
- this.def = this.resolveLayout(boot?.layout && ensureLayout(boot.layout) ? boot.layout : opts.layout ?? "4");
37512
+ this.monoLayout = opts.layout === false;
37513
+ const optLayout = opts.layout === false || opts.layout === void 0 ? "4" : opts.layout;
37514
+ this.def = this.resolveLayout(this.monoLayout ? "1" : boot?.layout && ensureLayout(boot.layout) ? boot.layout : optLayout);
37515
+ this.alertCap = Math.max(1, opts.alertCap ?? ALERT_CAP);
37090
37516
  if (boot?.trackSizes) for (const [id, ts] of Object.entries(boot.trackSizes)) this.trackSizes.set(id, ts);
37517
+ if (boot?.ext) this.extState = { ...boot.ext };
37091
37518
  if (boot?.charts) for (const { id, ...cs } of boot.charts) this.pool.set(id, cs);
37092
37519
  this.order = boot?.charts ? boot.charts.map((c) => c.id) : declaredOrder(opts.cells);
37093
37520
  const bootActive = boot?.activeCellId ?? null;
@@ -37141,7 +37568,9 @@ var VelaWorkspace = class {
37141
37568
  onTimeframe: (tf) => this.setActiveTimeframe(tf),
37142
37569
  onTimeframeFavorite: (tf, on) => this.setTimeframeFavorite(tf, on),
37143
37570
  onPriceStyle: (style) => this.active.setPriceStyle(style),
37144
- layout: {
37571
+ // Single-chart mode: no layout block at all — the topbar renders no layout
37572
+ // button and no sync switches (see TopbarOptions.layout).
37573
+ layout: this.monoLayout ? void 0 : {
37145
37574
  current: this.def.id,
37146
37575
  // The picker composes dynamic layouts on its grid canvas; registered
37147
37576
  // presets the canvas cannot express (bespoke plugin areas) list as rows.
@@ -37166,8 +37595,11 @@ var VelaWorkspace = class {
37166
37595
  });
37167
37596
  const main = doc.createElement("div");
37168
37597
  main.className = "vela-ws-main";
37598
+ const toolbar = buildToolbar(opts.drawings);
37599
+ this.drawingsEnabled = opts.drawings !== false;
37600
+ this.toolbarDef = toolbar.definition;
37169
37601
  let toolbarHost = null;
37170
- if (opts.drawingToolbar !== false) {
37602
+ if (this.drawingsEnabled && toolbar.visible && opts.drawingToolbar !== false) {
37171
37603
  toolbarHost = doc.createElement("div");
37172
37604
  toolbarHost.className = "vela-ws-toolbar";
37173
37605
  main.appendChild(toolbarHost);
@@ -37186,7 +37618,7 @@ var VelaWorkspace = class {
37186
37618
  this.dock.addBuiltIn({ id: "objects", title: "Object tree", icon: "objects", order: 20, panel: this.objectTree, onChart: (c) => this.objectTree.onChart(c) });
37187
37619
  this.dock.refresh();
37188
37620
  this.root.appendChild(main);
37189
- this.toast = new Toast(this.gridEl);
37621
+ this.toastHost = new Toast(this.gridEl);
37190
37622
  const attribution = rendererDefaults().attribution;
37191
37623
  if (attribution !== false) {
37192
37624
  const background = resolveTheme(opts.theme).background;
@@ -37232,7 +37664,7 @@ var VelaWorkspace = class {
37232
37664
  }
37233
37665
  }
37234
37666
  ) : null;
37235
- this.drawToolbar?.setDefinition(defaultToolbar());
37667
+ this.drawToolbar?.setDefinition(this.toolbarDef);
37236
37668
  this.drawToolbar?.setVisible(true);
37237
37669
  this.drawToolbar?.setDrawingsSyncMode(!!this.syncOpts.drawings);
37238
37670
  this.bottombar = opts.bottombar !== false ? new Bottombar(this.root, {
@@ -37254,11 +37686,11 @@ var VelaWorkspace = class {
37254
37686
  onTimeframeClick: () => this.openTimeframeDrawer(),
37255
37687
  ...picker ? { onIndicatorsClick: () => picker.open() } : {},
37256
37688
  getContext: () => this.context(),
37257
- onDrawingsClick: () => this.openDrawingsDrawer(),
37689
+ ...this.drawingsEnabled ? { onDrawingsClick: () => this.openDrawingsDrawer() } : {},
37258
37690
  onMoreClick: () => this.openMoreDrawer(),
37259
37691
  onSettingsClick: () => this.active.chart.renderer.openSettings()
37260
37692
  }) : null;
37261
- this.drawingPill = new DrawingPill(this.gridEl);
37693
+ this.drawingPill = this.drawingsEnabled ? new DrawingPill(this.gridEl) : null;
37262
37694
  hostEl.appendChild(this.root);
37263
37695
  this.layoutCtl = new LayoutModeController(this.root, opts.layoutMode ?? "auto");
37264
37696
  this.layoutCtl.onChange((mode) => this.onLayoutModeChange(mode));
@@ -37297,6 +37729,7 @@ var VelaWorkspace = class {
37297
37729
  this.manifestSettled = true;
37298
37730
  }
37299
37731
  this.mountAttachments();
37732
+ this.restoreGlobalExt();
37300
37733
  }
37301
37734
  // ── access ──────────────────────────────────────────────────
37302
37735
  /** The cell with identity `id` (its declared name, or `c<N>` when undeclared), or
@@ -37358,7 +37791,8 @@ var VelaWorkspace = class {
37358
37791
  openSymbolSearch: (query) => this.symbolPicker.open(query ?? ""),
37359
37792
  togglePanel: (id, open2) => this.dock.toggle(id, open2),
37360
37793
  root: this.root,
37361
- toast: (message, kind) => this.toast.show(message, kind)
37794
+ toast: (message, kind) => this.toastHost.show(message, kind),
37795
+ stateDirty: () => this.markStateDirty()
37362
37796
  });
37363
37797
  }
37364
37798
  /** Re-project contributed topbar actions + side panels, and mount late-registered attachments. */
@@ -37397,22 +37831,35 @@ var VelaWorkspace = class {
37397
37831
  if (this.trackSizes.size > 0) state.trackSizes = Object.fromEntries([...this.trackSizes].map(([k, v]) => [k, { ...v }]));
37398
37832
  const panels2 = this.dock.getState();
37399
37833
  if (panels2) state.panels = panels2;
37834
+ const ext = { ...this.extState };
37835
+ for (const h of statePersistenceHandlers("global")) {
37836
+ try {
37837
+ const value = h.serialize(this.context());
37838
+ if (value === void 0) delete ext[h.key];
37839
+ else ext[h.key] = value;
37840
+ } catch (err) {
37841
+ console.warn(`[vela] state persistence "${h.key}" serialize failed:`, err);
37842
+ }
37843
+ }
37844
+ this.extState = ext;
37845
+ if (Object.keys(ext).length > 0) state.ext = ext;
37400
37846
  return state;
37401
37847
  }
37402
37848
  /**
37403
37849
  * Restore a state document produced by {@link getState} (untrusted-safe: malformed
37404
- * fields are dropped). Replaces the WHOLE workspace state: prefs, sync links,
37405
- * layout, and every slot — current cells are rebuilt from the document. A layout id
37406
- * that is not registered keeps the current grid (register custom layouts first).
37850
+ * fields are dropped). When the document matches the live grid one-to-one — same
37851
+ * layout, same ordered slot identities — it is applied IN PLACE: every chart
37852
+ * instance survives (markets switch via `setMarket`), so chart references,
37853
+ * indicator handles, and event subscriptions stay valid. Any structural difference
37854
+ * (layout, slot count, renamed ids) falls back to the full rebuild: prefs, sync
37855
+ * links, layout, and every slot are replaced, current cells rebuilt from the
37856
+ * document. A layout id that is not registered keeps the current grid (register
37857
+ * custom layouts first).
37407
37858
  */
37408
37859
  applyState(state) {
37409
37860
  if (this.destroyed) return;
37410
37861
  const st = sanitizeState(state);
37411
37862
  if (!st) return;
37412
- if (st.timezone) {
37413
- this.timezone = st.timezone;
37414
- this.bottombar?.setTimezone(st.timezone);
37415
- }
37416
37863
  if (st.favorites) this.favs = [...st.favorites];
37417
37864
  if (st.timeframeFavorites) {
37418
37865
  this.tfFavs = [...st.timeframeFavorites];
@@ -37422,6 +37869,36 @@ var VelaWorkspace = class {
37422
37869
  for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) this.applySyncSetting(kind, st.sync?.[kind]);
37423
37870
  this.trackSizes.clear();
37424
37871
  if (st.trackSizes) for (const [id, ts] of Object.entries(st.trackSizes)) this.trackSizes.set(id, ts);
37872
+ const targetDef = this.monoLayout ? this.def : ensureLayout(st.layout) ?? this.def;
37873
+ const liveCount = this.def.cells.length;
37874
+ const inPlace = targetDef.id === this.def.id && st.charts.length >= liveCount && this.order.length >= liveCount && this.def.cells.every((_, i) => st.charts[i].id === this.order[i] && this.cellsById.has(this.order[i]));
37875
+ if (inPlace) {
37876
+ if (st.favorites) {
37877
+ for (const cell of this.cellsById.values()) cell.chart.drawings.setFavorites(this.favs);
37878
+ }
37879
+ this.drawingLinks.clear();
37880
+ for (const [i] of this.def.cells.entries()) {
37881
+ const { id, ...cs } = st.charts[i];
37882
+ this.cellsById.get(id)?.rehydrate(cs);
37883
+ }
37884
+ if (st.timezone) this.setTimezone(st.timezone);
37885
+ this.pool.clear();
37886
+ for (const { id, ...cs } of st.charts.slice(liveCount)) this.pool.set(id, cs);
37887
+ this.order = st.charts.map((c) => c.id);
37888
+ this.applyGrid();
37889
+ const nextActive2 = st.activeCellId && this.cellsById.has(st.activeCellId) ? st.activeCellId : this.order[0] ?? null;
37890
+ if (nextActive2 === this.activeId) this.projectActiveCell();
37891
+ else this.setActiveCell(nextActive2);
37892
+ this.refreshRetention();
37893
+ this.extState = { ...st.ext ?? {} };
37894
+ this.restoreGlobalExt();
37895
+ this.markStateDirty();
37896
+ return;
37897
+ }
37898
+ if (st.timezone) {
37899
+ this.timezone = st.timezone;
37900
+ this.bottombar?.setTimezone(st.timezone);
37901
+ }
37425
37902
  for (const [id, cell] of [...this.cellsById]) {
37426
37903
  cell.destroy();
37427
37904
  this.cellsById.delete(id);
@@ -37431,7 +37908,7 @@ var VelaWorkspace = class {
37431
37908
  this.drawingLinks.clear();
37432
37909
  for (const { id, ...cs } of st.charts) this.pool.set(id, cs);
37433
37910
  this.order = st.charts.map((c) => c.id);
37434
- const def = ensureLayout(st.layout);
37911
+ const def = this.monoLayout ? null : ensureLayout(st.layout);
37435
37912
  if (def) this.def = def;
37436
37913
  this.cellBackend = this.backendFor(this.def);
37437
37914
  this.applyGrid();
@@ -37443,8 +37920,24 @@ var VelaWorkspace = class {
37443
37920
  else this.setActiveCell(nextActive);
37444
37921
  this.refreshRetention();
37445
37922
  this.events.emit("layout:changed", { layout: this.def.id });
37923
+ this.extState = { ...st.ext ?? {} };
37924
+ this.restoreGlobalExt();
37446
37925
  this.markStateDirty();
37447
37926
  }
37927
+ /** Run the registered global-scope `restore` handlers against the document-level
37928
+ * `ext` bag — keys present in the document only; a failing handler is contained.
37929
+ * Handlers whose restore touches chart content should be `scope: 'cell'` instead
37930
+ * (those run inside the cell's history-mute). */
37931
+ restoreGlobalExt() {
37932
+ for (const h of statePersistenceHandlers("global")) {
37933
+ if (!(h.key in this.extState)) continue;
37934
+ try {
37935
+ h.restore(this.extState[h.key], this.context());
37936
+ } catch (err) {
37937
+ console.warn(`[vela] state persistence "${h.key}" restore failed:`, err);
37938
+ }
37939
+ }
37940
+ }
37448
37941
  /** Set the workspace-global display timezone — applied to EVERY cell. */
37449
37942
  setTimezone(zone) {
37450
37943
  this.timezone = zone;
@@ -37480,6 +37973,7 @@ var VelaWorkspace = class {
37480
37973
  */
37481
37974
  setLayout(layout) {
37482
37975
  if (this.destroyed) return;
37976
+ if (this.monoLayout) return;
37483
37977
  const next = this.resolveLayout(layout);
37484
37978
  const nextBackend = this.backendFor(next);
37485
37979
  const rebuildAll = nextBackend !== this.cellBackend;
@@ -37509,6 +38003,12 @@ var VelaWorkspace = class {
37509
38003
  resize() {
37510
38004
  this.splitters.layout();
37511
38005
  }
38006
+ /** Show a toast over the grid — the same surface the shell's own notices use
38007
+ * (alerts, script errors) and the one contributions reach via `ctx.toast`. */
38008
+ toast(message, kind = "info", durationMs = 3e3) {
38009
+ if (this.destroyed) return;
38010
+ this.toastHost.show(message, kind, durationMs);
38011
+ }
37512
38012
  destroy() {
37513
38013
  if (this.destroyed) return;
37514
38014
  this.persistNow();
@@ -37534,7 +38034,7 @@ var VelaWorkspace = class {
37534
38034
  this.topbar.destroy();
37535
38035
  this.bottombar?.destroy();
37536
38036
  this.mobileBar?.destroy();
37537
- this.drawingPill.destroy();
38037
+ this.drawingPill?.destroy();
37538
38038
  this.tfDrawer?.destroy();
37539
38039
  this.drawingsDrawer?.destroy();
37540
38040
  this.moreDrawer?.destroy();
@@ -37548,10 +38048,10 @@ var VelaWorkspace = class {
37548
38048
  this.indicatorPicker?.destroy();
37549
38049
  this.tfQuick.destroy();
37550
38050
  this.shortcutsHelp?.destroy();
37551
- this.toast.destroy();
38051
+ this.toastHost.destroy();
37552
38052
  this.alertsMenu?.destroy();
37553
38053
  this.glider.stop();
37554
- sharedBarStore.retain(/* @__PURE__ */ new Set());
38054
+ sharedBarStore.retain(/* @__PURE__ */ new Set(), this);
37555
38055
  this.root.remove();
37556
38056
  this.events.clear();
37557
38057
  }
@@ -37569,7 +38069,7 @@ var VelaWorkspace = class {
37569
38069
  this.mobileBar?.renderActions();
37570
38070
  this.mobileBar?.setSymbol(cell.symbol);
37571
38071
  this.mobileBar?.setTimeframe(cell.timeframe);
37572
- this.drawingPill.onChart(cell.chart);
38072
+ this.drawingPill?.onChart(cell.chart);
37573
38073
  const pushHistory = () => this.topbar.setHistoryState(cell.history.canUndo, cell.history.canRedo);
37574
38074
  this.historyUnsub?.();
37575
38075
  this.historyUnsub = cell.history.onChange(pushHistory);
@@ -37695,7 +38195,8 @@ var VelaWorkspace = class {
37695
38195
  onPriceStyleChanged: (id2) => this.onCellPriceStyleChanged(id2),
37696
38196
  onIndicatorsChanged: (id2) => this.onCellIndicatorsChanged(id2),
37697
38197
  onStateDirty: () => this.markStateDirty(),
37698
- manifestSettled: () => this.manifestSettled
38198
+ manifestSettled: () => this.manifestSettled,
38199
+ toast: (message, kind, durationMs) => this.toastHost.show(message, kind, durationMs)
37699
38200
  });
37700
38201
  cell.host.style.gridArea = perCell[slot.id]?.gridArea ?? "";
37701
38202
  this.cellsById.set(id, cell);
@@ -37704,6 +38205,7 @@ var VelaWorkspace = class {
37704
38205
  cell.chart.renderer.setLayoutMode(this.layoutCtl.current);
37705
38206
  if (this.favs.length > 0) cell.chart.drawings.setFavorites(this.favs);
37706
38207
  cell.setManifest(this.manifest, pooled?.indicators == null);
38208
+ cell.restorePersistedExt();
37707
38209
  this.events.emit("cell:created", { id });
37708
38210
  }
37709
38211
  for (const [i] of this.def.cells.entries()) {
@@ -37715,12 +38217,13 @@ var VelaWorkspace = class {
37715
38217
  * cell's whole life, so these live and die with the cell). */
37716
38218
  wireCell(cell) {
37717
38219
  const chart = cell.chart;
37718
- chart.on("indicator:error", ({ error }) => this.toast.show(`[${cell.id}] ${error.message}`, "error", 5e3));
38220
+ chart.on("indicator:error", ({ error }) => this.toastHost.show(`[${cell.id}] ${error.message}`, "error", 5e3));
37719
38221
  chart.on("script:run", (run) => this.events.emit("script:run", { ...run, cell: cell.id }));
37720
38222
  chart.on("alert", (alert) => {
37721
- this.alerts.unshift({ cellId: cell.id, symbol: cell.symbol, title: alert.title ?? "Alert", message: alert.message, time: alert.time });
37722
- if (this.alerts.length > ALERT_CAP) this.alerts.pop();
37723
- this.toast.show(`[${cell.id} \xB7 ${cell.symbol}] ${alert.title ? alert.title + " \u2014 " : ""}${alert.message}`, "info", 4e3);
38223
+ const source = [parseSymbol(cell.symbol).ticker || cell.symbol, timeframeLabel(cell.timeframe), alert.indicator].filter(Boolean).join(" ");
38224
+ this.alerts.unshift({ cellId: cell.id, source, title: alert.title ?? "Alert", message: alert.message, time: alert.time });
38225
+ if (this.alerts.length > this.alertCap) this.alerts.pop();
38226
+ this.toastHost.show(`${source} \u2014 ${alert.title ? alert.title + " \u2014 " : ""}${alert.message}`, "info", 4e3);
37724
38227
  this.topbar.setAlertCount(this.alerts.length);
37725
38228
  });
37726
38229
  chart.on("drawing:favorites", ({ favorites }) => {
@@ -37981,6 +38484,7 @@ var VelaWorkspace = class {
37981
38484
  this.mobileBar?.setTimeframe(cell.timeframe);
37982
38485
  this.objectTree.setSymbol(cell.symbol);
37983
38486
  this.bottombar?.setSession({ session: cell.session, enabled: cell.sessionAvailable });
38487
+ this.bottombar?.setActiveRange(cell.activeRangeId);
37984
38488
  }
37985
38489
  /** Trigger ② — a cell's price style changed: the topbar button/menu only if active.
37986
38490
  * Reads the cell back (not the requested style) so the button reflects what the
@@ -38065,7 +38569,7 @@ var VelaWorkspace = class {
38065
38569
  openDrawingsDrawer() {
38066
38570
  this.drawingsDrawer ?? (this.drawingsDrawer = new DrawingsDrawer({
38067
38571
  host: this.root,
38068
- toolbar: () => defaultToolbar(),
38572
+ toolbar: () => this.toolbarDef,
38069
38573
  // the shared static toolbar's definition (see constructor)
38070
38574
  currentTool: () => this.active.chart.drawings.getTool(),
38071
38575
  isFavorite: (type) => this.active.chart.drawings.isFavorite(type),
@@ -38088,14 +38592,15 @@ var VelaWorkspace = class {
38088
38592
  onPriceStyle: (id) => this.active.setPriceStyle(id),
38089
38593
  panels: () => [...this.dock.list()],
38090
38594
  onTogglePanel: (id) => this.dock.toggle(id),
38091
- alerts: () => this.alerts.map((a) => ({ title: `[${a.cellId} \xB7 ${a.symbol}] ${a.title}`, message: a.message, time: a.time })),
38595
+ alerts: () => this.alerts.map((a) => ({ title: `${a.source} \xB7 ${a.title}`, message: a.message, time: a.time })),
38092
38596
  // Left-aligned actions have their own bottom-bar stop — only the rest
38093
38597
  // lands in the drawer, or every left action would appear twice.
38094
38598
  actions: () => widgetActions("topbar", this.context()).filter((a) => a.align !== "left").map((a) => ({ label: a.label, icon: a.icon, run: () => a.run(this.context()) })),
38095
38599
  // The desktop layout dropdown's whole surface — the grid canvas, the
38096
38600
  // non-canvas presets and the sync switches — relocated into the kebab
38097
- // drawer (the topbar is hidden on mobile). Same reads as the topbar block.
38098
- layout: {
38601
+ // drawer (the topbar is hidden on mobile). Same reads as the topbar block;
38602
+ // single-chart mode omits it here too.
38603
+ layout: this.monoLayout ? void 0 : {
38099
38604
  shape: () => layoutShape(this.def),
38100
38605
  presets: () => layouts().filter((l) => layoutShape(l) === null).map((l) => ({ id: l.id, label: l.label, checked: l.id === this.def.id })),
38101
38606
  onSelectGrid: (rows, cols) => this.setLayout(layoutForGrid(rows, cols)),
@@ -38138,7 +38643,7 @@ var VelaWorkspace = class {
38138
38643
  this.alertsMenu?.destroy();
38139
38644
  const items = this.alerts.length ? this.alerts.map((a, i) => ({
38140
38645
  id: String(i),
38141
- label: `[${a.cellId} \xB7 ${a.symbol}] ${new Date(a.time).toLocaleTimeString()} \xB7 ${a.title}: ${a.message}`.slice(0, 80)
38646
+ label: `${a.source} \xB7 ${new Date(a.time).toLocaleTimeString()} \xB7 ${a.title}: ${a.message}`.slice(0, 80)
38142
38647
  })) : [{ id: "none", label: "No alerts yet", disabled: true }];
38143
38648
  this.alertsMenu = new Menu({
38144
38649
  host: this.root,
@@ -38259,7 +38764,7 @@ var VelaWorkspace = class {
38259
38764
  if (!raw) continue;
38260
38765
  symbols.add(this.feed.resolveSymbol(raw)?.ticker ?? raw);
38261
38766
  }
38262
- sharedBarStore.retain(symbols);
38767
+ sharedBarStore.retain(symbols, this);
38263
38768
  }
38264
38769
  };
38265
38770