@luxalgo/vela 0.6.4 → 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-6FA4TDUF.js → chunk-JQFO2WMU.js} +2666 -2333
  7. package/dist/{chunk-BEJ57HP4.js → chunk-OEUGUXL7.js} +14 -1
  8. package/dist/{chunk-3QX6QOCX.js → chunk-TMXK2SJR.js} +5852 -3340
  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 +2681 -2332
  12. package/dist/index.d.cts +65 -14
  13. package/dist/index.d.ts +65 -14
  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-CkiStRaI.d.cts → plugin-CN8U2__Q.d.cts} +11 -5
  18. package/dist/{plugin-BeCMF1VQ.d.ts → plugin-WnvNNJOp.d.ts} +11 -5
  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 +2661 -2312
  32. package/dist/vela.global.min.js +48 -48
  33. package/dist/widget.cjs +35658 -34074
  34. package/dist/widget.d.cts +39 -271
  35. package/dist/widget.d.ts +39 -271
  36. package/dist/widget.js +60 -1313
  37. package/dist/workspace.cjs +1584 -960
  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 };
@@ -13407,6 +13422,7 @@ var CSS8 = `
13407
13422
  .vela-sp-badge[data-p='hyperliquid'] { color: #50d2c1; } /* palette-exempt: venue brand mark */
13408
13423
  .vela-sp-empty { padding: var(--vela-space-3); color: var(--vela-fg-muted); text-align: center; }
13409
13424
  `;
13425
+ var PAGE = 100;
13410
13426
  var SymbolPicker = class {
13411
13427
  constructor(opts) {
13412
13428
  this.source = () => [];
@@ -13414,6 +13430,7 @@ var SymbolPicker = class {
13414
13430
  this.highlighted = 0;
13415
13431
  this.seed = "";
13416
13432
  this.activeTab = "All";
13433
+ this.visible = PAGE;
13417
13434
  const doc = (opts.host ?? document.body).ownerDocument;
13418
13435
  injectStyles(STYLE_ID7, CSS8, doc);
13419
13436
  this.input = doc.createElement("input");
@@ -13440,6 +13457,12 @@ var SymbolPicker = class {
13440
13457
  }
13441
13458
  this.list = doc.createElement("div");
13442
13459
  this.list.className = "vela-sp-list";
13460
+ this.list.addEventListener("scroll", () => {
13461
+ if (this.rows.length < this.visible) return;
13462
+ if (this.list.scrollTop + this.list.clientHeight < this.list.scrollHeight - 200) return;
13463
+ this.visible += PAGE;
13464
+ this.grow();
13465
+ });
13443
13466
  this.dialog = new Dialog({
13444
13467
  title: "Symbol Search",
13445
13468
  host: opts.host,
@@ -13505,11 +13528,15 @@ var SymbolPicker = class {
13505
13528
  } else delete el.dataset.highlighted;
13506
13529
  });
13507
13530
  }
13508
- refresh() {
13509
- const doc = this.list.ownerDocument;
13531
+ computeRows() {
13510
13532
  const TAB_TYPES = { Crypto: ["crypto"], Stocks: ["stock"], ETFs: ["etf"], Forex: ["forex"], Commodities: ["commodity"] };
13511
13533
  const pool = this.activeTab === "All" ? this.source() : this.source().filter((s) => TAB_TYPES[this.activeTab]?.includes((s.type ?? "").toLowerCase()) || this.activeTab === "Crypto" && (s.type ?? "").toLowerCase() === "futures");
13512
- this.rows = filterSymbols(pool, this.input.value);
13534
+ return filterSymbols(pool, this.input.value, this.visible);
13535
+ }
13536
+ refresh() {
13537
+ const doc = this.list.ownerDocument;
13538
+ this.visible = PAGE;
13539
+ this.rows = this.computeRows();
13513
13540
  this.highlighted = 0;
13514
13541
  this.list.replaceChildren();
13515
13542
  if (!this.rows.length) {
@@ -13519,34 +13546,42 @@ var SymbolPicker = class {
13519
13546
  this.list.appendChild(empty);
13520
13547
  return;
13521
13548
  }
13522
- for (const s of this.rows) {
13523
- const row = doc.createElement("div");
13524
- row.className = "vela-sp-row";
13525
- row.dataset.ticker = s.ticker;
13526
- const venue = s.prefix ?? s.provider;
13527
- if (venue) row.dataset.venue = venue;
13528
- const av = tickerIconEl(doc, baseOf(s), s.ticker, "vela-sp-avatar");
13529
- const main = doc.createElement("span");
13530
- main.className = "vela-sp-main";
13531
- const t = doc.createElement("span");
13532
- t.className = "vela-sp-ticker";
13533
- t.textContent = s.ticker;
13534
- const d = doc.createElement("span");
13535
- d.className = "vela-sp-desc";
13536
- d.textContent = s.description ?? (s.type ?? "");
13537
- main.append(t, d);
13538
- row.append(av, main);
13539
- if (venue) {
13540
- const badge = doc.createElement("span");
13541
- badge.className = "vela-sp-badge";
13542
- badge.dataset.p = s.provider ?? venue;
13543
- badge.textContent = venue;
13544
- row.appendChild(badge);
13545
- }
13546
- this.list.appendChild(row);
13547
- }
13549
+ for (const s of this.rows) this.list.appendChild(this.rowEl(s));
13548
13550
  this.renderHighlight();
13549
13551
  }
13552
+ /** Append the page the grown `visible` just uncovered — rows already on screen stay put. */
13553
+ grow() {
13554
+ const already = this.rows.length;
13555
+ this.rows = this.computeRows();
13556
+ for (const s of this.rows.slice(already)) this.list.appendChild(this.rowEl(s));
13557
+ }
13558
+ rowEl(s) {
13559
+ const doc = this.list.ownerDocument;
13560
+ const row = doc.createElement("div");
13561
+ row.className = "vela-sp-row";
13562
+ row.dataset.ticker = s.ticker;
13563
+ const venue = s.prefix ?? s.provider;
13564
+ if (venue) row.dataset.venue = venue;
13565
+ const av = tickerIconEl(doc, baseOf(s), s.ticker, "vela-sp-avatar");
13566
+ const main = doc.createElement("span");
13567
+ main.className = "vela-sp-main";
13568
+ const t = doc.createElement("span");
13569
+ t.className = "vela-sp-ticker";
13570
+ t.textContent = s.ticker;
13571
+ const d = doc.createElement("span");
13572
+ d.className = "vela-sp-desc";
13573
+ d.textContent = s.description ?? (s.type ?? "");
13574
+ main.append(t, d);
13575
+ row.append(av, main);
13576
+ if (venue) {
13577
+ const badge = doc.createElement("span");
13578
+ badge.className = "vela-sp-badge";
13579
+ badge.dataset.p = s.provider ?? venue;
13580
+ badge.textContent = venue;
13581
+ row.appendChild(badge);
13582
+ }
13583
+ return row;
13584
+ }
13550
13585
  };
13551
13586
 
13552
13587
  // src/widget/indicator-picker.ts
@@ -14192,10 +14227,11 @@ var MobileBar = class {
14192
14227
  const indicators = onIndicators ? item("vela-mb-indicators", "Indicators", onIndicators, "indicators") : null;
14193
14228
  this.actionsHost = doc.createElement("span");
14194
14229
  this.actionsHost.className = "vela-mb-actions";
14195
- 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;
14196
14232
  const more = item("vela-mb-more", "More", opts.onMoreClick, "kebab");
14197
14233
  const settings = item("vela-mb-settings", "Chart settings", opts.onSettingsClick, "gear");
14198
- 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);
14199
14235
  host.appendChild(this.el);
14200
14236
  this.renderActions();
14201
14237
  }
@@ -17010,6 +17046,35 @@ function rendererDefaults() {
17010
17046
  return Object.fromEntries(defaults);
17011
17047
  }
17012
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
+
17013
17078
  // src/workspace/sync.ts
17014
17079
  function syncTargets(originId, setting, cellIds) {
17015
17080
  if (setting == null || setting === false) return [];
@@ -17069,8 +17134,18 @@ function sanitizeState(doc) {
17069
17134
  if (tracks) out.trackSizes = tracks;
17070
17135
  const panels2 = sanitizePanels(d.panels);
17071
17136
  if (panels2) out.panels = panels2;
17137
+ const ext = sanitizeExt(d.ext);
17138
+ if (ext) out.ext = ext;
17072
17139
  return out;
17073
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
+ }
17074
17149
  function sanitizeCell(raw) {
17075
17150
  if (raw == null || typeof raw !== "object") return null;
17076
17151
  const c = raw;
@@ -17092,6 +17167,8 @@ function sanitizeCell(raw) {
17092
17167
  const natives = Array.isArray(ind.natives) ? ind.natives.filter((n) => typeof n === "string") : [];
17093
17168
  out.indicators = { manifest, natives };
17094
17169
  }
17170
+ const ext = sanitizeExt(c.ext);
17171
+ if (ext) out.ext = ext;
17095
17172
  return out;
17096
17173
  }
17097
17174
  function sanitizeSync(raw) {
@@ -18424,7 +18501,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
18424
18501
  this.emitScriptRun(id, cause, first);
18425
18502
  },
18426
18503
  onAlert: (a) => {
18427
- this.events.emit("alert", a);
18504
+ const indicator = record.options?.title ?? record.prepared?.meta.title ?? record.title;
18505
+ this.events.emit("alert", { ...a, indicator });
18428
18506
  handle.emit("alert", { id: a.id, message: a.message, title: a.title, time: a.time });
18429
18507
  },
18430
18508
  onWarning: (w) => this.events.emit("warning", w),
@@ -18472,6 +18550,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
18472
18550
  overlay: d.overlay,
18473
18551
  paneHint: d.paneHint,
18474
18552
  native: { type: record.native.type },
18553
+ ...out.paneAxis != null ? { paneAxis: out.paneAxis } : {},
18475
18554
  series: out.series ?? [],
18476
18555
  fills: out.fills ?? [],
18477
18556
  backgrounds: out.backgrounds ?? [],
@@ -19066,6 +19145,24 @@ var RendererControl = class {
19066
19145
  this.renderer.setSettingsSections?.(sections);
19067
19146
  return this;
19068
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
+ }
19069
19166
  /** Tell the renderer's own chrome which size class the host shell is in —
19070
19167
  * `'mobile'` switches its dialogs/toolbars to the touch-first presentation.
19071
19168
  * Silent no-op on a renderer without adaptive chrome. */
@@ -21448,652 +21545,6 @@ function supportsWebGL2() {
21448
21545
  return webgl2Probe;
21449
21546
  }
21450
21547
 
21451
- // src/renderers/native/chrome/ticks.ts
21452
- function priceTicks(min, max, target = 6) {
21453
- if (!(max > min) || !Number.isFinite(min) || !Number.isFinite(max)) return [];
21454
- const raw = (max - min) / Math.max(1, target);
21455
- const mag = Math.pow(10, Math.floor(Math.log10(raw)));
21456
- const norm = raw / mag;
21457
- const step = (norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10) * mag;
21458
- const decimals = Math.max(0, -Math.floor(Math.log10(step)) + 1);
21459
- const out = [];
21460
- const start = Math.ceil(min / step) * step;
21461
- for (let v = start; v <= max + step * 1e-6; v += step) {
21462
- out.push(Number(v.toFixed(decimals)));
21463
- }
21464
- return out;
21465
- }
21466
- function priceDecimals(min, max, target = 6) {
21467
- if (!(max > min)) return 2;
21468
- const raw = (max - min) / Math.max(1, target);
21469
- const mag = Math.pow(10, Math.floor(Math.log10(raw)));
21470
- const norm = raw / mag;
21471
- const step = (norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10) * mag;
21472
- return Math.max(0, Math.min(8, -Math.floor(Math.log10(step)) + 1));
21473
- }
21474
- function logPriceTicks(min, max, target = 6) {
21475
- if (min <= 0 || !(max > min) || !Number.isFinite(min) || !Number.isFinite(max)) return [];
21476
- if (Math.log10(max) - Math.log10(min) < 1.1) return priceTicks(min, max, target);
21477
- const out = [];
21478
- const startExp = Math.floor(Math.log10(min));
21479
- const endExp = Math.ceil(Math.log10(max));
21480
- for (let e = startExp; e <= endExp; e += 1) {
21481
- for (const m of [1, 2, 5]) {
21482
- const v = m * Math.pow(10, e);
21483
- if (v >= min && v <= max) out.push(v);
21484
- }
21485
- }
21486
- return out;
21487
- }
21488
- function valueDecimals(v) {
21489
- const a = Math.abs(v);
21490
- if (a >= 100) return 0;
21491
- if (a >= 1) return 2;
21492
- if (a >= 0.01) return 4;
21493
- return 6;
21494
- }
21495
- function tickDecimals(tick) {
21496
- if (!(tick > 0) || !Number.isFinite(tick)) return 2;
21497
- for (let d = 0; d <= 8; d += 1) {
21498
- if (Math.abs(Number(tick.toFixed(d)) - tick) <= tick * 1e-6) return d;
21499
- }
21500
- return 8;
21501
- }
21502
- function axisDecimals(scale, heightPx, mintick) {
21503
- const d = mintick != null && mintick > 0 ? tickDecimals(mintick) : priceDecimals(scale.min, scale.max, tickCount(heightPx));
21504
- return d === 0 ? 2 : d;
21505
- }
21506
- function tickCount(paneHeightPx) {
21507
- return Math.max(2, Math.min(16, Math.round(paneHeightPx / 50)));
21508
- }
21509
- function paneTicks(scale, heightPx) {
21510
- return scale.log ? logPriceTicks(scale.min, scale.max, tickCount(heightPx)) : priceTicks(scale.min, scale.max, tickCount(heightPx));
21511
- }
21512
- function toPct(price, baseline) {
21513
- return (price / baseline - 1) * 100;
21514
- }
21515
- function toIndex(price, baseline) {
21516
- return price / baseline * 100;
21517
- }
21518
- function formatPct(pct) {
21519
- const sign = pct >= 0 ? "+" : "-";
21520
- return `${sign}${Math.abs(pct).toFixed(2)}%`;
21521
- }
21522
- function formatIndex(idx) {
21523
- return idx.toFixed(2);
21524
- }
21525
- function formatCompactValue(v) {
21526
- const a = Math.abs(v);
21527
- if (a >= 1e9) return `${trimZeros(v / 1e9)}B`;
21528
- if (a >= 1e6) return `${trimZeros(v / 1e6)}M`;
21529
- if (a >= 1e3) return `${trimZeros(v / 1e3)}K`;
21530
- return trimZeros(v);
21531
- }
21532
- function trimZeros(v) {
21533
- return Number(v.toFixed(2)).toString();
21534
- }
21535
- function paneAxisTicks(scale, heightPx, pct, mintick, format) {
21536
- if (pct) {
21537
- const { baseline, indexed } = pct;
21538
- const lo = Math.min(scale.min, scale.max);
21539
- const hi = Math.max(scale.min, scale.max);
21540
- if (indexed) {
21541
- const iLo = toIndex(lo, baseline);
21542
- const iHi = toIndex(hi, baseline);
21543
- return priceTicks(iLo, iHi, tickCount(heightPx)).map((idx) => ({ price: baseline * idx / 100, label: formatIndex(idx) }));
21544
- }
21545
- const pLo = toPct(lo, baseline);
21546
- const pHi = toPct(hi, baseline);
21547
- return priceTicks(pLo, pHi, tickCount(heightPx)).map((p) => ({ price: baseline * (1 + p / 100), label: formatPct(p) }));
21548
- }
21549
- if (format === "volume") {
21550
- return paneTicks(scale, heightPx).map((price) => ({ price, label: formatCompactValue(price) }));
21551
- }
21552
- return paneTicks(scale, heightPx).map((price) => ({ price, label: formatPriceLabel(scale, heightPx, price, mintick) }));
21553
- }
21554
- function formatAxisValue(scale, heightPx, value, pct, mintick, format) {
21555
- if (pct) return pct.indexed ? formatIndex(toIndex(value, pct.baseline)) : formatPct(toPct(value, pct.baseline));
21556
- if (format === "volume") return formatCompactValue(value);
21557
- return formatPriceLabel(scale, heightPx, value, mintick);
21558
- }
21559
- function formatPriceLabel(scale, heightPx, value, mintick) {
21560
- const wideLog = scale.log && Math.log10(scale.max) - Math.log10(scale.min) >= 1.1;
21561
- if (wideLog) {
21562
- const d = valueDecimals(value);
21563
- return value.toFixed(d === 0 ? 2 : d);
21564
- }
21565
- return value.toFixed(axisDecimals(scale, heightPx, mintick));
21566
- }
21567
- var SEC = 1e3;
21568
- var MIN = 60 * SEC;
21569
- var HOUR = 60 * MIN;
21570
- var DAY = 24 * HOUR;
21571
- var WEEK = 7 * DAY;
21572
- var MONTH = 30 * DAY;
21573
- var YEAR = 365 * DAY;
21574
- var STEP_LADDER = [
21575
- SEC,
21576
- 5 * SEC,
21577
- 15 * SEC,
21578
- 30 * SEC,
21579
- MIN,
21580
- 5 * MIN,
21581
- 15 * MIN,
21582
- 30 * MIN,
21583
- HOUR,
21584
- 2 * HOUR,
21585
- 4 * HOUR,
21586
- 6 * HOUR,
21587
- 12 * HOUR,
21588
- DAY,
21589
- 2 * DAY,
21590
- WEEK,
21591
- MONTH,
21592
- 3 * MONTH,
21593
- YEAR
21594
- ];
21595
- var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
21596
- var pad2 = (n) => n < 10 ? `0${n}` : String(n);
21597
- function pickStep(targetMs) {
21598
- for (const step of STEP_LADDER) if (step >= targetMs) return step;
21599
- return STEP_LADDER[STEP_LADDER.length - 1];
21600
- }
21601
- function timeTicks(fromMs, toMs, target = 8, offsetMs = 0) {
21602
- const span = toMs - fromMs;
21603
- if (!(span > 0)) return [];
21604
- const step = pickStep(span / Math.max(1, target));
21605
- const zFrom = fromMs + offsetMs;
21606
- const zTo = toMs + offsetMs;
21607
- const first = Math.ceil(zFrom / step) * step;
21608
- const out = [];
21609
- for (let zt = first; zt <= zTo; zt += step) {
21610
- const t = zt - offsetMs;
21611
- const d = new Date(zt);
21612
- let label;
21613
- let major = false;
21614
- if (step < DAY) {
21615
- const h = d.getUTCHours();
21616
- const m = d.getUTCMinutes();
21617
- if (h === 0 && m === 0) {
21618
- label = `${MONTHS[d.getUTCMonth()]} ${d.getUTCDate()}`;
21619
- major = true;
21620
- } else {
21621
- label = `${pad2(h)}:${pad2(m)}`;
21622
- }
21623
- } else if (step < YEAR) {
21624
- label = `${MONTHS[d.getUTCMonth()]} ${d.getUTCDate()}`;
21625
- if (d.getUTCDate() === 1) {
21626
- label = MONTHS[d.getUTCMonth()];
21627
- major = true;
21628
- }
21629
- } else {
21630
- label = String(d.getUTCFullYear());
21631
- major = true;
21632
- }
21633
- out.push({ time: t, label, major });
21634
- }
21635
- return out;
21636
- }
21637
-
21638
- // src/renderers/shared/trade-markers.ts
21639
- var TRADE_LONG_COLOR = TRADE_LONG;
21640
- var TRADE_SHORT_COLOR = TRADE_SHORT;
21641
- var TRADE_EXIT_COLOR = TRADE_EXIT;
21642
- function defaultTradeMarkersState() {
21643
- return {
21644
- visible: true,
21645
- labels: true,
21646
- qty: true,
21647
- colors: { long: TRADE_LONG_COLOR, short: TRADE_SHORT_COLOR, exit: TRADE_EXIT_COLOR }
21648
- };
21649
- }
21650
- function mergeTradeMarkersState(base, patch) {
21651
- const p = patch && typeof patch === "object" ? patch : {};
21652
- const c = p.colors && typeof p.colors === "object" ? p.colors : {};
21653
- const bool = (v, fb) => typeof v === "boolean" ? v : fb;
21654
- const color = (v, fb) => typeof v === "string" && v.trim().length > 0 ? v : fb;
21655
- return {
21656
- visible: bool(p.visible, base.visible),
21657
- labels: bool(p.labels, base.labels),
21658
- qty: bool(p.qty, base.qty),
21659
- colors: {
21660
- long: color(c.long, base.colors.long),
21661
- short: color(c.short, base.colors.short),
21662
- exit: color(c.exit, base.colors.exit)
21663
- }
21664
- };
21665
- }
21666
- var BAR_GAP = 10;
21667
- var ARROW_W = 9;
21668
- var HEAD_H = 6;
21669
- var ARROW_H = 14;
21670
- var STEM_W = 3;
21671
- var CAP_H = 2;
21672
- var CAP_GAP = 2;
21673
- var TEXT_GAP = 3;
21674
- var UNIT_GAP = 6;
21675
- var TICK_W = 6;
21676
- var TICK_H = 8;
21677
- function lineHeightOf(fontSize) {
21678
- return fontSize + 4;
21679
- }
21680
- function qtyText(exec) {
21681
- if (exec.qty == null || !Number.isFinite(exec.qty)) return null;
21682
- const magnitude = Number(Math.abs(exec.qty).toFixed(8));
21683
- return `${exec.side === "buy" ? "+" : "-"}${magnitude}`;
21684
- }
21685
- function unitOf(exec, state, lineH) {
21686
- const lines = [];
21687
- if (state.labels && exec.label) lines.push(exec.label);
21688
- if (state.qty) {
21689
- const q = qtyText(exec);
21690
- if (q) lines.push(q);
21691
- }
21692
- return { exec, lines, height: ARROW_H + (lines.length ? TEXT_GAP + lines.length * lineH : 0) };
21693
- }
21694
- function stacksFor(trades, state, deps, from, to, lineH) {
21695
- const byBar = /* @__PURE__ */ new Map();
21696
- for (const exec of trades) {
21697
- const logical = Math.round(deps.timeToLogical(exec.time));
21698
- if (logical < from || logical > to) continue;
21699
- let stack = byBar.get(logical);
21700
- if (!stack) {
21701
- stack = { logical, buys: [], sells: [] };
21702
- byBar.set(logical, stack);
21703
- }
21704
- (exec.side === "buy" ? stack.buys : stack.sells).push(unitOf(exec, state, lineH));
21705
- }
21706
- return [...byBar.values()];
21707
- }
21708
- function stackExtent(units) {
21709
- if (units.length === 0) return 0;
21710
- let px = BAR_GAP;
21711
- for (const u of units) px += u.height;
21712
- return px + (units.length - 1) * UNIT_GAP;
21713
- }
21714
- function tradesPriceHints(trades, state, deps, from, to, fontSize) {
21715
- if (trades.length === 0) return null;
21716
- const lineH = lineHeightOf(fontSize);
21717
- let min = Infinity;
21718
- let max = -Infinity;
21719
- let abovePx = 0;
21720
- let belowPx = 0;
21721
- for (const stack of stacksFor(trades, state, deps, Math.floor(from), Math.ceil(to), lineH)) {
21722
- const bar = deps.barAt(stack.logical);
21723
- if (!bar) continue;
21724
- if (bar.low < min) min = bar.low;
21725
- if (bar.high > max) max = bar.high;
21726
- belowPx = Math.max(belowPx, stackExtent(stack.buys));
21727
- abovePx = Math.max(abovePx, stackExtent(stack.sells));
21728
- }
21729
- if (!Number.isFinite(min) || !Number.isFinite(max)) return null;
21730
- return { min, max, abovePx, belowPx };
21731
- }
21732
- function renderTradeMarkers(ctx, trades, state, deps, xOf, yOf, text, width, barHalfPx) {
21733
- if (trades.length === 0) return;
21734
- const lineH = lineHeightOf(text.fontSize);
21735
- const stacks = stacksFor(trades, state, deps, -Infinity, Infinity, lineH);
21736
- if (stacks.length === 0) return;
21737
- ctx.save();
21738
- ctx.font = `${text.fontSize}px ${text.fontFamily}`;
21739
- ctx.textAlign = "center";
21740
- ctx.textBaseline = "middle";
21741
- for (const stack of stacks) {
21742
- const x = xOf(stack.logical);
21743
- if (x < -150 || x > width + 150) continue;
21744
- const bar = deps.barAt(stack.logical);
21745
- if (!bar) continue;
21746
- const yBottom = Math.max(yOf(bar.low), yOf(bar.high));
21747
- const yTop = Math.min(yOf(bar.low), yOf(bar.high));
21748
- let y = yBottom + BAR_GAP;
21749
- for (const unit of stack.buys) {
21750
- ctx.fillStyle = colorOf(unit.exec, state.colors);
21751
- drawArrowUp(ctx, x, y, unit.exec.kind === "exit");
21752
- drawFillTick(ctx, unit.exec.side, x, yOf(unit.exec.price), barHalfPx);
21753
- drawTextLines(ctx, unit.lines, x, y + ARROW_H + TEXT_GAP + lineH / 2, lineH, text.color);
21754
- y += unit.height + UNIT_GAP;
21755
- }
21756
- y = yTop - BAR_GAP;
21757
- for (const unit of stack.sells) {
21758
- ctx.fillStyle = colorOf(unit.exec, state.colors);
21759
- drawArrowDown(ctx, x, y, unit.exec.kind === "exit");
21760
- drawFillTick(ctx, unit.exec.side, x, yOf(unit.exec.price), barHalfPx);
21761
- drawTextLines(ctx, unit.lines, x, y - ARROW_H - TEXT_GAP - lineH / 2, -lineH, text.color);
21762
- y -= unit.height + UNIT_GAP;
21763
- }
21764
- }
21765
- ctx.restore();
21766
- }
21767
- function colorOf(exec, colors) {
21768
- if (exec.kind === "exit") return colors.exit;
21769
- return exec.side === "buy" ? colors.long : colors.short;
21770
- }
21771
- function drawArrowUp(ctx, x, yTip, capped) {
21772
- ctx.beginPath();
21773
- ctx.moveTo(x, yTip);
21774
- ctx.lineTo(x - ARROW_W / 2, yTip + HEAD_H);
21775
- ctx.lineTo(x + ARROW_W / 2, yTip + HEAD_H);
21776
- ctx.closePath();
21777
- ctx.fill();
21778
- ctx.fillRect(x - STEM_W / 2, yTip + HEAD_H, STEM_W, ARROW_H - HEAD_H);
21779
- if (capped) ctx.fillRect(x - ARROW_W / 2, yTip - CAP_GAP - CAP_H, ARROW_W, CAP_H);
21780
- }
21781
- function drawArrowDown(ctx, x, yTip, capped) {
21782
- ctx.beginPath();
21783
- ctx.moveTo(x, yTip);
21784
- ctx.lineTo(x - ARROW_W / 2, yTip - HEAD_H);
21785
- ctx.lineTo(x + ARROW_W / 2, yTip - HEAD_H);
21786
- ctx.closePath();
21787
- ctx.fill();
21788
- ctx.fillRect(x - STEM_W / 2, yTip - ARROW_H, STEM_W, ARROW_H - HEAD_H);
21789
- if (capped) ctx.fillRect(x - ARROW_W / 2, yTip + CAP_GAP, ARROW_W, CAP_H);
21790
- }
21791
- function drawFillTick(ctx, side, x, yFill, barHalfPx) {
21792
- const edge = side === "buy" ? x - barHalfPx : x + barHalfPx;
21793
- const back = side === "buy" ? edge - TICK_W : edge + TICK_W;
21794
- ctx.beginPath();
21795
- ctx.moveTo(edge, yFill);
21796
- ctx.lineTo(back, yFill - TICK_H / 2);
21797
- ctx.lineTo(back, yFill + TICK_H / 2);
21798
- ctx.closePath();
21799
- ctx.fill();
21800
- }
21801
- function drawTextLines(ctx, lines, x, firstY, step, color) {
21802
- if (lines.length === 0) return;
21803
- ctx.fillStyle = color;
21804
- for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], x, firstY + i * step);
21805
- }
21806
-
21807
- // src/renderers/native/core/SceneGraph.ts
21808
- var SceneGraph = class {
21809
- constructor() {
21810
- this.panes = /* @__PURE__ */ new Map();
21811
- this.indicators = /* @__PURE__ */ new Map();
21812
- this.bars = [];
21813
- /** Volume-layer config pushed by the volume native indicator (null ⇒ layer off). Ephemeral. */
21814
- this.volumeLayer = null;
21815
- /** Generic native-data channels for SDK renderer layers (`setNativeData(id, …)`). Ephemeral. */
21816
- this.nativeData = /* @__PURE__ */ new Map();
21817
- /** Loading ranges per channel (`setNativeData(id + '-pending', …)`). Ephemeral. */
21818
- this.nativePending = /* @__PURE__ */ new Map();
21819
- /** VPVR-layer config pushed by the VPVR native indicator (null ⇒ layer off). Ephemeral. */
21820
- this.vpvrLayer = null;
21821
- this.crosshair = null;
21822
- /** How the base price series is drawn on the price pane (candles by default). */
21823
- this.priceStyle = "candles";
21824
- /** Price-series base painting for the ACTIVE style (see ChartTypeDefinition.basePainting). */
21825
- this.basePainting = "candles";
21826
- /** The ACTIVE style's own candle cosmetics (`chartTypes.<id>.candle*`) when it is a
21827
- * candle-based plugin type; null ⇒ paint with the shared `style.candle` block. */
21828
- this.candleOverride = null;
21829
- /** Explicit baseline reference price for `priceStyle:'baseline'`; when null the
21830
- * baseline follows `style.baseline.baselineLevel` as a percent of the visible pane
21831
- * range (resolved per frame via `baselinePriceFor`). */
21832
- this.baselineValue = null;
21833
- /** Draw the dashed horizontal line at the latest price (price pane). Independent
21834
- * of the axis label chip (`showPriceLabel`) — either can show without the other. */
21835
- this.showPriceLine = true;
21836
- /** Draw the last-price label chip on the price axis. Independent of the line. */
21837
- this.showPriceLabel = true;
21838
- /** Draw the countdown-to-bar-close chip on the price axis. When the price label is
21839
- * also shown, the two merge into one stacked block (countdown under the label);
21840
- * when either shows alone it's centered on the latest price level. */
21841
- this.showCountdown = true;
21842
- /** Logarithmic price scale on the price pane. */
21843
- this.logScale = false;
21844
- /** Inverted price axis on the price pane (high at the bottom). Study panes carry their own. */
21845
- this.invertScale = false;
21846
- /** Exchange tick size for the active symbol (e.g. 0.01), when known. Drives the
21847
- * price-axis decimals — the instrument's true precision instead of the zoom-derived
21848
- * formula. Undefined until symbol metadata loads (the formula is the fallback). */
21849
- this.priceMintick = void 0;
21850
- /** Price-axis mode on the price pane: `'price'` (absolute) or `'percent'` (change
21851
- * vs `percentBaseline`). Gridlines, axis labels and crosshair chip all follow it. */
21852
- this.scaleMode = "price";
21853
- /** Reference price for percent mode (first visible bar's close); recomputed per frame. */
21854
- this.percentBaseline = 0;
21855
- /** IANA time zone for the time axis + crosshair/data-window stamps (`'UTC'` default). */
21856
- this.timezone = "UTC";
21857
- /** Draw the background gridlines (price + time). Master toggle (`gridlines`
21858
- * feature); per-axis visibility + colors live in `style.gridVert`/`gridHorz`. */
21859
- this.showGrid = true;
21860
- /** Comprehensive cosmetic config (item 15): grid colors, crosshair, candle
21861
- * border/wick, fonts, separators. Serialized via the renderer's `getConfig()`/
21862
- * `applyConfig()`; every draw layer reads its knobs from here, falling back to
21863
- * the theme for any value left at its inherit default. */
21864
- this.style = defaultChartStyle();
21865
- /** Draw the price/time axis tick labels. */
21866
- this.showAxisLabels = true;
21867
- /** Strategy trade-marker display (the `tradeMarkers` feature): master toggle, the
21868
- * two text lines, and the palette. Trade markers always paint on the price pane. */
21869
- this.tradeMarkers = defaultTradeMarkersState();
21870
- /** Renderer-owned shaded time bands (session highlighting), behind grid + data. */
21871
- this.highlights = [];
21872
- /** Pre/post-market bands pushed by the host (`sessionZones` feature); null ⇒ no sessions. */
21873
- this.sessionZones = null;
21874
- /** Draw-order key of the price candles, relative to indicator series z (see `seriesZ`).
21875
- * Indicators with z below this draw BEHIND the candles; at/above draw in front.
21876
- * Default 0 with indicators mounting at z < 0 ⇒ the price reads on top of every overlay,
21877
- * and user drawings (z ≥ 1 by default) on top of the price. */
21878
- this.candleZ = 0;
21879
- /** Hide the base price series (candles/bars/line/area) without removing it — overlay
21880
- * indicators keep drawing and the pane autoscales to them. Toggled from the object tree. */
21881
- this.candlesHidden = false;
21882
- /** Per-indicator foreground draw-order key (series layer), keyed by indicator id.
21883
- * Higher = drawn later (in front). Assigned on mount to the current BOTTOM of the stack,
21884
- * so each indicator arrives behind the candles (and behind older indicators);
21885
- * `setIndicatorZ`/`bringToFront`/`sendToBack` change it. */
21886
- this.seriesZ = /* @__PURE__ */ new Map();
21887
- /** Per-pane raster layers of user drawings interleaved into the series stack — each is a
21888
- * prepainted canvas the backend composites just before the series carrying `beforeZ`.
21889
- * Rebuilt by the renderer per data frame; empty when every drawing sits over the stack. */
21890
- this.drawingSlices = /* @__PURE__ */ new Map();
21891
- /** Per-model index offset: the chart bar index of the model's `anchorTime` — its
21892
- * index-aligned payloads (dense series arrays, `bar_index` drawings) count from that
21893
- * bar. Only nonzero for models computed over a SUFFIX of the bars (whole-chart models,
21894
- * the norm, aren't stored). Recomputed by the renderer on setBars + mount/patch. */
21895
- this.anchorOffsets = /* @__PURE__ */ new Map();
21896
- /** Per-indicator private price windows (merged indicators drawn on their own scale
21897
- * column). Populated per frame for models flagged `ownScale`; absent ⇒ the model
21898
- * shares its pane's master scale. */
21899
- this.indicatorScales = /* @__PURE__ */ new Map();
21900
- /** Cached sort of `panes` by order; invalidated on add/remove/reorder. */
21901
- this.orderedCache = null;
21902
- }
21903
- /** Panes sorted top-to-bottom by `order`. Cached — callers must NOT mutate the array. */
21904
- orderedPanes() {
21905
- if (!this.orderedCache) this.orderedCache = [...this.panes.values()].sort((a, b) => a.order - b.order);
21906
- return this.orderedCache;
21907
- }
21908
- /** The session zones resolved into colored bands (pre/post-market washes from the
21909
- * config's session colors) — consumed by the same painting path as {@link highlights}. */
21910
- sessionHighlightBands() {
21911
- if (!this.sessionZones) return [];
21912
- const out = [];
21913
- for (const [from, to] of this.sessionZones.pre) out.push({ from, to, color: this.style.sessions.premarketColor });
21914
- for (const [from, to] of this.sessionZones.post) out.push({ from, to, color: this.style.sessions.postmarketColor });
21915
- return out;
21916
- }
21917
- indicatorsForPane(paneId) {
21918
- const out = [];
21919
- for (const model of this.indicators.values()) if (model.paneId === paneId) out.push(model);
21920
- return out;
21921
- }
21922
- /** Merged (own-scale) indicators on a pane, ordered by z — one axis column each. */
21923
- ownScaleIndicatorsForPane(paneId) {
21924
- return this.orderedIndicatorsForPane(paneId).filter((m) => m.ownScale === true);
21925
- }
21926
- /** Ensure a merged indicator has a private scale slot (seeded from the pane if given). */
21927
- ensureIndicatorScale(id, seed) {
21928
- let s = this.indicatorScales.get(id);
21929
- if (!s) {
21930
- const base = seed ?? { min: 0, max: 1 };
21931
- s = { scale: { ...base }, scaleTarget: { ...base }, initialized: false, manualScale: null };
21932
- this.indicatorScales.set(id, s);
21933
- }
21934
- return s;
21935
- }
21936
- dropIndicatorScale(id) {
21937
- this.indicatorScales.delete(id);
21938
- }
21939
- /** The price window a model renders on: its own scale when merged (`ownScale`), else the pane's. */
21940
- scaleFor(model, pane) {
21941
- if (model.ownScale === true) {
21942
- const s = this.indicatorScales.get(model.id);
21943
- if (s) return s.scale;
21944
- }
21945
- return pane.scale;
21946
- }
21947
- /** Apply a new top-to-bottom pane order (ids not present are ignored). */
21948
- orderPanes(orderedIds) {
21949
- orderedIds.forEach((id, i) => {
21950
- const pane = this.panes.get(id);
21951
- if (pane) pane.order = i;
21952
- });
21953
- this.orderedCache = null;
21954
- }
21955
- /** Indicators on a pane sorted by foreground z (ascending). Array#sort is stable,
21956
- * so equal-z models keep their insertion order (the default). */
21957
- orderedIndicatorsForPane(paneId) {
21958
- return this.indicatorsForPane(paneId).sort((a, b) => this.zOf(a.id) - this.zOf(b.id));
21959
- }
21960
- /** The foreground draw-order key of an indicator (0 when never assigned). */
21961
- zOf(id) {
21962
- return this.seriesZ.get(id) ?? 0;
21963
- }
21964
- /** The model's index offset: chart bar index its index-aligned payloads count from (0 = whole-chart). */
21965
- offsetOf(id) {
21966
- return this.anchorOffsets.get(id) ?? 0;
21967
- }
21968
- /** Offsets are SIGNED. Positive: the model starts after the chart's first bar (it ran
21969
- * over a suffix) — readers skip its leading chart bars. Negative: the model starts
21970
- * BEFORE it (the chart's head moved forward under a mounted model) — readers skip the
21971
- * model's own leading points, `points[i - off]` reaching further in. Storing only the
21972
- * positive case silently pinned such a model at index 0, i.e. drew it shifted. */
21973
- setAnchorOffset(id, offset) {
21974
- if (offset !== 0 && Number.isFinite(offset)) this.anchorOffsets.set(id, offset);
21975
- else this.anchorOffsets.delete(id);
21976
- }
21977
- forgetAnchorOffset(id) {
21978
- this.anchorOffsets.delete(id);
21979
- }
21980
- /** Resolve the baseline reference price for the given pane window: the explicit
21981
- * `baselineValue` when set, else `style.baseline.baselineLevel` as the price that sits
21982
- * at that fraction of the pane height. Interpolated in the same space the pane renders
21983
- * in (log when `scale.log`, else linear) so `level%` always lands at `level%` of the
21984
- * height — matching `CoordinateSystem.yToPrice`. */
21985
- baselinePriceFor(scale) {
21986
- if (this.baselineValue != null) return this.baselineValue;
21987
- const t = this.style.baseline.baselineLevel / 100;
21988
- if (scale.log && scale.min > 0 && scale.max > scale.min) {
21989
- const lo = Math.log(scale.min);
21990
- return Math.exp(lo + t * (Math.log(scale.max) - lo));
21991
- }
21992
- return scale.min + (scale.max - scale.min) * t;
21993
- }
21994
- /** Assign a default z on mount: the current bottom of the stack, so a new indicator
21995
- * paints behind the candles and behind every indicator already there — the price stays
21996
- * the top of the pile until the user restacks it. No-op if the indicator already has one. */
21997
- assignIndicatorZ(id) {
21998
- if (!this.seriesZ.has(id)) this.seriesZ.set(id, this.bottomZ() - 1);
21999
- }
22000
- forgetIndicatorZ(id) {
22001
- this.seriesZ.delete(id);
22002
- }
22003
- setIndicatorZ(id, z) {
22004
- this.seriesZ.set(id, z);
22005
- }
22006
- /** Snapshot of the current ordering for a UI/read API: `{ id, z }` sorted by z. */
22007
- indicatorZOrder() {
22008
- return [...this.seriesZ.entries()].map(([id, z]) => ({ id, z })).sort((a, b) => a.z - b.z);
22009
- }
22010
- /** The pane's series z keys (each indicator, plus the candles on the price pane), sorted
22011
- * ascending and de-duplicated — the boundaries a user drawing's z is slotted against. */
22012
- seriesBoundaries(paneId) {
22013
- const keys = /* @__PURE__ */ new Set();
22014
- if (paneId === "price") keys.add(this.candleZ);
22015
- for (const m of this.indicatorsForPane(paneId)) keys.add(this.zOf(m.id));
22016
- return [...keys].sort((a, b) => a - b);
22017
- }
22018
- /** Raise an indicator above every other layer (other indicators AND the candles). */
22019
- bringIndicatorToFront(id) {
22020
- this.seriesZ.set(id, this.topZ() + 1);
22021
- }
22022
- /** Drop an indicator below every other layer (other indicators AND the candles). */
22023
- sendIndicatorToBack(id) {
22024
- this.seriesZ.set(id, this.bottomZ() - 1);
22025
- }
22026
- topZ() {
22027
- let max = this.candleZ;
22028
- for (const z of this.seriesZ.values()) if (z > max) max = z;
22029
- return max;
22030
- }
22031
- bottomZ() {
22032
- let min = this.candleZ;
22033
- for (const z of this.seriesZ.values()) if (z < min) min = z;
22034
- return min;
22035
- }
22036
- ensurePane(id, kind, order, heightWeight) {
22037
- this.orderedCache = null;
22038
- const existing = this.panes.get(id);
22039
- if (existing) {
22040
- existing.order = order;
22041
- existing.heightWeight = heightWeight;
22042
- existing.kind = kind;
22043
- return existing;
22044
- }
22045
- 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 };
22046
- this.panes.set(id, pane);
22047
- return pane;
22048
- }
22049
- removePane(id) {
22050
- this.panes.delete(id);
22051
- this.orderedCache = null;
22052
- }
22053
- };
22054
- function paneScaleMode(scene, pane) {
22055
- return pane.kind === "price" ? scene.scaleMode : pane.scaleMode ?? "price";
22056
- }
22057
- function paneLogScale(scene, pane) {
22058
- return pane.kind === "price" ? scene.logScale : pane.logScale ?? false;
22059
- }
22060
- function paneInvert(scene, pane) {
22061
- return pane.kind === "price" ? scene.invertScale : pane.invert ?? false;
22062
- }
22063
- function percentScaleFor(scene, pane) {
22064
- const mode = paneScaleMode(scene, pane);
22065
- if (mode !== "percent" && mode !== "indexed") return void 0;
22066
- const baseline = pane.percentBaseline;
22067
- if (!Number.isFinite(baseline) || baseline === 0) return void 0;
22068
- return { baseline, indexed: mode === "indexed" };
22069
- }
22070
-
22071
- // src/renderers/native/chrome/tz.ts
22072
- function tzOffsetMs(ms, timeZone) {
22073
- if (!timeZone || timeZone === "UTC") return 0;
22074
- try {
22075
- const dtf = new Intl.DateTimeFormat("en-US", {
22076
- timeZone,
22077
- hourCycle: "h23",
22078
- year: "numeric",
22079
- month: "2-digit",
22080
- day: "2-digit",
22081
- hour: "2-digit",
22082
- minute: "2-digit",
22083
- second: "2-digit"
22084
- });
22085
- const parts = dtf.formatToParts(new Date(ms));
22086
- const get = (t) => Number(parts.find((p) => p.type === t)?.value);
22087
- const asUTC = Date.UTC(get("year"), get("month") - 1, get("day"), get("hour") % 24, get("minute"), get("second"));
22088
- return asUTC - ms;
22089
- } catch {
22090
- return 0;
22091
- }
22092
- }
22093
- function zonedDate(ms, timeZone) {
22094
- return new Date(ms + tzOffsetMs(ms, timeZone));
22095
- }
22096
-
22097
21548
  // src/renderers/native/backend/candle-lod.ts
22098
21549
  var CANDLE_BODY_MIN_SPACING = 3;
22099
21550
  var CANDLE_WICK_W = 1.5;
@@ -22383,11 +21834,6 @@ var DASH = {
22383
21834
  dashed: [6, 4],
22384
21835
  dotted: [2, 3]
22385
21836
  };
22386
- function crispHairline(cssCenter, dpr) {
22387
- const w = Math.max(1, Math.round(dpr));
22388
- const edge = Math.round(cssCenter * dpr - w / 2);
22389
- return { pos: edge / dpr, size: w / dpr };
22390
- }
22391
21837
  function joinSegments(hw) {
22392
21838
  return Math.max(6, Math.min(20, Math.round(hw * 4)));
22393
21839
  }
@@ -22397,7 +21843,6 @@ var WebGL2Backend = class {
22397
21843
  this.modelAlpha = 1;
22398
21844
  this.candleBodyAlpha = 1;
22399
21845
  this.candleStructureAlpha = 1;
22400
- this.gridAlpha = 1;
22401
21846
  this.candleBodyScale = 1;
22402
21847
  /** Set by the renderer; invoked after a context restore to request a repaint. */
22403
21848
  this.onNeedsRedraw = null;
@@ -22627,11 +22072,7 @@ var WebGL2Backend = class {
22627
22072
  }
22628
22073
  };
22629
22074
  b.alpha = 1;
22630
- if (pane.collapsed) {
22631
- this.emitGrid(b, coords, theme, dataW, pane, scene);
22632
- } else {
22633
- this.emitHighlights(b, scene, pane, coords);
22634
- this.emitGrid(b, coords, theme, dataW, pane, scene);
22075
+ if (!pane.collapsed) {
22635
22076
  const models = scene.orderedIndicatorsForPane(pane.id);
22636
22077
  const isPrice = pane.kind === "price";
22637
22078
  const effPane = (m) => {
@@ -22816,49 +22257,6 @@ var WebGL2Backend = class {
22816
22257
  this.screenProgram = null;
22817
22258
  }
22818
22259
  // ── geometry emit (mirrors Canvas2dBackend, in CSS-px space) ──
22819
- emitGrid(b, coords, theme, dataW, pane, scene) {
22820
- const top = pane.bounds.top;
22821
- const bot = pane.bounds.top + pane.bounds.height;
22822
- const { gridVert, gridHorz } = scene.style;
22823
- const dpr = coords.dpr;
22824
- b.alpha = this.gridAlpha;
22825
- if (scene.showGrid && gridVert.visible && !pane.collapsed) {
22826
- const grid = parseColor(gridVert.color ?? theme.gridColor);
22827
- const tr = coords.visibleTimeRange();
22828
- const offset = tzOffsetMs((tr.from + tr.to) / 2, scene.timezone);
22829
- for (const tick of timeTicks(tr.from, tr.to, 8, offset)) {
22830
- const x = coords.timeToX(tick.time);
22831
- if (x < 0 || x > dataW) continue;
22832
- const g = crispHairline(x, dpr);
22833
- b.rect(g.pos, top, g.size, bot - top, grid);
22834
- }
22835
- }
22836
- if (scene.showGrid && gridHorz.visible && !pane.collapsed) {
22837
- const grid = parseColor(gridHorz.color ?? theme.gridColor);
22838
- const pct = percentScaleFor(scene, pane);
22839
- for (const t of paneAxisTicks(pane.scale, pane.bounds.height, pct)) {
22840
- const y = coords.priceToY(t.price, pane.scale, pane.bounds);
22841
- if (y < top || y > bot) continue;
22842
- const g = crispHairline(y, dpr);
22843
- b.rect(0, g.pos, dataW, g.size, grid);
22844
- }
22845
- }
22846
- }
22847
- /** Renderer-owned session highlight bands, clipped per-pane (scissor reconstructs full height).
22848
- * Session-zone washes (pre/post-market) paint first, host highlights on top. */
22849
- emitHighlights(b, scene, pane, coords) {
22850
- const bands = [...scene.sessionHighlightBands(), ...scene.highlights];
22851
- if (bands.length === 0) return;
22852
- for (const band of bands) {
22853
- const x1 = coords.timeToX(band.from);
22854
- const x2 = coords.timeToX(band.to);
22855
- if (x2 < 0 || x1 > coords.width || x2 <= x1) continue;
22856
- const cx = Math.max(0, x1);
22857
- const cw = Math.min(coords.width, x2) - cx;
22858
- if (cw <= 0) continue;
22859
- b.rect(cx, pane.bounds.top, cw, pane.bounds.height, parseColor(band.color));
22860
- }
22861
- }
22862
22260
  emitBackground(b, bg, pane, coords) {
22863
22261
  const x1 = coords.timeToX(bg.from);
22864
22262
  const x2 = coords.timeToX(bg.to);
@@ -24181,6 +23579,445 @@ var KeyboardController = class {
24181
23579
  }
24182
23580
  };
24183
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
+
24184
24021
  // src/renderers/native/backend/Canvas2dBackend.ts
24185
24022
  var Canvas2dBackend = class {
24186
24023
  constructor() {
@@ -24188,7 +24025,6 @@ var Canvas2dBackend = class {
24188
24025
  this.modelAlpha = 1;
24189
24026
  this.candleBodyAlpha = 1;
24190
24027
  this.candleStructureAlpha = 1;
24191
- this.gridAlpha = 1;
24192
24028
  this.candleBodyScale = 1;
24193
24029
  this.canvas = null;
24194
24030
  this.ctx = null;
@@ -24215,8 +24051,6 @@ var Canvas2dBackend = class {
24215
24051
  if (i1 < i0) return;
24216
24052
  const barColorMap = mergeBarColors2(scene.indicators);
24217
24053
  const panes = scene.orderedPanes();
24218
- this.drawHighlights(ctx, scene, coords);
24219
- this.drawGrid(ctx, scene, coords, theme, dataW);
24220
24054
  for (const pane of panes) {
24221
24055
  if (pane.collapsed) continue;
24222
24056
  const models = scene.orderedIndicatorsForPane(pane.id);
@@ -24823,22 +24657,6 @@ var Canvas2dBackend = class {
24823
24657
  ctx.fillStyle = bg.color;
24824
24658
  ctx.fillRect(x1, pane.bounds.top, x2 - x1, pane.bounds.height);
24825
24659
  }
24826
- /** Renderer-owned session highlight bands: full-height (all panes), behind grid + data.
24827
- * Session-zone washes (pre/post-market) paint first, host highlights on top. */
24828
- drawHighlights(ctx, scene, coords) {
24829
- const bands = [...scene.sessionHighlightBands(), ...scene.highlights];
24830
- if (bands.length === 0) return;
24831
- for (const band of bands) {
24832
- const x1 = coords.timeToX(band.from);
24833
- const x2 = coords.timeToX(band.to);
24834
- if (x2 < 0 || x1 > coords.width || x2 <= x1) continue;
24835
- const cx = Math.max(0, x1);
24836
- const cw = Math.min(coords.width, x2) - cx;
24837
- if (cw <= 0) continue;
24838
- ctx.fillStyle = band.color;
24839
- ctx.fillRect(cx, 0, cw, coords.height);
24840
- }
24841
- }
24842
24660
  drawHline(ctx, pl, pane, coords, dataW, theme) {
24843
24661
  const y = Math.round(coords.priceToY(pl.price, pane.scale, pane.bounds)) + 0.5;
24844
24662
  if (y < pane.bounds.top || y > pane.bounds.top + pane.bounds.height) return;
@@ -24851,46 +24669,6 @@ var Canvas2dBackend = class {
24851
24669
  ctx.stroke();
24852
24670
  setDash(ctx, "solid");
24853
24671
  }
24854
- // ── grid (L0, behind data) ── vert/horz gate on `scene.showGrid` AND their own
24855
- // per-axis visibility (style); each uses its own color. Pane separators are drawn on the
24856
- // chrome layer (full-width, above the data) so series never overpaint them.
24857
- drawGrid(ctx, scene, coords, theme, dataW) {
24858
- const panes = scene.orderedPanes();
24859
- const { gridVert, gridHorz } = scene.style;
24860
- const vertColor = gridVert.color ?? theme.gridColor;
24861
- const horzColor = gridHorz.color ?? theme.gridColor;
24862
- ctx.lineWidth = 1;
24863
- if (scene.showGrid && gridVert.visible) {
24864
- ctx.globalAlpha = this.gridAlpha;
24865
- ctx.strokeStyle = vertColor;
24866
- const tr = coords.visibleTimeRange();
24867
- const offset = tzOffsetMs((tr.from + tr.to) / 2, scene.timezone);
24868
- ctx.beginPath();
24869
- for (const tick of timeTicks(tr.from, tr.to, 8, offset)) {
24870
- const x = Math.round(coords.timeToX(tick.time)) + 0.5;
24871
- if (x < 0 || x > dataW) continue;
24872
- ctx.moveTo(x, 0);
24873
- ctx.lineTo(x, coords.height);
24874
- }
24875
- ctx.stroke();
24876
- }
24877
- for (const pane of panes) {
24878
- if (scene.showGrid && gridHorz.visible && !pane.collapsed) {
24879
- ctx.globalAlpha = this.gridAlpha;
24880
- ctx.strokeStyle = horzColor;
24881
- const pct = percentScaleFor(scene, pane);
24882
- ctx.beginPath();
24883
- for (const t of paneAxisTicks(pane.scale, pane.bounds.height, pct)) {
24884
- const y = Math.round(coords.priceToY(t.price, pane.scale, pane.bounds)) + 0.5;
24885
- if (y < pane.bounds.top || y > pane.bounds.top + pane.bounds.height) continue;
24886
- ctx.moveTo(0, y);
24887
- ctx.lineTo(dataW, y);
24888
- }
24889
- ctx.stroke();
24890
- }
24891
- }
24892
- ctx.globalAlpha = 1;
24893
- }
24894
24672
  };
24895
24673
  function setDash(ctx, style) {
24896
24674
  if (style === "dashed") ctx.setLineDash([6, 4]);
@@ -25602,6 +25380,221 @@ var DrawingSceneRenderer = class {
25602
25380
  }
25603
25381
  };
25604
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
+
25605
25598
  // src/renderers/native/chrome/ChromeRenderer.ts
25606
25599
  var ChromeRenderer = class {
25607
25600
  constructor() {
@@ -25781,6 +25774,13 @@ var ChromeRenderer = class {
25781
25774
  if (y < pane.bounds.top + 6 || y > pane.bounds.top + pane.bounds.height - 4) continue;
25782
25775
  ctx.fillText(t.label, dataW + 6, y);
25783
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
+ }
25784
25784
  }
25785
25785
  ctx.textAlign = "start";
25786
25786
  }
@@ -26022,7 +26022,7 @@ var CrosshairRenderer = class {
26022
26022
  }
26023
26023
  }
26024
26024
  const chipBg = cs.labelBackground ?? theme.borderColor;
26025
- if (pane) {
26025
+ if (pane && pane.axisFormat !== "none") {
26026
26026
  const price = coords.yToPrice(ch.y, pane.scale, pane.bounds);
26027
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);
26028
26028
  }
@@ -26070,7 +26070,7 @@ var CrosshairRenderer = class {
26070
26070
  break;
26071
26071
  }
26072
26072
  }
26073
- if (pane) {
26073
+ if (pane && pane.axisFormat !== "none") {
26074
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);
26075
26075
  }
26076
26076
  }
@@ -26113,6 +26113,156 @@ function formatStamp(ms, timeZone) {
26113
26113
  return `${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}`;
26114
26114
  }
26115
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
+
26116
26266
  // src/renderers/native/chrome/SettingsDialog.ts
26117
26267
  var BUILTIN_STYLE_LABELS2 = {
26118
26268
  candles: "Candles",
@@ -26231,12 +26381,21 @@ var SettingsDialog = class {
26231
26381
  this.activeSection = null;
26232
26382
  /** Mobile chrome: fullscreen card, burger-opened section sidebar, TOC as top tabs. */
26233
26383
  this.mobileLayout = false;
26384
+ /** The visibility policy: setting ids hidden by the host (subtree semantics). */
26385
+ this.hiddenSettings = /* @__PURE__ */ new Set();
26234
26386
  if (getComputedStyle(container).position === "static") container.style.position = "relative";
26235
26387
  }
26236
26388
  /** Host-app sections (e.g. the widget's Status line tab) — re-shown on next open. */
26237
26389
  setHostSections(sections) {
26238
26390
  this.hostSections = sections;
26239
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
+ }
26240
26399
  /** Configure the Canvas → Theme row (see {@link themeControl}); null hides the row. */
26241
26400
  setThemeControl(current, onSelect) {
26242
26401
  this.themeControl = { current, onSelect };
@@ -26304,8 +26463,12 @@ var SettingsDialog = class {
26304
26463
  }
26305
26464
  const body = document.createElement("div");
26306
26465
  body.style.cssText = "display:flex;flex-direction:column;gap:0;";
26307
- body.append(this.section("Symbol"));
26308
- 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"));
26309
26472
  const groups = {};
26310
26473
  const showActive = (style) => {
26311
26474
  const active = style === "heikinashi" ? "candles" : style;
@@ -26314,64 +26477,64 @@ var SettingsDialog = class {
26314
26477
  }
26315
26478
  };
26316
26479
  body.append(
26317
- 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) => {
26318
26481
  this.emit({ series: { style: v } });
26319
26482
  showActive(v);
26320
26483
  this.syncTypeTabs?.(v);
26321
- })
26484
+ }), "symbol.type")
26322
26485
  );
26323
- const candles = this.group();
26486
+ const candles = sid(this.group(), "symbol.style.candles");
26324
26487
  candles.append(this.sectionTitle("Candles"));
26325
- 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 } }), [
26326
26489
  this.swatch(config.candles.upColor, (v) => this.emit({ candles: { upColor: v } })),
26327
26490
  this.swatch(config.candles.downColor, (v) => this.emit({ candles: { downColor: v } }))
26328
- ]));
26329
- 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 } }), [
26330
26493
  this.swatch(config.candles.borderUpColor, (v) => this.emit({ candles: { borderUpColor: v } })),
26331
26494
  this.swatch(config.candles.borderDownColor, (v) => this.emit({ candles: { borderDownColor: v } }))
26332
- ]));
26333
- 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 } }), [
26334
26497
  this.swatch(config.candles.wickUpColor, (v) => this.emit({ candles: { wickUpColor: v } })),
26335
26498
  this.swatch(config.candles.wickDownColor, (v) => this.emit({ candles: { wickDownColor: v } }))
26336
- ]));
26337
- 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"));
26338
26501
  groups.candles = candles;
26339
26502
  body.append(candles);
26340
- const bars = this.group();
26503
+ const bars = sid(this.group(), "symbol.style.bars");
26341
26504
  bars.append(this.sectionTitle("Bars"));
26342
- bars.append(this.colorRow("Color Up", config.bars.upColor, (v) => this.emit({ bars: { upColor: v } })));
26343
- bars.append(this.colorRow("Color Down", config.bars.downColor, (v) => this.emit({ bars: { downColor: v } })));
26344
- 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"));
26345
26508
  groups.bars = bars;
26346
26509
  body.append(bars);
26347
- const line = this.group();
26510
+ const line = sid(this.group(), "symbol.style.line");
26348
26511
  line.append(this.sectionTitle("Line"));
26349
- line.append(this.colorRow("Color", config.line.color, (v) => this.emit({ line: { color: v } })));
26350
- 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"));
26351
26514
  groups.line = line;
26352
26515
  body.append(line);
26353
- const area = this.group();
26516
+ const area = sid(this.group(), "symbol.style.area");
26354
26517
  area.append(this.sectionTitle("Area"));
26355
- area.append(this.colorRow("Line color", config.area.lineColor, (v) => this.emit({ area: { lineColor: v } })));
26356
- area.append(this.numberRow("Width", config.area.width, 1, 10, 1, (v) => this.emit({ area: { width: v } })));
26357
- area.append(this.colorRow("Top fill", config.area.topColor, (v) => this.emit({ area: { topColor: v } })));
26358
- 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"));
26359
26522
  groups.area = area;
26360
26523
  body.append(area);
26361
- const baseline = this.group();
26524
+ const baseline = sid(this.group(), "symbol.style.baseline");
26362
26525
  baseline.append(this.sectionTitle("Baseline"));
26363
- baseline.append(this.rowWith("Top line", [this.swatch(config.baseline.topLineColor, (v) => this.emit({ baseline: { topLineColor: v } }))]));
26364
- baseline.append(this.rowWith("Bottom line", [this.swatch(config.baseline.bottomLineColor, (v) => this.emit({ baseline: { bottomLineColor: v } }))]));
26365
- 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", [
26366
26529
  this.swatch(config.baseline.topFillColor, (v) => this.emit({ baseline: { topFillColor: v } })),
26367
26530
  this.swatch(config.baseline.topFillColor2, (v) => this.emit({ baseline: { topFillColor2: v } }))
26368
- ]));
26369
- baseline.append(this.rowWith("Fill bottom area", [
26531
+ ]), "symbol.style.baseline.fill-top"));
26532
+ baseline.append(sid(this.rowWith("Fill bottom area", [
26370
26533
  this.swatch(config.baseline.bottomFillColor2, (v) => this.emit({ baseline: { bottomFillColor2: v } })),
26371
26534
  this.swatch(config.baseline.bottomFillColor, (v) => this.emit({ baseline: { bottomFillColor: v } }))
26372
- ]));
26373
- baseline.append(this.numberRow("Base level %", config.baseline.baselineLevel, 0, 100, 1, (v) => this.emit({ baseline: { baselineLevel: v } })));
26374
- 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"));
26375
26538
  groups.baseline = baseline;
26376
26539
  body.append(baseline);
26377
26540
  for (const def of chartTypes()) {
@@ -26379,32 +26542,36 @@ var SettingsDialog = class {
26379
26542
  const bag = config.chartTypes[def.id] ?? {};
26380
26543
  const colorOf2 = (key, shared) => typeof bag[key] === "string" && bag[key] !== "" ? bag[key] : shared;
26381
26544
  const boolOf = (key, shared) => typeof bag[key] === "boolean" ? bag[key] : shared;
26382
- const g = this.group();
26545
+ const g = sid(this.group(), `symbol.style.${def.id}`);
26383
26546
  g.append(this.sectionTitle("Candles"));
26384
- 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), [
26385
26548
  this.swatch(colorOf2("candleUpColor", config.candles.upColor), (v) => this.emitType(def.id, "candleUpColor", v)),
26386
26549
  this.swatch(colorOf2("candleDownColor", config.candles.downColor), (v) => this.emitType(def.id, "candleDownColor", v))
26387
- ]));
26388
- 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), [
26389
26552
  this.swatch(colorOf2("candleBorderUpColor", config.candles.borderUpColor), (v) => this.emitType(def.id, "candleBorderUpColor", v)),
26390
26553
  this.swatch(colorOf2("candleBorderDownColor", config.candles.borderDownColor), (v) => this.emitType(def.id, "candleBorderDownColor", v))
26391
- ]));
26392
- 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), [
26393
26556
  this.swatch(colorOf2("candleWickUpColor", config.candles.wickUpColor), (v) => this.emitType(def.id, "candleWickUpColor", v)),
26394
26557
  this.swatch(colorOf2("candleWickDownColor", config.candles.wickDownColor), (v) => this.emitType(def.id, "candleWickDownColor", v))
26395
- ]));
26396
- 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`));
26397
26560
  groups[def.id] = g;
26398
26561
  body.append(g);
26399
26562
  }
26400
26563
  showActive(config.series.style);
26401
- body.append(this.sectionTitle("Time zone"));
26402
- 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"));
26403
26566
  const renderHostSections = (placement) => {
26404
26567
  for (const hs of this.hostSections) {
26405
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;
26406
26573
  body.append(placement === "symbol" ? this.sectionTitle(hs.title) : this.section(hs.title));
26407
- for (const hr of hs.rows) {
26574
+ for (const hr of rows) {
26408
26575
  if (hr.kind === "heading") body.append(this.sectionTitle(hr.label));
26409
26576
  else if (hr.kind === "toggle") body.append(this.boolRow(hr.label, hr.get(), (v) => hr.set(v)));
26410
26577
  else if (hr.kind === "color") body.append(this.colorRow(hr.label, hr.get(), (v) => hr.set(v)));
@@ -26417,53 +26584,69 @@ var SettingsDialog = class {
26417
26584
  const typeSettings = def.settings;
26418
26585
  if (!typeSettings) continue;
26419
26586
  if ((typeSettings.placement ?? "end") !== placement) continue;
26587
+ if (settingsIdHidden(`type:${def.id}`, this.hiddenSettings)) continue;
26420
26588
  this.chartTypeSection(def.id, typeSettings, config, body);
26421
26589
  }
26422
26590
  };
26423
26591
  renderHostSections("symbol");
26424
26592
  renderChartTypeSections("after-symbol");
26425
26593
  renderHostSections("after-symbol");
26426
- body.append(this.section("Scales and lines"));
26427
- 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"));
26428
26596
  body.append(
26429
- this.selectRowLabeled(
26597
+ sid(this.selectRowLabeled(
26430
26598
  "Mode",
26431
26599
  config.priceScale.log ? "log" : config.priceScale.mode,
26432
26600
  [["price", "Regular"], ["percent", "Percent"], ["indexed", "Indexed to 100"], ["log", "Logarithmic"]],
26433
26601
  (v) => this.emit({ priceScale: v === "log" ? { mode: "price", log: true } : { mode: v, log: false } })
26434
- )
26602
+ ), "scales.price-scale.mode")
26435
26603
  );
26436
- body.append(this.boolRow("Invert scale", config.priceScale.invert, (v) => this.emit({ priceScale: { invert: v } })));
26437
- body.append(this.separator());
26438
- body.append(this.boolRow("Last Price Line", config.priceScale.currentPriceLine, (v) => this.emit({ priceScale: { currentPriceLine: v } })));
26439
- body.append(this.boolRow("Last price label", config.priceScale.priceLabel, (v) => this.emit({ priceScale: { priceLabel: v } })));
26440
- body.append(this.boolRow("Countdown to bar close", config.priceScale.countdown, (v) => this.emit({ priceScale: { countdown: v } })));
26441
- body.append(this.boolRow("Axis labels", config.priceScale.labelsVisible, (v) => this.emit({ priceScale: { labelsVisible: v } })));
26442
- body.append(this.colorRow("Scale border color", config.priceScale.borderColor, (v) => this.emit({ priceScale: { borderColor: v } })));
26443
- body.append(this.sectionTitle("Crosshair"));
26444
- body.append(this.colorRow("Color", config.crosshair.color, (v) => this.emit({ crosshair: { color: v } })));
26445
- body.append(this.numberRow("Width", config.crosshair.width, 0.5, 8, 0.5, (v) => this.emit({ crosshair: { width: v } })));
26446
- body.append(this.selectRowLabeled("Style", config.crosshair.style, [["solid", "Solid"], ["dashed", "Dashed"], ["dotted", "Dotted"]], (v) => this.emit({ crosshair: { style: v } })));
26447
- body.append(this.section("Canvas"));
26448
- body.append(this.sectionTitle("Background & text"));
26449
- body.append(this.colorRow("Background", config.layout.background, (v) => this.emit({ layout: { background: v } })));
26450
- body.append(this.colorRow("Text color", config.layout.textColor, (v) => this.emit({ layout: { textColor: v } })));
26451
- body.append(this.numberRow("Text size", config.layout.fontSize, 6, 32, 1, (v) => this.emit({ layout: { fontSize: v } })));
26452
- body.append(this.colorRow("Pane separator color", config.panes.separatorColor, (v) => this.emit({ panes: { separatorColor: v } })));
26453
- body.append(this.sectionTitle("Grid"));
26454
- 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 } } }), [
26455
26623
  this.swatch(config.grid.vertLines.color, (v) => this.emit({ grid: { vertLines: { color: v } } }))
26456
- ]));
26457
- 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 } } }), [
26458
26626
  this.swatch(config.grid.horzLines.color, (v) => this.emit({ grid: { horzLines: { color: v } } }))
26459
- ]));
26627
+ ]), "canvas.grid.horizontal"));
26460
26628
  if (this.themeControl) {
26461
26629
  const tc = this.themeControl;
26462
- body.append(this.sectionTitle("Theme"));
26463
- 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"));
26464
26632
  }
26465
26633
  renderChartTypeSections("end");
26466
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
+ }
26467
26650
  const shell = document.createElement("div");
26468
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;";
26469
26652
  const rail = document.createElement("div");
@@ -26501,6 +26684,9 @@ var SettingsDialog = class {
26501
26684
  }
26502
26685
  if (current) current.appendChild(child);
26503
26686
  }
26687
+ for (let i = panes.length - 1; i >= 0; i--) {
26688
+ if (panes[i].el.childElementCount === 0) panes.splice(i, 1);
26689
+ }
26504
26690
  this.syncTypeTabs = (active) => {
26505
26691
  let hidActive = false;
26506
26692
  panes.forEach((pn, idx) => {
@@ -26640,19 +26826,25 @@ var SettingsDialog = class {
26640
26826
  this.emitType(typeId, key, v);
26641
26827
  for (const r of refreshers) r();
26642
26828
  };
26829
+ const scope = `type:${typeId}`;
26643
26830
  if (section.instances && section.instances.length > 0) {
26644
- 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));
26645
26833
  } else if (section.rows) {
26646
- if (section.layout === "grouped") body.append(this.groupedRows(`${typeId}/rows`, section.rows, bag, put, refreshers));
26647
- 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);
26648
26837
  }
26649
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;
26650
26842
  const subMarker = this.section(sub.title);
26651
26843
  subMarker.dataset.sdStyle = typeId;
26652
26844
  subMarker.dataset.sdVisibility = section.visibility ?? "active";
26653
26845
  subMarker.dataset.sdSub = "1";
26654
26846
  body.append(subMarker);
26655
- 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));
26656
26848
  }
26657
26849
  for (const r of refreshers) r();
26658
26850
  }
@@ -30942,6 +31134,94 @@ function resizeSplit(split, dyTotal, minPx = MIN_PANE_PX) {
30942
31134
  return { above, below: combinedWeight - above };
30943
31135
  }
30944
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
+
30945
31225
  // src/renderers/native/volume/paintVolume.ts
30946
31226
  var VOLUME_FILL_ALPHA = 0.5;
30947
31227
  function paintVolume(ctx, bars, geom, colors) {
@@ -31035,6 +31315,19 @@ function rendererLayers() {
31035
31315
  return [...registry4.values()];
31036
31316
  }
31037
31317
 
31318
+ // src/renderers/native/core/layerStacking.ts
31319
+ function stackLayers(entries, candleZ) {
31320
+ const keyed = entries.map((e) => ({
31321
+ id: e.id,
31322
+ key: e.ownerZ ?? (e.placement === "below-data" ? -Infinity : candleZ)
31323
+ }));
31324
+ keyed.sort((a, b) => a.key - b.key);
31325
+ const below = [];
31326
+ const above = [];
31327
+ for (const k of keyed) (k.key < candleZ ? below : above).push(k.id);
31328
+ return { below, above };
31329
+ }
31330
+
31038
31331
  // src/renderers/shared/dom-raster.ts
31039
31332
  function hasInk(color) {
31040
31333
  if (!color || color === "transparent") return false;
@@ -31404,9 +31697,12 @@ var NativeRenderer = class {
31404
31697
  // px reserved on the left for the docked drawings toolbar (0 when hidden)
31405
31698
  this.mountContainer = null;
31406
31699
  this.userDrawings = null;
31700
+ this.backdropRenderer = new BackdropRenderer();
31407
31701
  this.volumeRenderer = new VolumeRenderer();
31408
31702
  /** SDK renderer layers instantiated at mount ({@link registerRendererLayer}). */
31409
31703
  this.extLayers = [];
31704
+ /** Last applied layer-canvas order (ids below + above the data canvas) — re-slotted only on change. */
31705
+ this.layerOrderSig = "";
31410
31706
  // The attribution mark (see chrome/AttributionMark + the NOTICE file): default-on;
31411
31707
  // disabling requires an equivalent visible attribution elsewhere in the host UI.
31412
31708
  this.attributionEl = null;
@@ -31529,6 +31825,9 @@ var NativeRenderer = class {
31529
31825
  // bar under the crosshair; null when off a bar
31530
31826
  // ── settings dialog (rich, serializable config — item 15) ──
31531
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 = [];
31532
31831
  /** Where modal dialogs mount — a HOST override (multi-chart shells pass their root
31533
31832
  * so dialogs center globally instead of clipping inside one cell). Null = the plot. */
31534
31833
  this.dialogHost = null;
@@ -32201,6 +32500,7 @@ var NativeRenderer = class {
32201
32500
  }
32202
32501
  this.settingsDialog.setTheme(this.theme);
32203
32502
  this.settingsDialog.setHostSections(this.hostSettingsSections);
32503
+ this.settingsDialog.setHiddenSettings(this.hiddenSettings);
32204
32504
  this.syncThemeControl();
32205
32505
  this.settingsDialog.toggle(
32206
32506
  this.getConfig(),
@@ -32337,9 +32637,7 @@ var NativeRenderer = class {
32337
32637
  if (el.getAttribute("data-vela-screenshot") === "under") rasterizeOverlay(ctx, el, frame);
32338
32638
  }
32339
32639
  }
32340
- const below = this.extLayers.filter((l) => l.def.placement === "below-data").map((l) => l.canvas);
32341
- const above = this.extLayers.filter((l) => l.def.placement !== "below-data").map((l) => l.canvas);
32342
- for (const canvas of [...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above, this.chromeCanvas, this.drawingsCanvas]) {
32640
+ for (const canvas of [...this.canvasPile(), this.chromeCanvas, this.drawingsCanvas]) {
32343
32641
  if (canvas && canvas.width > 0 && canvas.height > 0) ctx.drawImage(canvas, 0, 0);
32344
32642
  }
32345
32643
  if (frame) {
@@ -32450,6 +32748,8 @@ var NativeRenderer = class {
32450
32748
  this.wrapper = document.createElement("div");
32451
32749
  Object.assign(this.wrapper.style, { position: "relative", width: "100%", height: "100%", overflow: "hidden", cursor: "crosshair", userSelect: "none", webkitUserSelect: "none" });
32452
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" });
32453
32753
  this.volumeCanvas = document.createElement("canvas");
32454
32754
  Object.assign(this.volumeCanvas.style, { position: "absolute", inset: "0", width: "100%", height: "100%", pointerEvents: "none" });
32455
32755
  this.dataCanvas = this.createGeometryBackend();
@@ -32472,7 +32772,8 @@ var NativeRenderer = class {
32472
32772
  });
32473
32773
  const below = this.extLayers.filter((l) => l.def.placement === "below-data").map((l) => l.canvas);
32474
32774
  const above = this.extLayers.filter((l) => l.def.placement !== "below-data").map((l) => l.canvas);
32475
- 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);
32776
+ this.layerOrderSig = "";
32476
32777
  this.wrapper.appendChild(this.plot);
32477
32778
  this.factoryConfig = this.getConfig();
32478
32779
  this.attributionEl = this.buildAttributionEl();
@@ -32481,6 +32782,7 @@ var NativeRenderer = class {
32481
32782
  this.wrapper.appendChild(this.attributionEl);
32482
32783
  container.appendChild(this.wrapper);
32483
32784
  this.applyBackground();
32785
+ this.backdropRenderer.mount(this.backdropCanvas);
32484
32786
  this.volumeRenderer.mount(this.volumeCanvas);
32485
32787
  this.vpvrRenderer.mount(this.vpvrCanvas);
32486
32788
  for (const l of this.extLayers) l.instance.mount(l.canvas);
@@ -32770,6 +33072,7 @@ var NativeRenderer = class {
32770
33072
  this.scrollButton = null;
32771
33073
  for (const l of this.extLayers) l.instance.destroy?.();
32772
33074
  this.extLayers = [];
33075
+ this.backdropRenderer.destroy();
32773
33076
  this.volumeRenderer.destroy();
32774
33077
  this.vpvrRenderer.destroy();
32775
33078
  this.backend.destroy();
@@ -33001,7 +33304,8 @@ var NativeRenderer = class {
33001
33304
  mountIndicator(model) {
33002
33305
  this.scene.indicators.set(model.id, model);
33003
33306
  this.refreshAnchorOffset(model);
33004
- this.scene.assignIndicatorZ(model.id);
33307
+ if (model.native && this.extLayers.some((l) => l.def.id === model.native.type)) this.scene.assignIndicatorZTop(model.id);
33308
+ else this.scene.assignIndicatorZ(model.id);
33005
33309
  this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
33006
33310
  native: !!model.native,
33007
33311
  ...model.shorttitle ? { settingsTitle: model.title } : {}
@@ -33148,6 +33452,13 @@ var NativeRenderer = class {
33148
33452
  this.hostSettingsSections = sections;
33149
33453
  this.settingsDialog?.setHostSections(sections);
33150
33454
  }
33455
+ setSettingsVisibility(policy) {
33456
+ this.hiddenSettings = [...policy.hidden ?? []];
33457
+ this.settingsDialog?.setHiddenSettings(this.hiddenSettings);
33458
+ }
33459
+ listSettingsIds() {
33460
+ return settingsIdCatalog(this.hostSettingsSections);
33461
+ }
33151
33462
  onChartTypeSettingsChange(cb) {
33152
33463
  this.chartTypeSettingsCbs.add(cb);
33153
33464
  return () => this.chartTypeSettingsCbs.delete(cb);
@@ -33820,10 +34131,17 @@ var NativeRenderer = class {
33820
34131
  const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
33821
34132
  for (const l of this.extLayers) {
33822
34133
  if (!l.def.repaintOnCursor) continue;
33823
- l.instance.render(this.extLayerArgs(l.def.id, pane.scale, pane.bounds, nowMs));
34134
+ const lp = this.layerPane(l.def.id) ?? pane;
34135
+ if (lp.collapsed) continue;
34136
+ l.instance.render(this.extLayerArgs(l.def.id, lp.scale, lp.bounds, nowMs));
33824
34137
  if (this.animZoom && l.instance.animating?.()) this.animator.start();
33825
34138
  }
33826
34139
  }
34140
+ /** Blank one SDK layer canvas (a collapsed host pane suppresses the layer's painting). */
34141
+ clearLayerCanvas(canvas) {
34142
+ if (canvas.width === 0 || canvas.height === 0) return;
34143
+ canvas.getContext("2d")?.clearRect(0, 0, canvas.width, canvas.height);
34144
+ }
33827
34145
  /** One frame's args for an SDK renderer layer (shared by the data + cursor paint paths). */
33828
34146
  extLayerArgs(id, scale, bounds, nowMs) {
33829
34147
  return {
@@ -33842,11 +34160,12 @@ var NativeRenderer = class {
33842
34160
  }
33843
34161
  /** Paint the below-data (L-1) + geometry (L0) + chrome (L1) layers from the current scene/coords. */
33844
34162
  paintData() {
34163
+ this.syncLayerCanvasOrder();
33845
34164
  this.stampScaleInvert();
33846
34165
  this.backend.modelAlpha = this.modelAlpha;
33847
34166
  this.backend.candleBodyAlpha = this.candleBodyAlpha;
33848
34167
  this.backend.candleStructureAlpha = this.candleStructureAlpha;
33849
- this.backend.gridAlpha = 1;
34168
+ let gridAlpha = 1;
33850
34169
  const candleBodyScale = 1;
33851
34170
  this.backend.candleBodyScale = candleBodyScale;
33852
34171
  const pane = this.scene.panes.get(PRICE_PANE_ID2);
@@ -33872,15 +34191,20 @@ var NativeRenderer = class {
33872
34191
  const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
33873
34192
  let folded = null;
33874
34193
  for (const l of this.extLayers) {
33875
- const args = this.extLayerArgs(l.def.id, pane.scale, pane.bounds, nowMs);
34194
+ const lp = this.layerPane(l.def.id) ?? pane;
34195
+ if (lp.collapsed) {
34196
+ this.clearLayerCanvas(l.canvas);
34197
+ continue;
34198
+ }
34199
+ const args = this.extLayerArgs(l.def.id, lp.scale, lp.bounds, nowMs);
33876
34200
  l.instance.render(args);
33877
- folded = foldBaseModulation(folded, l.instance.modulateBase?.(args) ?? null);
34201
+ if (lp === pane) folded = foldBaseModulation(folded, l.instance.modulateBase?.(args) ?? null);
33878
34202
  if (this.animZoom && l.instance.animating?.()) this.animator.start();
33879
34203
  }
33880
34204
  if (folded) {
33881
34205
  if (folded.candleBodyScale != null) this.backend.candleBodyScale = clamp012(folded.candleBodyScale) || 0.01;
33882
34206
  if (folded.candleBodyAlpha != null) this.backend.candleBodyAlpha = clamp012(folded.candleBodyAlpha) * this.candleBodyAlpha;
33883
- if (folded.gridAlpha != null) this.backend.gridAlpha = clamp012(folded.gridAlpha);
34207
+ if (folded.gridAlpha != null) gridAlpha = clamp012(folded.gridAlpha);
33884
34208
  }
33885
34209
  }
33886
34210
  const li = this.bars.length - 1;
@@ -33888,6 +34212,7 @@ var NativeRenderer = class {
33888
34212
  const easeLive = !!liveActual && this.liveEaseTime === liveActual.time && (liveActual.high !== this.liveEaseHigh || liveActual.low !== this.liveEaseLow || liveActual.close !== this.liveEaseClose);
33889
34213
  if (easeLive && liveActual) this.bars[li] = { ...liveActual, high: this.liveEaseHigh, low: this.liveEaseLow, close: this.liveEaseClose };
33890
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);
33891
34216
  this.backend.render(this.scene, this.coords, this.theme);
33892
34217
  this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
33893
34218
  this.userDrawings?.render();
@@ -34030,12 +34355,21 @@ var NativeRenderer = class {
34030
34355
  pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
34031
34356
  pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
34032
34357
  pane.axisFormat = void 0;
34358
+ pane.axisBands = void 0;
34033
34359
  if (this.volumeOwnsPane(pane, masterModels)) {
34034
34360
  const maxVol = this.maxVisibleVolume(i0, i1);
34035
34361
  if (maxVol > 0) {
34036
34362
  pane.scaleTarget = { min: 0, max: maxVol / VOLUME_PANE_FILL_FRAC };
34037
34363
  pane.axisFormat = "volume";
34038
34364
  }
34365
+ } else if (this.layerNativesOwnPane(pane, masterModels)) {
34366
+ pane.scaleTarget = computePaneScale([], this.bars, true, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
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
+ }
34039
34373
  }
34040
34374
  if (pane === pricePane && this.scene.tradeMarkers.visible && pane.bounds.height > 0) {
34041
34375
  const th = this.tradesScaleHints(vr);
@@ -34122,6 +34456,59 @@ var NativeRenderer = class {
34122
34456
  }
34123
34457
  return null;
34124
34458
  }
34459
+ /** The mounted native indicator that OWNS an SDK layer — the one whose type equals the
34460
+ * layer id (the id doubles as the data channel, so the pairing is the SDK's own
34461
+ * contract). Null for chart-type channels and while the owner is hidden (a hidden
34462
+ * indicator leaves the scene; its cleared data channel paints nothing anyway). */
34463
+ layerOwner(layerId) {
34464
+ for (const m of this.scene.indicators.values()) {
34465
+ if (m.native?.type === layerId) return m;
34466
+ }
34467
+ return null;
34468
+ }
34469
+ /** The pane an SDK layer paints on: its owner's pane, else the price pane. */
34470
+ layerPane(layerId) {
34471
+ const owner = this.layerOwner(layerId);
34472
+ const paneId = owner ? owner.paneId ?? PRICE_PANE_ID2 : PRICE_PANE_ID2;
34473
+ return this.scene.panes.get(paneId) ?? null;
34474
+ }
34475
+ /** The SDK layer canvases split around the data canvas, each side back-to-front:
34476
+ * owned layers by their owner's z key against the candles' (an indicator restacked
34477
+ * below the candles takes its layer canvas along), unowned by declared placement. */
34478
+ orderedLayerCanvases() {
34479
+ const byId = new Map(this.extLayers.map((l) => [l.def.id, l.canvas]));
34480
+ const { below, above } = stackLayers(
34481
+ this.extLayers.map((l) => {
34482
+ const owner = this.layerOwner(l.def.id);
34483
+ return {
34484
+ id: l.def.id,
34485
+ placement: l.def.placement === "below-data" ? "below-data" : "above-data",
34486
+ ownerZ: owner ? this.scene.zOf(owner.id) : null
34487
+ };
34488
+ }),
34489
+ this.scene.candleZ
34490
+ );
34491
+ return { below: below.map((id) => byId.get(id)), above: above.map((id) => byId.get(id)) };
34492
+ }
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. */
34495
+ canvasPile() {
34496
+ const { below, above } = this.orderedLayerCanvases();
34497
+ return [this.backdropCanvas, ...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above];
34498
+ }
34499
+ /** Re-slot the SDK layer canvases in the plot when the computed order changed (a z
34500
+ * write, a restored config, an indicator mount/remove/restack). Runs at the top of
34501
+ * every data frame; a no-op when the signature is unchanged. Re-inserting an
34502
+ * absolutely-positioned, pointer-transparent canvas repaints nothing by itself. */
34503
+ syncLayerCanvasOrder() {
34504
+ if (this.extLayers.length === 0 || !this.plot) return;
34505
+ const { below, above } = this.orderedLayerCanvases();
34506
+ const sig = [...below.map((c) => this.extLayers.find((l) => l.canvas === c).def.id), "|", ...above.map((c) => this.extLayers.find((l) => l.canvas === c).def.id)].join(",");
34507
+ if (sig === this.layerOrderSig) return;
34508
+ this.layerOrderSig = sig;
34509
+ for (const c of below) this.plot.insertBefore(c, this.dataCanvas);
34510
+ for (const c of above) this.plot.insertBefore(c, this.chromeCanvas);
34511
+ }
34125
34512
  /** True when an active volume layer is this study pane's ONLY content — so its scale should
34126
34513
  * come from volume, not the empty {0,1} placeholder. (In the price pane, or alongside a real
34127
34514
  * series, volume stays a bottom overlay and the master scale wins.) */
@@ -34131,6 +34518,15 @@ var NativeRenderer = class {
34131
34518
  if (this.nativeLayerPane("volume") !== pane) return false;
34132
34519
  return masterModels.every((m) => m.native?.type === "volume");
34133
34520
  }
34521
+ /** True when this study pane's master content is only SDK-layer natives (series-less
34522
+ * models whose type names a mounted layer) — its scale then follows the visible bars
34523
+ * (see the call site). Any real master series takes over the scale as usual. */
34524
+ layerNativesOwnPane(pane, masterModels) {
34525
+ if (pane.kind === "price" || masterModels.length === 0) return false;
34526
+ return masterModels.every(
34527
+ (m) => m.series.length === 0 && !!m.native && this.extLayers.some((l) => l.def.id === m.native.type)
34528
+ );
34529
+ }
34134
34530
  /** Per-pane scale state for a host UI (e.g. a price-axis context menu): the pane's pixel
34135
34531
  * band (`top`/`height`, so a click y maps to a pane) plus its current axis `mode`/`log`.
34136
34532
  * Top-to-bottom order. Every pane is independent — the price pane from the scene setting,
@@ -34415,6 +34811,8 @@ var NativeRenderer = class {
34415
34811
  const ph = h;
34416
34812
  this.dataCanvas.width = Math.round(pw * dpr);
34417
34813
  this.dataCanvas.height = Math.round(ph * dpr);
34814
+ this.backdropCanvas.width = this.dataCanvas.width;
34815
+ this.backdropCanvas.height = this.dataCanvas.height;
34418
34816
  this.volumeCanvas.width = this.dataCanvas.width;
34419
34817
  this.volumeCanvas.height = this.dataCanvas.height;
34420
34818
  for (const l of this.extLayers) {
@@ -34556,35 +34954,6 @@ function normalizeBars(bars) {
34556
34954
  return out;
34557
34955
  }
34558
34956
 
34559
- // src/core/price-styles/heikin-ashi.ts
34560
- function heikinAshiNext(raw, prevHa) {
34561
- const haClose = (raw.open + raw.high + raw.low + raw.close) / 4;
34562
- const haOpen = prevHa ? (prevHa.open + prevHa.close) / 2 : (raw.open + raw.close) / 2;
34563
- return {
34564
- time: raw.time,
34565
- open: haOpen,
34566
- high: Math.max(raw.high, haOpen, haClose),
34567
- low: Math.min(raw.low, haOpen, haClose),
34568
- close: haClose,
34569
- ...raw.volume != null ? { volume: raw.volume } : {}
34570
- };
34571
- }
34572
- function heikinAshiFull(raw) {
34573
- const out = new Array(raw.length);
34574
- let prev2;
34575
- for (let i = 0; i < raw.length; i += 1) {
34576
- prev2 = heikinAshiNext(raw[i], prev2);
34577
- out[i] = prev2;
34578
- }
34579
- return out;
34580
- }
34581
-
34582
- // src/chart-types/builtins.ts
34583
- var HEIKIN_ASHI = { full: heikinAshiFull, next: heikinAshiNext };
34584
- function registerBuiltinChartTypes() {
34585
- registerChartType({ id: "heikinashi", label: "Heikin Ashi", barTransform: HEIKIN_ASHI });
34586
- }
34587
-
34588
34957
  // src/core/native-indicators/volume/VolumeIndicator.ts
34589
34958
  var DEFAULT_UP = BULLISH;
34590
34959
  var DEFAULT_DOWN = BEARISH;
@@ -34781,6 +35150,7 @@ var Vela = class {
34781
35150
  // the volume indicator is on by default
34782
35151
  };
34783
35152
  this.rendererControl = new RendererControl(renderer);
35153
+ if (options.settings) this.rendererControl.setSettingsVisibility(options.settings);
34784
35154
  this.dataControl = new DataControl(feed);
34785
35155
  this.orchestrator = new EngineOrchestrator(element, renderer, feed, engines, config, this.dataControl);
34786
35156
  const defaults2 = rendererDefaults();
@@ -35911,8 +36281,8 @@ function seedDefaults(opts) {
35911
36281
  };
35912
36282
  }
35913
36283
  function cellChartDefaults(opts) {
35914
- const { renderer, defaultLanguage, currentPriceLine, logScale, animations, glow, upColor, downColor, drawings } = opts;
35915
- 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 };
35916
36286
  }
35917
36287
  function cellDrawings(opt) {
35918
36288
  if (opt === false) return false;
@@ -35925,7 +36295,11 @@ var ChartCell = class {
35925
36295
  this.deps = deps;
35926
36296
  /** This cell's unified app+drawings undo timeline (the shared Ctrl+Z routes here). */
35927
36297
  this.history = new WidgetHistory(() => this.inner);
35928
- /** 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`). */
35929
36303
  this.instances = [];
35930
36304
  /** The native-indicator catalog with this cell's live supported/present flags. */
35931
36305
  this.nativeCatalog = [];
@@ -35950,6 +36324,14 @@ var ChartCell = class {
35950
36324
  this.presentNatives = [];
35951
36325
  this.rangeBars = 0;
35952
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 = {};
35953
36335
  /** Indicator titles (this cell's in-chart legend rows) shown. */
35954
36336
  this.indicatorTitlesOn = true;
35955
36337
  /** Plot values beside this cell's legend titles shown. */
@@ -36016,9 +36398,16 @@ var ChartCell = class {
36016
36398
  this.pendingManifestNames = [...seed.indicators.manifest];
36017
36399
  }
36018
36400
  this.volumeIntent = seed.indicators ? seed.indicators.natives.includes("volume") : deps.volume;
36401
+ this.extState = { ...seed.ext ?? {} };
36019
36402
  this.inner.on("load:end", () => {
36020
36403
  this.volumeMayBePending = false;
36021
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
+ });
36022
36411
  this.inner.on("load:start", () => this.watermark?.setLoading(true));
36023
36412
  this.inner.on("load:end", () => this.watermark?.setLoading(false));
36024
36413
  const tz = deps.timezone();
@@ -36179,11 +36568,13 @@ var ChartCell = class {
36179
36568
  const eth = "Extended hours (ETH)";
36180
36569
  const sessionSection = {
36181
36570
  title: "Trading session",
36571
+ id: "trading-session",
36182
36572
  placement: "symbol",
36183
36573
  rows: [
36184
36574
  {
36185
36575
  kind: "select",
36186
36576
  label: "Session",
36577
+ id: "session",
36187
36578
  options: [rth, eth],
36188
36579
  get: () => this.session === "extended" ? eth : rth,
36189
36580
  set: (v) => this.setSession(v === eth ? "extended" : "regular")
@@ -36191,12 +36582,14 @@ var ChartCell = class {
36191
36582
  {
36192
36583
  kind: "color",
36193
36584
  label: "Pre-market",
36585
+ id: "premarket-color",
36194
36586
  get: () => this.sessionShadeColor("premarketColor"),
36195
36587
  set: (v) => this.setSessionShadeColor("premarketColor", v)
36196
36588
  },
36197
36589
  {
36198
36590
  kind: "color",
36199
36591
  label: "Post-market",
36592
+ id: "postmarket-color",
36200
36593
  get: () => this.sessionShadeColor("postmarketColor"),
36201
36594
  set: (v) => this.setSessionShadeColor("postmarketColor", v)
36202
36595
  }
@@ -36204,11 +36597,13 @@ var ChartCell = class {
36204
36597
  };
36205
36598
  const advanced = {
36206
36599
  title: "Advanced",
36600
+ id: "advanced",
36207
36601
  placement: "end",
36208
36602
  rows: [
36209
36603
  {
36210
36604
  kind: "select",
36211
36605
  label: "Bars to fetch",
36606
+ id: "bars",
36212
36607
  options: ["500", "1000", "2000", "5000", "10000", "20000"],
36213
36608
  get: () => String(this.state.bars ?? 1e3),
36214
36609
  set: (v) => {
@@ -36221,11 +36616,13 @@ var ChartCell = class {
36221
36616
  };
36222
36617
  const watermarkSection = {
36223
36618
  title: "Watermark",
36619
+ id: "watermark",
36224
36620
  placement: "symbol",
36225
36621
  rows: [
36226
36622
  {
36227
36623
  kind: "toggle",
36228
36624
  label: "Symbol watermark",
36625
+ id: "visible",
36229
36626
  get: () => this.watermarkOn,
36230
36627
  set: (v) => this.setWatermarkVisible(v)
36231
36628
  }
@@ -36236,22 +36633,25 @@ var ChartCell = class {
36236
36633
  const sl = this.statusline;
36237
36634
  sections.push({
36238
36635
  title: "Status line",
36636
+ id: "status-line",
36239
36637
  rows: [
36240
- { kind: "heading", label: "Status line" },
36241
- { kind: "toggle", label: "Symbol name", get: () => sl.partVisible("name"), set: (v) => sl.setPartVisible("name", v) },
36242
- { kind: "toggle", label: "Market status", get: () => sl.partVisible("market"), set: (v) => sl.setPartVisible("market", v) },
36243
- { kind: "toggle", label: "OHLC values", get: () => sl.partVisible("ohlc"), set: (v) => sl.setPartVisible("ohlc", v) },
36244
- { kind: "toggle", label: "Bar change values", get: () => sl.partVisible("change"), set: (v) => sl.setPartVisible("change", v) },
36245
- { 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" },
36246
36644
  {
36247
36645
  kind: "toggle",
36248
36646
  label: "Titles",
36647
+ id: "indicator-titles",
36249
36648
  get: () => this.indicatorTitlesOn,
36250
36649
  set: (v) => this.setIndicatorTitlesVisible(v)
36251
36650
  },
36252
36651
  {
36253
36652
  kind: "toggle",
36254
36653
  label: "Values",
36654
+ id: "indicator-values",
36255
36655
  get: () => this.indicatorValuesOn,
36256
36656
  set: (v) => this.setIndicatorValuesVisible(v)
36257
36657
  }
@@ -36304,6 +36704,7 @@ var ChartCell = class {
36304
36704
  /** Switch this cell's market in place (the chart instance survives). */
36305
36705
  setSymbol(symbol) {
36306
36706
  if (!this.inner || symbol === this.symbol) return;
36707
+ this.unresolvedToasted = null;
36307
36708
  void this.inner.setMarket({ symbol });
36308
36709
  }
36309
36710
  setTimeframe(timeframe) {
@@ -36394,6 +36795,41 @@ var ChartCell = class {
36394
36795
  for (const entry of list) if (entry.enabled) this.addManifestInstance(entry, { record: false });
36395
36796
  }
36396
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
+ }
36397
36833
  /** The picker's library rows: supported natives first, then the manifest. */
36398
36834
  libraryRows() {
36399
36835
  return [
@@ -36423,10 +36859,20 @@ var ChartCell = class {
36423
36859
  if (index < present.length) this.removeNative(present[index].type);
36424
36860
  else this.removeInstance(index - present.length);
36425
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
+ }
36426
36872
  /** Add ONE instance of a manifest entry (repeatable — duplicates are legitimate). */
36427
36873
  addManifestInstance(entry, opts = {}) {
36428
36874
  if (this.destroyed) return;
36429
- const it = { entry, handle: this.addToChart(entry) };
36875
+ const it = { entry, handle: this.addToChart(entry), ...opts.external ? { external: true } : {} };
36430
36876
  this.instances.push(it);
36431
36877
  this.deps.onIndicatorsChanged(this.id);
36432
36878
  if (opts.record === false) return;
@@ -36507,12 +36953,97 @@ var ChartCell = class {
36507
36953
  return null;
36508
36954
  }
36509
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
+ }
36510
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
+ }
36511
37041
  /** Snapshot everything the pool needs to restore this slot later. The market fields
36512
37042
  * come from the LIVE config (`chart.market`) — the requested identity — so a switch
36513
37043
  * still loading when the snapshot is taken (persist-on-close) is not lost. */
36514
37044
  dehydrate() {
36515
37045
  const live = this.inner?.market;
37046
+ const ext = this.inner ? this.dehydrateExt() : Object.keys(this.extState).length > 0 ? { ...this.extState } : void 0;
36516
37047
  return {
36517
37048
  ...this.state,
36518
37049
  ...live ? { symbol: live.symbol, provider: live.provider, timeframe: live.timeframe } : {},
@@ -36525,14 +37056,17 @@ var ChartCell = class {
36525
37056
  // Natives from the chart's SYNC registry read — an async catalog mirror here
36526
37057
  // lost unload-time saves, and the old empty-set fallbacks resurrected removed
36527
37058
  // indicators. Manifest names fall back to the restored ledger only until the
36528
- // 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.
36529
37062
  indicators: indicatorLedger({
36530
37063
  present: this.inner ? this.inner.presentNativeIndicators() : [],
36531
- instanceNames: this.instances.map((it) => it.entry.name),
37064
+ instanceNames: this.instances.filter((it) => !it.external).map((it) => it.entry.name),
36532
37065
  pendingManifest: this.pendingManifestNames,
36533
37066
  manifestSettled: this.deps.manifestSettled(),
36534
37067
  volumePending: this.volumeMayBePending && this.volumeIntent
36535
- })
37068
+ }),
37069
+ ...ext ? { ext } : {}
36536
37070
  };
36537
37071
  }
36538
37072
  destroy() {
@@ -36569,6 +37103,9 @@ function buildContext(host) {
36569
37103
  togglePanel: (id, open2) => host.togglePanel(id, open2),
36570
37104
  host: host.root,
36571
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(),
36572
37109
  cells: host.cells().map((c) => ({ id: c.id, chart: c.chart, symbol: c.symbol, timeframe: c.timeframe })),
36573
37110
  activeCellId: active?.id ?? "",
36574
37111
  setActiveCell: (id) => host.setActiveCell(id)
@@ -36930,6 +37467,10 @@ var VelaWorkspace = class {
36930
37467
  /** Plot-local y of the last price-axis long-press (targets the pane under the finger). */
36931
37468
  this.priceScalePressY = 0;
36932
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 = {};
36933
37474
  /** The single grid-wide attribution mark — re-inked on a live theme swap. */
36934
37475
  this.attributionMark = null;
36935
37476
  this.onRootKeydown = (ev) => this.routeTyping(ev);
@@ -36944,6 +37485,7 @@ var VelaWorkspace = class {
36944
37485
  state: () => ({ ...this.syncOpts })
36945
37486
  };
36946
37487
  registerBuiltinLayouts();
37488
+ registerBuiltinChartTypes();
36947
37489
  const hostEl = typeof container === "string" ? document.querySelector(container) : container;
36948
37490
  if (!hostEl) throw new Error(`VelaWorkspace: container not found: ${String(container)}`);
36949
37491
  this.opts = opts;
@@ -36967,8 +37509,12 @@ var VelaWorkspace = class {
36967
37509
  for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) {
36968
37510
  this.applySyncSetting(kind, sync?.[kind]);
36969
37511
  }
36970
- 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);
36971
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 };
36972
37518
  if (boot?.charts) for (const { id, ...cs } of boot.charts) this.pool.set(id, cs);
36973
37519
  this.order = boot?.charts ? boot.charts.map((c) => c.id) : declaredOrder(opts.cells);
36974
37520
  const bootActive = boot?.activeCellId ?? null;
@@ -37022,7 +37568,9 @@ var VelaWorkspace = class {
37022
37568
  onTimeframe: (tf) => this.setActiveTimeframe(tf),
37023
37569
  onTimeframeFavorite: (tf, on) => this.setTimeframeFavorite(tf, on),
37024
37570
  onPriceStyle: (style) => this.active.setPriceStyle(style),
37025
- 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 : {
37026
37574
  current: this.def.id,
37027
37575
  // The picker composes dynamic layouts on its grid canvas; registered
37028
37576
  // presets the canvas cannot express (bespoke plugin areas) list as rows.
@@ -37047,8 +37595,11 @@ var VelaWorkspace = class {
37047
37595
  });
37048
37596
  const main = doc.createElement("div");
37049
37597
  main.className = "vela-ws-main";
37598
+ const toolbar = buildToolbar(opts.drawings);
37599
+ this.drawingsEnabled = opts.drawings !== false;
37600
+ this.toolbarDef = toolbar.definition;
37050
37601
  let toolbarHost = null;
37051
- if (opts.drawingToolbar !== false) {
37602
+ if (this.drawingsEnabled && toolbar.visible && opts.drawingToolbar !== false) {
37052
37603
  toolbarHost = doc.createElement("div");
37053
37604
  toolbarHost.className = "vela-ws-toolbar";
37054
37605
  main.appendChild(toolbarHost);
@@ -37067,7 +37618,7 @@ var VelaWorkspace = class {
37067
37618
  this.dock.addBuiltIn({ id: "objects", title: "Object tree", icon: "objects", order: 20, panel: this.objectTree, onChart: (c) => this.objectTree.onChart(c) });
37068
37619
  this.dock.refresh();
37069
37620
  this.root.appendChild(main);
37070
- this.toast = new Toast(this.gridEl);
37621
+ this.toastHost = new Toast(this.gridEl);
37071
37622
  const attribution = rendererDefaults().attribution;
37072
37623
  if (attribution !== false) {
37073
37624
  const background = resolveTheme(opts.theme).background;
@@ -37113,7 +37664,7 @@ var VelaWorkspace = class {
37113
37664
  }
37114
37665
  }
37115
37666
  ) : null;
37116
- this.drawToolbar?.setDefinition(defaultToolbar());
37667
+ this.drawToolbar?.setDefinition(this.toolbarDef);
37117
37668
  this.drawToolbar?.setVisible(true);
37118
37669
  this.drawToolbar?.setDrawingsSyncMode(!!this.syncOpts.drawings);
37119
37670
  this.bottombar = opts.bottombar !== false ? new Bottombar(this.root, {
@@ -37135,11 +37686,11 @@ var VelaWorkspace = class {
37135
37686
  onTimeframeClick: () => this.openTimeframeDrawer(),
37136
37687
  ...picker ? { onIndicatorsClick: () => picker.open() } : {},
37137
37688
  getContext: () => this.context(),
37138
- onDrawingsClick: () => this.openDrawingsDrawer(),
37689
+ ...this.drawingsEnabled ? { onDrawingsClick: () => this.openDrawingsDrawer() } : {},
37139
37690
  onMoreClick: () => this.openMoreDrawer(),
37140
37691
  onSettingsClick: () => this.active.chart.renderer.openSettings()
37141
37692
  }) : null;
37142
- this.drawingPill = new DrawingPill(this.gridEl);
37693
+ this.drawingPill = this.drawingsEnabled ? new DrawingPill(this.gridEl) : null;
37143
37694
  hostEl.appendChild(this.root);
37144
37695
  this.layoutCtl = new LayoutModeController(this.root, opts.layoutMode ?? "auto");
37145
37696
  this.layoutCtl.onChange((mode) => this.onLayoutModeChange(mode));
@@ -37178,6 +37729,7 @@ var VelaWorkspace = class {
37178
37729
  this.manifestSettled = true;
37179
37730
  }
37180
37731
  this.mountAttachments();
37732
+ this.restoreGlobalExt();
37181
37733
  }
37182
37734
  // ── access ──────────────────────────────────────────────────
37183
37735
  /** The cell with identity `id` (its declared name, or `c<N>` when undeclared), or
@@ -37239,7 +37791,8 @@ var VelaWorkspace = class {
37239
37791
  openSymbolSearch: (query) => this.symbolPicker.open(query ?? ""),
37240
37792
  togglePanel: (id, open2) => this.dock.toggle(id, open2),
37241
37793
  root: this.root,
37242
- toast: (message, kind) => this.toast.show(message, kind)
37794
+ toast: (message, kind) => this.toastHost.show(message, kind),
37795
+ stateDirty: () => this.markStateDirty()
37243
37796
  });
37244
37797
  }
37245
37798
  /** Re-project contributed topbar actions + side panels, and mount late-registered attachments. */
@@ -37278,22 +37831,35 @@ var VelaWorkspace = class {
37278
37831
  if (this.trackSizes.size > 0) state.trackSizes = Object.fromEntries([...this.trackSizes].map(([k, v]) => [k, { ...v }]));
37279
37832
  const panels2 = this.dock.getState();
37280
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;
37281
37846
  return state;
37282
37847
  }
37283
37848
  /**
37284
37849
  * Restore a state document produced by {@link getState} (untrusted-safe: malformed
37285
- * fields are dropped). Replaces the WHOLE workspace state: prefs, sync links,
37286
- * layout, and every slot — current cells are rebuilt from the document. A layout id
37287
- * 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).
37288
37858
  */
37289
37859
  applyState(state) {
37290
37860
  if (this.destroyed) return;
37291
37861
  const st = sanitizeState(state);
37292
37862
  if (!st) return;
37293
- if (st.timezone) {
37294
- this.timezone = st.timezone;
37295
- this.bottombar?.setTimezone(st.timezone);
37296
- }
37297
37863
  if (st.favorites) this.favs = [...st.favorites];
37298
37864
  if (st.timeframeFavorites) {
37299
37865
  this.tfFavs = [...st.timeframeFavorites];
@@ -37303,6 +37869,36 @@ var VelaWorkspace = class {
37303
37869
  for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) this.applySyncSetting(kind, st.sync?.[kind]);
37304
37870
  this.trackSizes.clear();
37305
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
+ }
37306
37902
  for (const [id, cell] of [...this.cellsById]) {
37307
37903
  cell.destroy();
37308
37904
  this.cellsById.delete(id);
@@ -37312,7 +37908,7 @@ var VelaWorkspace = class {
37312
37908
  this.drawingLinks.clear();
37313
37909
  for (const { id, ...cs } of st.charts) this.pool.set(id, cs);
37314
37910
  this.order = st.charts.map((c) => c.id);
37315
- const def = ensureLayout(st.layout);
37911
+ const def = this.monoLayout ? null : ensureLayout(st.layout);
37316
37912
  if (def) this.def = def;
37317
37913
  this.cellBackend = this.backendFor(this.def);
37318
37914
  this.applyGrid();
@@ -37324,8 +37920,24 @@ var VelaWorkspace = class {
37324
37920
  else this.setActiveCell(nextActive);
37325
37921
  this.refreshRetention();
37326
37922
  this.events.emit("layout:changed", { layout: this.def.id });
37923
+ this.extState = { ...st.ext ?? {} };
37924
+ this.restoreGlobalExt();
37327
37925
  this.markStateDirty();
37328
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
+ }
37329
37941
  /** Set the workspace-global display timezone — applied to EVERY cell. */
37330
37942
  setTimezone(zone) {
37331
37943
  this.timezone = zone;
@@ -37361,6 +37973,7 @@ var VelaWorkspace = class {
37361
37973
  */
37362
37974
  setLayout(layout) {
37363
37975
  if (this.destroyed) return;
37976
+ if (this.monoLayout) return;
37364
37977
  const next = this.resolveLayout(layout);
37365
37978
  const nextBackend = this.backendFor(next);
37366
37979
  const rebuildAll = nextBackend !== this.cellBackend;
@@ -37390,6 +38003,12 @@ var VelaWorkspace = class {
37390
38003
  resize() {
37391
38004
  this.splitters.layout();
37392
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
+ }
37393
38012
  destroy() {
37394
38013
  if (this.destroyed) return;
37395
38014
  this.persistNow();
@@ -37415,7 +38034,7 @@ var VelaWorkspace = class {
37415
38034
  this.topbar.destroy();
37416
38035
  this.bottombar?.destroy();
37417
38036
  this.mobileBar?.destroy();
37418
- this.drawingPill.destroy();
38037
+ this.drawingPill?.destroy();
37419
38038
  this.tfDrawer?.destroy();
37420
38039
  this.drawingsDrawer?.destroy();
37421
38040
  this.moreDrawer?.destroy();
@@ -37429,10 +38048,10 @@ var VelaWorkspace = class {
37429
38048
  this.indicatorPicker?.destroy();
37430
38049
  this.tfQuick.destroy();
37431
38050
  this.shortcutsHelp?.destroy();
37432
- this.toast.destroy();
38051
+ this.toastHost.destroy();
37433
38052
  this.alertsMenu?.destroy();
37434
38053
  this.glider.stop();
37435
- sharedBarStore.retain(/* @__PURE__ */ new Set());
38054
+ sharedBarStore.retain(/* @__PURE__ */ new Set(), this);
37436
38055
  this.root.remove();
37437
38056
  this.events.clear();
37438
38057
  }
@@ -37450,7 +38069,7 @@ var VelaWorkspace = class {
37450
38069
  this.mobileBar?.renderActions();
37451
38070
  this.mobileBar?.setSymbol(cell.symbol);
37452
38071
  this.mobileBar?.setTimeframe(cell.timeframe);
37453
- this.drawingPill.onChart(cell.chart);
38072
+ this.drawingPill?.onChart(cell.chart);
37454
38073
  const pushHistory = () => this.topbar.setHistoryState(cell.history.canUndo, cell.history.canRedo);
37455
38074
  this.historyUnsub?.();
37456
38075
  this.historyUnsub = cell.history.onChange(pushHistory);
@@ -37576,7 +38195,8 @@ var VelaWorkspace = class {
37576
38195
  onPriceStyleChanged: (id2) => this.onCellPriceStyleChanged(id2),
37577
38196
  onIndicatorsChanged: (id2) => this.onCellIndicatorsChanged(id2),
37578
38197
  onStateDirty: () => this.markStateDirty(),
37579
- manifestSettled: () => this.manifestSettled
38198
+ manifestSettled: () => this.manifestSettled,
38199
+ toast: (message, kind, durationMs) => this.toastHost.show(message, kind, durationMs)
37580
38200
  });
37581
38201
  cell.host.style.gridArea = perCell[slot.id]?.gridArea ?? "";
37582
38202
  this.cellsById.set(id, cell);
@@ -37585,6 +38205,7 @@ var VelaWorkspace = class {
37585
38205
  cell.chart.renderer.setLayoutMode(this.layoutCtl.current);
37586
38206
  if (this.favs.length > 0) cell.chart.drawings.setFavorites(this.favs);
37587
38207
  cell.setManifest(this.manifest, pooled?.indicators == null);
38208
+ cell.restorePersistedExt();
37588
38209
  this.events.emit("cell:created", { id });
37589
38210
  }
37590
38211
  for (const [i] of this.def.cells.entries()) {
@@ -37596,12 +38217,13 @@ var VelaWorkspace = class {
37596
38217
  * cell's whole life, so these live and die with the cell). */
37597
38218
  wireCell(cell) {
37598
38219
  const chart = cell.chart;
37599
- 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));
37600
38221
  chart.on("script:run", (run) => this.events.emit("script:run", { ...run, cell: cell.id }));
37601
38222
  chart.on("alert", (alert) => {
37602
- this.alerts.unshift({ cellId: cell.id, symbol: cell.symbol, title: alert.title ?? "Alert", message: alert.message, time: alert.time });
37603
- if (this.alerts.length > ALERT_CAP) this.alerts.pop();
37604
- 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);
37605
38227
  this.topbar.setAlertCount(this.alerts.length);
37606
38228
  });
37607
38229
  chart.on("drawing:favorites", ({ favorites }) => {
@@ -37862,6 +38484,7 @@ var VelaWorkspace = class {
37862
38484
  this.mobileBar?.setTimeframe(cell.timeframe);
37863
38485
  this.objectTree.setSymbol(cell.symbol);
37864
38486
  this.bottombar?.setSession({ session: cell.session, enabled: cell.sessionAvailable });
38487
+ this.bottombar?.setActiveRange(cell.activeRangeId);
37865
38488
  }
37866
38489
  /** Trigger ② — a cell's price style changed: the topbar button/menu only if active.
37867
38490
  * Reads the cell back (not the requested style) so the button reflects what the
@@ -37946,7 +38569,7 @@ var VelaWorkspace = class {
37946
38569
  openDrawingsDrawer() {
37947
38570
  this.drawingsDrawer ?? (this.drawingsDrawer = new DrawingsDrawer({
37948
38571
  host: this.root,
37949
- toolbar: () => defaultToolbar(),
38572
+ toolbar: () => this.toolbarDef,
37950
38573
  // the shared static toolbar's definition (see constructor)
37951
38574
  currentTool: () => this.active.chart.drawings.getTool(),
37952
38575
  isFavorite: (type) => this.active.chart.drawings.isFavorite(type),
@@ -37969,14 +38592,15 @@ var VelaWorkspace = class {
37969
38592
  onPriceStyle: (id) => this.active.setPriceStyle(id),
37970
38593
  panels: () => [...this.dock.list()],
37971
38594
  onTogglePanel: (id) => this.dock.toggle(id),
37972
- 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 })),
37973
38596
  // Left-aligned actions have their own bottom-bar stop — only the rest
37974
38597
  // lands in the drawer, or every left action would appear twice.
37975
38598
  actions: () => widgetActions("topbar", this.context()).filter((a) => a.align !== "left").map((a) => ({ label: a.label, icon: a.icon, run: () => a.run(this.context()) })),
37976
38599
  // The desktop layout dropdown's whole surface — the grid canvas, the
37977
38600
  // non-canvas presets and the sync switches — relocated into the kebab
37978
- // drawer (the topbar is hidden on mobile). Same reads as the topbar block.
37979
- 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 : {
37980
38604
  shape: () => layoutShape(this.def),
37981
38605
  presets: () => layouts().filter((l) => layoutShape(l) === null).map((l) => ({ id: l.id, label: l.label, checked: l.id === this.def.id })),
37982
38606
  onSelectGrid: (rows, cols) => this.setLayout(layoutForGrid(rows, cols)),
@@ -38019,7 +38643,7 @@ var VelaWorkspace = class {
38019
38643
  this.alertsMenu?.destroy();
38020
38644
  const items = this.alerts.length ? this.alerts.map((a, i) => ({
38021
38645
  id: String(i),
38022
- 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)
38023
38647
  })) : [{ id: "none", label: "No alerts yet", disabled: true }];
38024
38648
  this.alertsMenu = new Menu({
38025
38649
  host: this.root,
@@ -38140,7 +38764,7 @@ var VelaWorkspace = class {
38140
38764
  if (!raw) continue;
38141
38765
  symbols.add(this.feed.resolveSymbol(raw)?.ticker ?? raw);
38142
38766
  }
38143
- sharedBarStore.retain(symbols);
38767
+ sharedBarStore.retain(symbols, this);
38144
38768
  }
38145
38769
  };
38146
38770