@luxalgo/vela 0.6.5 → 0.6.7

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 (48) hide show
  1. package/README.md +16 -16
  2. package/dist/{DataProvider-BmHkcwvJ.d.ts → DataProvider-BSLlBpB9.d.ts} +11 -1
  3. package/dist/{DataProvider-DrN7ZIou.d.cts → DataProvider-BsQM2WNH.d.cts} +11 -1
  4. package/dist/{history-BmyjaVir.d.cts → bottombar-Br4rzx_R.d.ts} +158 -129
  5. package/dist/{history-CvOtsjT5.d.ts → bottombar-Div5zibZ.d.cts} +158 -129
  6. package/dist/{chunk-FQBT4NFM.js → chunk-6WFKTAT4.js} +2797 -2527
  7. package/dist/{chunk-BEJ57HP4.js → chunk-L3I2CCYO.js} +62 -1
  8. package/dist/chunk-W4EJWLEO.js +12 -0
  9. package/dist/{chunk-N6MBJHMV.js → chunk-WVP4XV5A.js} +6050 -3402
  10. package/dist/{contributions-DatoAzMx.d.ts → contributions-Hv4_cOJo.d.cts} +152 -5
  11. package/dist/{contributions-2bEmCF9S.d.cts → contributions-Z12BrWCy.d.ts} +152 -5
  12. package/dist/index.cjs +2798 -2484
  13. package/dist/index.d.cts +44 -16
  14. package/dist/index.d.ts +44 -16
  15. package/dist/index.js +2 -2
  16. package/dist/{options-YdiaaVjt.d.cts → options-BaVTMXaO.d.cts} +48 -2
  17. package/dist/{options-YdiaaVjt.d.ts → options-BaVTMXaO.d.ts} +48 -2
  18. package/dist/{plugin-Bcone6Vh.d.ts → plugin-9koBjc5H.d.ts} +6 -4
  19. package/dist/{plugin-DO_DX0gp.d.cts → plugin-BXhDHTYn.d.cts} +6 -4
  20. package/dist/plugin.cjs +44 -0
  21. package/dist/plugin.d.cts +4 -4
  22. package/dist/plugin.d.ts +4 -4
  23. package/dist/plugin.js +1 -1
  24. package/dist/providers/binance.cjs +16 -0
  25. package/dist/providers/binance.d.cts +5 -2
  26. package/dist/providers/binance.d.ts +5 -2
  27. package/dist/providers/binance.js +7 -0
  28. package/dist/providers/coinbase.cjs +16 -0
  29. package/dist/providers/coinbase.d.cts +5 -2
  30. package/dist/providers/coinbase.d.ts +5 -2
  31. package/dist/providers/coinbase.js +7 -0
  32. package/dist/providers/hyperliquid.cjs +16 -0
  33. package/dist/providers/hyperliquid.d.cts +5 -2
  34. package/dist/providers/hyperliquid.d.ts +5 -2
  35. package/dist/providers/hyperliquid.js +7 -0
  36. package/dist/ui.d.cts +1 -1
  37. package/dist/ui.d.ts +1 -1
  38. package/dist/vela.global.js +2824 -2484
  39. package/dist/vela.global.min.js +48 -48
  40. package/dist/widget.cjs +35791 -34070
  41. package/dist/widget.d.cts +80 -275
  42. package/dist/widget.d.ts +80 -275
  43. package/dist/widget.js +61 -1313
  44. package/dist/workspace.cjs +1700 -995
  45. package/dist/workspace.d.cts +120 -15
  46. package/dist/workspace.d.ts +120 -15
  47. package/dist/workspace.js +7 -2281
  48. package/package.json +1 -1
@@ -303,6 +303,28 @@ var ProviderRegistry = class {
303
303
  const d = this.entries.get(resolved.provider)?.byTicker?.get(normTicker(resolved.ticker));
304
304
  return `${d?.prefix ?? resolved.provider}:${d?.ticker ?? resolved.ticker}`;
305
305
  }
306
+ /** The icon URL for a DESCRIPTOR — routed to its OWNING provider's resolver (the
307
+ * provider owns the knowledge of where its asset class's icons live). A missing
308
+ * provider, an absent resolver, or a resolver that throws all mean "no icon" —
309
+ * the shells' initials badge takes over, never an error. */
310
+ symbolIconOf(d) {
311
+ const name = d.provider ? normName(d.provider) : [...this.entries.keys()][0];
312
+ const entry = name !== void 0 ? this.entries.get(name) : void 0;
313
+ try {
314
+ return entry?.provider.resolveSymbolIcon?.(d) ?? void 0;
315
+ } catch {
316
+ return void 0;
317
+ }
318
+ }
319
+ /** The icon URL for a raw SYMBOL string (the statusline/object-tree path): resolve
320
+ * to the owning provider, find its descriptor, route to the resolver. Before the
321
+ * index settles (or for a lenient sole-provider resolution) a minimal synthetic
322
+ * descriptor is offered — crypto resolvers can answer from the ticker alone. */
323
+ symbolIcon(resolved) {
324
+ if (!resolved) return void 0;
325
+ const d = this.entries.get(resolved.provider)?.byTicker?.get(normTicker(resolved.ticker));
326
+ return this.symbolIconOf({ ...d ?? { ticker: resolved.ticker }, provider: d?.provider ?? resolved.provider });
327
+ }
306
328
  /** Indexed symbols for one provider (or all, concatenated) — for autocomplete. */
307
329
  symbolsOf(rawName) {
308
330
  if (rawName != null) return this.entries.get(normName(rawName))?.descriptors ?? [];
@@ -337,8 +359,12 @@ var BarStore = class {
337
359
  this.series = /* @__PURE__ */ new Map();
338
360
  /** Earliest bar-open time fetched for a series — what the cache actually covers. */
339
361
  this.coveredFrom = /* @__PURE__ */ new Map();
340
- /** Symbols protected from the current-symbol purge (multi-chart cells). Empty = legacy single-chart behavior. */
362
+ /** Symbols protected from the current-symbol purge (multi-chart cells) the UNION
363
+ * of every owner's declaration. Empty = legacy single-chart behavior. */
341
364
  this.retained = /* @__PURE__ */ new Set();
365
+ /** Per-owner declarations behind {@link retained} — several shells on one page must
366
+ * not clobber (or, on destroy, evict) each other's protected symbols. */
367
+ this.retainedByOwner = /* @__PURE__ */ new Map();
342
368
  }
343
369
  get(key) {
344
370
  return this.series.get(key);
@@ -382,14 +408,21 @@ var BarStore = class {
382
408
  * Declare the set of symbols a multi-chart workspace is displaying (CANONICAL
383
409
  * tickers, post-registry resolution — `chart.data.resolve(sym).ticker`). These
384
410
  * 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).
411
+ * stop evicting each other's history. Replaces the previous set FOR THAT OWNER
412
+ * (pass the shell instance as `owner`; several shells on one page keep separate
413
+ * declarations, the effective set is their union) and purges anything now outside
414
+ * the union {currentSymbol} immediately. An empty set releases the owner's
415
+ * declaration with no owners left, the legacy single-chart policy is back.
416
+ * Note: SECONDARY symbols a script fetches (`request.security` cross-symbol) are
417
+ * not in this set and still drop on cross-cell loads — correctness is unaffected
418
+ * (they re-fetch on demand).
390
419
  */
391
- retain(symbols) {
392
- this.retained = new Set(symbols);
420
+ retain(symbols, owner = "default") {
421
+ if (symbols.size === 0) this.retainedByOwner.delete(owner);
422
+ else this.retainedByOwner.set(owner, new Set(symbols));
423
+ const union = /* @__PURE__ */ new Set();
424
+ for (const set of this.retainedByOwner.values()) for (const s of set) union.add(s);
425
+ this.retained = union;
393
426
  this.purgeOutside(this.currentSymbol);
394
427
  }
395
428
  /** Drop every series whose symbol is neither `current` nor retained. */
@@ -644,6 +677,14 @@ var MultiProviderFeed = class {
644
677
  providerInstance(name) {
645
678
  return this.registry.get(name);
646
679
  }
680
+ /** The icon URL for a DESCRIPTOR — its owning provider's `resolveSymbolIcon` (picker rows). */
681
+ symbolIconOf(d) {
682
+ return this.registry.symbolIconOf(d);
683
+ }
684
+ /** The icon URL for a raw SYMBOL string — resolve, then route (statusline, object tree). */
685
+ symbolIcon(raw) {
686
+ return this.registry.symbolIcon(this.resolveSymbol(raw));
687
+ }
647
688
  symbols(name) {
648
689
  return this.registry.symbolsOf(name);
649
690
  }
@@ -4543,6 +4584,29 @@ function tickerModifierIds() {
4543
4584
  return [...registry2.values()].filter((d) => d.tickerModifier ?? d.barTransform != null).map((d) => d.id);
4544
4585
  }
4545
4586
 
4587
+ // src/widget/topbar-composition.ts
4588
+ var TOPBAR_BUILTIN_IDS = ["symbol", "timeframes", "style", "layout", "indicators", "actions", "undo-redo", "alerts", "panels", "screenshot"];
4589
+ var TOPBAR_DEFAULT_LEFT = ["symbol", "timeframes", "style", "layout", "indicators", "actions", "undo-redo"];
4590
+ var TOPBAR_DEFAULT_RIGHT = ["actions", "alerts", "panels", "screenshot"];
4591
+ function resolveTopbarComposition(opt) {
4592
+ const seen = /* @__PURE__ */ new Set();
4593
+ const take = (list) => {
4594
+ const out = [];
4595
+ const local = /* @__PURE__ */ new Set();
4596
+ for (const id of list) {
4597
+ if (!id || local.has(id) || id !== "actions" && seen.has(id)) continue;
4598
+ local.add(id);
4599
+ seen.add(id);
4600
+ out.push(id);
4601
+ }
4602
+ return out;
4603
+ };
4604
+ return { left: take(opt?.left ?? TOPBAR_DEFAULT_LEFT), right: take(opt?.right ?? TOPBAR_DEFAULT_RIGHT) };
4605
+ }
4606
+ function topbarHas(comp, id) {
4607
+ return comp.left.includes(id) || comp.right.includes(id);
4608
+ }
4609
+
4546
4610
  // src/widget/contributions.ts
4547
4611
  var registry3 = /* @__PURE__ */ new Map();
4548
4612
  var attachments = /* @__PURE__ */ new Map();
@@ -4554,6 +4618,13 @@ var DEFAULT_PANEL_ORDER = 100;
4554
4618
  function sidePanels() {
4555
4619
  return [...panels.values()].sort((a, b) => (a.order ?? DEFAULT_PANEL_ORDER) - (b.order ?? DEFAULT_PANEL_ORDER));
4556
4620
  }
4621
+ var OVERRIDABLE_TOPBAR_IDS = ["indicators", "screenshot"];
4622
+ var overridableSet = new Set(OVERRIDABLE_TOPBAR_IDS);
4623
+ new Set(TOPBAR_BUILTIN_IDS);
4624
+ function topbarActionOverride(id) {
4625
+ if (!overridableSet.has(id)) return void 0;
4626
+ return registry3.get(id);
4627
+ }
4557
4628
  function widgetActions(target, ctx) {
4558
4629
  const list = [...registry3.values()].filter((d) => d.target === target);
4559
4630
  list.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
@@ -4571,6 +4642,10 @@ function legendActionsProviderFor(chart, context) {
4571
4642
  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
4643
  };
4573
4644
  }
4645
+ var stateHandlers = /* @__PURE__ */ new Map();
4646
+ function statePersistenceHandlers(scope) {
4647
+ return [...stateHandlers.values()].filter((h) => h.scope === scope);
4648
+ }
4574
4649
  var defaultEngines = /* @__PURE__ */ new Map();
4575
4650
  function resolveEngines(overrides) {
4576
4651
  return { ...Object.fromEntries(defaultEngines), ...overrides };
@@ -5053,9 +5128,14 @@ var CSS3 = `
5053
5128
  align-items: center;
5054
5129
  justify-content: center;
5055
5130
  }
5056
- .vela-widget-actions { margin-left: auto; display: inline-flex; gap: var(--vela-space-1); }
5131
+ /* The right side of the bar \u2014 whatever the composition puts there rides this one
5132
+ auto-margin push (the flow-actions host used to carry it; composition can omit it). */
5133
+ .vela-topbar-right { margin-left: auto; display: inline-flex; align-items: center; gap: var(--vela-space-1); }
5134
+ .vela-widget-actions { display: inline-flex; gap: var(--vela-space-1); }
5057
5135
  /* Left-aligned contributed actions \u2014 the primary-chrome cluster after the dropdowns. */
5058
5136
  .vela-widget-actions-left { display: inline-flex; align-items: center; gap: var(--vela-space-1); }
5137
+ /* One PINNED contributed action's slot (a composition entry naming the action's id). */
5138
+ .vela-widget-action-pin { display: inline-flex; align-items: center; }
5059
5139
  /* The side-panel toggles, one per docked panel \u2014 a group so the dock can rebuild them
5060
5140
  without disturbing the tools around it. */
5061
5141
  .vela-widget-panels { display: inline-flex; align-items: center; gap: var(--vela-space-1); }
@@ -5114,6 +5194,12 @@ var Topbar = class {
5114
5194
  this.tooltips = [];
5115
5195
  this.panelBtns = /* @__PURE__ */ new Map();
5116
5196
  this.panelTooltips = [];
5197
+ /** Pinned contributed-action slots, by action id (composition entries that name one,
5198
+ * plus built-in slots taken over by an override). */
5199
+ this.pinned = /* @__PURE__ */ new Map();
5200
+ /** Overrides that LEFT their native slot (default side + a declared `order`) — they
5201
+ * render through the flow cluster like ordinary actions. */
5202
+ this.flowingOverrides = /* @__PURE__ */ new Set();
5117
5203
  this.onHairlineSync = () => {
5118
5204
  if (this.hairlineRaf) return;
5119
5205
  const win = this.el.ownerDocument.defaultView;
@@ -5128,6 +5214,8 @@ var Topbar = class {
5128
5214
  this.host = host;
5129
5215
  this.timeframe = opts.timeframe;
5130
5216
  this.priceStyle = opts.priceStyle;
5217
+ this.comp = resolveTopbarComposition(opts.composition);
5218
+ const vis = (id) => topbarHas(this.comp, id);
5131
5219
  const doc = host.ownerDocument;
5132
5220
  injectStyles(STYLE_ID2, CSS3, doc);
5133
5221
  this.el = doc.createElement("div");
@@ -5150,7 +5238,7 @@ var Topbar = class {
5150
5238
  this.styleButton.className = "vela-widget-style";
5151
5239
  this.renderStyleButton(doc);
5152
5240
  let indicatorsBtn = null;
5153
- if (opts.onIndicatorsClick) {
5241
+ if (opts.onIndicatorsClick && !topbarActionOverride("indicators")) {
5154
5242
  indicatorsBtn = doc.createElement("button");
5155
5243
  indicatorsBtn.className = "vela-widget-indicators";
5156
5244
  indicatorsBtn.append(iconEl("indicators", doc), doc.createTextNode("Indicators"));
@@ -5163,18 +5251,23 @@ var Topbar = class {
5163
5251
  this.panelsHost = doc.createElement("span");
5164
5252
  this.panelsHost.className = "vela-widget-panels";
5165
5253
  const tool = (cls, icon2, tip, onClick) => this.toolButton(cls, icon2, tip, onClick, this.tooltips);
5166
- this.undoBtn = tool("vela-widget-undo", "undo", "Undo", opts.onUndoClick);
5167
- this.redoBtn = tool("vela-widget-redo", "redo", "Redo", opts.onRedoClick);
5254
+ if (vis("undo-redo")) {
5255
+ this.undoBtn = tool("vela-widget-undo", "undo", "Undo", opts.onUndoClick);
5256
+ this.redoBtn = tool("vela-widget-redo", "redo", "Redo", opts.onRedoClick);
5257
+ } else {
5258
+ this.undoBtn = doc.createElement("button");
5259
+ this.redoBtn = doc.createElement("button");
5260
+ }
5168
5261
  this.setHistoryState(false, false);
5169
- const screenshotBtn = tool("vela-widget-screenshot", "camera", "Download screenshot", opts.onScreenshotClick);
5170
- this.alertsBtn = tool("vela-widget-alerts", "bell", "Alerts");
5262
+ const screenshotBtn = vis("screenshot") && !topbarActionOverride("screenshot") ? tool("vela-widget-screenshot", "camera", "Download screenshot", opts.onScreenshotClick) : null;
5263
+ this.alertsBtn = vis("alerts") ? tool("vela-widget-alerts", "bell", "Alerts") : doc.createElement("button");
5171
5264
  this.alertsBtn.style.position = "relative";
5172
5265
  if (opts.onAlertsClick) this.alertsBtn.addEventListener("click", () => opts.onAlertsClick(this.alertsBtn));
5173
5266
  this.alertsBadge = doc.createElement("span");
5174
5267
  this.alertsBadge.className = "vela-alerts-badge";
5175
5268
  this.alertsBadge.style.display = "none";
5176
5269
  this.alertsBtn.appendChild(this.alertsBadge);
5177
- if (opts.layout) {
5270
+ if (opts.layout && vis("layout")) {
5178
5271
  this.layoutId = opts.layout.current;
5179
5272
  this.layoutButton = doc.createElement("button");
5180
5273
  this.layoutButton.className = "vela-widget-style";
@@ -5185,12 +5278,65 @@ var Topbar = class {
5185
5278
  d.className = "vela-sep";
5186
5279
  return d;
5187
5280
  };
5188
- const leading = [this.symbolEl, sep(), tfGroup, sep(), this.styleButton, sep()];
5189
- if (this.layoutButton) leading.push(this.layoutButton, sep());
5190
- if (indicatorsBtn) leading.push(indicatorsBtn, sep());
5191
5281
  this.leftActionsSep = sep();
5192
5282
  this.leftActionsSep.hidden = true;
5193
- this.el.append(...leading, this.leftActionsHost, this.leftActionsSep, this.undoBtn, this.redoBtn, this.actionsHost, this.alertsBtn, this.panelsHost, screenshotBtn);
5283
+ const primaries = /* @__PURE__ */ new Set(["symbol", "timeframes", "style", "layout", "indicators"]);
5284
+ const pinSlot = (id, left) => {
5285
+ const slot = doc.createElement("span");
5286
+ slot.className = "vela-widget-action-pin";
5287
+ this.pinned.set(id, { host: slot, left });
5288
+ return [slot];
5289
+ };
5290
+ const overridden = (id, left) => {
5291
+ const ov = topbarActionOverride(id);
5292
+ if (!ov) return null;
5293
+ const sideDeclared = left ? opts.composition?.left != null : opts.composition?.right != null;
5294
+ if (!sideDeclared && ov.order !== void 0) {
5295
+ this.flowingOverrides.add(id);
5296
+ return [];
5297
+ }
5298
+ return pinSlot(id, left);
5299
+ };
5300
+ const elementsFor = (id, left) => {
5301
+ switch (id) {
5302
+ case "symbol":
5303
+ return [this.symbolEl];
5304
+ case "timeframes":
5305
+ return [tfGroup];
5306
+ case "style":
5307
+ return [this.styleButton];
5308
+ case "layout":
5309
+ return this.layoutButton ? [this.layoutButton] : [];
5310
+ case "indicators":
5311
+ return overridden(id, left) ?? (indicatorsBtn ? [indicatorsBtn] : []);
5312
+ case "actions":
5313
+ return left ? [this.leftActionsHost, this.leftActionsSep] : [this.actionsHost];
5314
+ case "undo-redo":
5315
+ return [this.undoBtn, this.redoBtn];
5316
+ case "alerts":
5317
+ return [this.alertsBtn];
5318
+ case "panels":
5319
+ return [this.panelsHost];
5320
+ case "screenshot":
5321
+ return overridden(id, left) ?? (screenshotBtn ? [screenshotBtn] : []);
5322
+ default:
5323
+ return pinSlot(id, left);
5324
+ }
5325
+ };
5326
+ const sideEls = (list, left) => {
5327
+ const out = [];
5328
+ for (const [i, id] of list.entries()) {
5329
+ const els = elementsFor(id, left);
5330
+ if (els.length === 0) continue;
5331
+ out.push(...els);
5332
+ if (primaries.has(id) && i < list.length - 1) out.push(sep());
5333
+ }
5334
+ return out;
5335
+ };
5336
+ const right = doc.createElement("span");
5337
+ right.className = "vela-topbar-right";
5338
+ right.append(...sideEls(this.comp.right, false));
5339
+ this.el.append(...sideEls(this.comp.left, true), right);
5194
5340
  host.appendChild(this.el);
5195
5341
  this.renderTfChips();
5196
5342
  this.onHairlineSync();
@@ -5307,14 +5453,24 @@ var Topbar = class {
5307
5453
  else this.styleButton.appendChild(doc.createTextNode(priceStyleLabel(this.priceStyle)));
5308
5454
  this.styleButton.setAttribute("aria-label", `Chart style \u2014 ${priceStyleLabel(this.priceStyle)}`);
5309
5455
  }
5310
- /** Re-project the contributed topbar actions (call after registrations change). */
5456
+ /** Re-project the contributed topbar actions (call after registrations change).
5457
+ * An action PINNED by the composition renders into its named slot (list position
5458
+ * wins over `align`/`order`); the rest flow into the side's `actions` slot — or
5459
+ * not at all when an explicit list omits it (the list is the side's contract). */
5311
5460
  renderActions() {
5312
5461
  const ctx = this.opts.getContext?.();
5313
5462
  this.actionsHost.replaceChildren();
5314
5463
  this.leftActionsHost.replaceChildren();
5464
+ for (const pin of this.pinned.values()) pin.host.replaceChildren();
5315
5465
  const doc = this.actionsHost.ownerDocument;
5466
+ const flowLeft = this.comp.left.includes("actions");
5467
+ const flowRight = this.comp.right.includes("actions");
5468
+ const builtin = new Set(TOPBAR_BUILTIN_IDS);
5316
5469
  for (const action of widgetActions("topbar", ctx)) {
5317
- const left = action.align === "left";
5470
+ const pin = this.pinned.get(action.id);
5471
+ if (!pin && builtin.has(action.id) && !this.flowingOverrides.has(action.id)) continue;
5472
+ const left = pin ? pin.left : action.align === "left";
5473
+ if (!pin && !(left ? flowLeft : flowRight)) continue;
5318
5474
  const b = doc.createElement("button");
5319
5475
  b.className = left ? "vela-widget-action-left" : "vela-widget-action";
5320
5476
  if (action.icon) b.appendChild(iconEl(action.icon, doc));
@@ -5323,7 +5479,7 @@ var Topbar = class {
5323
5479
  const c = this.opts.getContext?.();
5324
5480
  if (c) action.run(c);
5325
5481
  });
5326
- (left ? this.leftActionsHost : this.actionsHost).appendChild(b);
5482
+ (pin ? pin.host : left ? this.leftActionsHost : this.actionsHost).appendChild(b);
5327
5483
  }
5328
5484
  this.leftActionsSep.hidden = this.leftActionsHost.childElementCount === 0;
5329
5485
  this.onHairlineSync();
@@ -5716,41 +5872,39 @@ var Bottombar = class {
5716
5872
  }
5717
5873
  };
5718
5874
 
5719
- // src/widget/symbol-icon.ts
5720
- var iconFailed = /* @__PURE__ */ new Set();
5721
- function initialsOf(name) {
5722
- return (name || "?").replace(/[^A-Za-z0-9]/g, "").slice(0, 2).toUpperCase() || "?";
5723
- }
5724
- function cryptoIconUrl(base) {
5725
- return `https://crypto-icons.ledger.com/${encodeURIComponent(base.toUpperCase())}.png`;
5726
- }
5875
+ // src/data/symbol-base.ts
5727
5876
  function baseOf(d) {
5728
5877
  const fromDesc = d.description?.split("/")[0]?.trim();
5729
5878
  if (fromDesc) return fromDesc.replace(/\s+Perpetual$/i, "");
5730
5879
  return d.ticker.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || d.ticker;
5731
5880
  }
5732
- function tickerIconEl(doc, base, name, className) {
5881
+
5882
+ // src/widget/symbol-icon.ts
5883
+ var iconFailed = /* @__PURE__ */ new Set();
5884
+ function initialsOf(name) {
5885
+ return (name || "?").replace(/[^A-Za-z0-9]/g, "").slice(0, 2).toUpperCase() || "?";
5886
+ }
5887
+ function tickerIconEl(doc, base, name, className, iconUrl) {
5733
5888
  const wrap = doc.createElement("span");
5734
5889
  wrap.className = className;
5735
- const key = base.toUpperCase();
5736
5890
  const fallback = () => {
5737
5891
  wrap.replaceChildren();
5738
5892
  wrap.style.background = categoricalColor(name);
5739
5893
  wrap.textContent = initialsOf(base || name);
5740
5894
  };
5741
- if (!key || iconFailed.has(key)) {
5895
+ if (!iconUrl || iconFailed.has(iconUrl)) {
5742
5896
  fallback();
5743
5897
  return wrap;
5744
5898
  }
5745
5899
  const img = doc.createElement("img");
5746
5900
  img.alt = "";
5747
5901
  img.crossOrigin = "anonymous";
5748
- img.src = cryptoIconUrl(key);
5902
+ img.src = iconUrl;
5749
5903
  img.style.cssText = "width:100%;height:100%;border-radius:50%;display:block;object-fit:cover;";
5750
5904
  img.addEventListener(
5751
5905
  "error",
5752
5906
  () => {
5753
- iconFailed.add(key);
5907
+ iconFailed.add(iconUrl);
5754
5908
  fallback();
5755
5909
  },
5756
5910
  { once: true }
@@ -12003,11 +12157,14 @@ var MenuBuilder = class {
12003
12157
  }
12004
12158
  };
12005
12159
  var ObjectTree = class extends SidePanel {
12006
- constructor(host) {
12160
+ constructor(host, iconFor) {
12007
12161
  super(host, "Object tree", "vela-ot");
12162
+ this.iconFor = iconFor;
12008
12163
  this.chart = null;
12009
12164
  this.selectedDrawing = null;
12010
12165
  this.symbolName = "";
12166
+ /** The raw (possibly venue-prefixed) symbol — what icon resolution routes on. */
12167
+ this.symbolRaw = "";
12011
12168
  /** Drawing bundles — a view-side grouping, held for the panel's lifetime and never persisted.
12012
12169
  * Kept per chart because a workspace points this one panel at whichever chart is active, and
12013
12170
  * each chart's bundles have to survive the switch. */
@@ -12044,6 +12201,7 @@ var ObjectTree = class extends SidePanel {
12044
12201
  if (open2) this.refresh();
12045
12202
  }
12046
12203
  setSymbol(symbol) {
12204
+ this.symbolRaw = symbol;
12047
12205
  this.symbolName = parseSymbol(symbol).ticker;
12048
12206
  }
12049
12207
  /** (Re)bind to a chart instance — called after every widget rebuild. */
@@ -12244,7 +12402,7 @@ var ObjectTree = class extends SidePanel {
12244
12402
  const { chart } = pass;
12245
12403
  if (row.kind === "price") {
12246
12404
  const base = this.symbolName.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || this.symbolName;
12247
- const icon2 = tickerIconEl(doc, base || "P", this.symbolName || "Price", "vela-ot-avatar");
12405
+ const icon2 = tickerIconEl(doc, base || "P", this.symbolName || "Price", "vela-ot-avatar", this.symbolRaw ? this.iconFor?.(this.symbolRaw) : void 0);
12248
12406
  const el2 = this.row(icon2, row.label, row.visible, [
12249
12407
  {
12250
12408
  icon: row.visible ? "eye" : "eye-off",
@@ -13274,8 +13432,6 @@ var PanelDock = class {
13274
13432
  for (const entry of this.entries) this.deps.chrome.setPanelActive(entry.id, entry.panel.open);
13275
13433
  }
13276
13434
  };
13277
-
13278
- // src/widget/symbol-picker.ts
13279
13435
  var TOP_TICKERS = ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT", "DOGEUSDT", "ADAUSDT", "LINKUSDT"];
13280
13436
  function parseQuery(raw, venues) {
13281
13437
  const m = raw.match(/^\s*([^\s:]+)\s*[:\s]\s*(.*)$/);
@@ -13290,17 +13446,18 @@ function onlyOne(matches) {
13290
13446
  return matches.length === 1 ? matches[0] : null;
13291
13447
  }
13292
13448
  var venueOf = (s) => s.prefix ?? s.provider;
13293
- function filterSymbols(list, query, limit = 100) {
13449
+ function filterSymbols(list, query, limit = 100, top = TOP_TICKERS) {
13294
13450
  const venues = [...new Set(list.flatMap((s) => [venueOf(s)?.toLowerCase(), s.provider?.toLowerCase()]).filter((p) => !!p))];
13295
13451
  const { scope, term } = parseQuery(query, venues);
13296
13452
  const pool = scope ? list.filter((s) => venueOf(s)?.toLowerCase() === scope || s.provider?.toLowerCase() === scope) : list;
13297
13453
  const q = term.toUpperCase();
13298
13454
  if (!q) {
13299
13455
  if (scope) return [...pool].sort((a, b) => a.ticker.localeCompare(b.ticker)).slice(0, limit);
13456
+ if (top === false) return pool.slice(0, limit);
13300
13457
  const byTicker = new Map(pool.map((s) => [s.ticker.toUpperCase(), s]));
13301
- const top = TOP_TICKERS.map((t) => byTicker.get(t)).filter((s) => s !== void 0);
13302
- const rest = pool.filter((s) => !TOP_TICKERS.includes(s.ticker.toUpperCase()));
13303
- return [...top, ...rest].slice(0, limit);
13458
+ const pinned = top.map((t) => byTicker.get(t)).filter((s) => s !== void 0);
13459
+ const rest = pool.filter((s) => !top.includes(s.ticker.toUpperCase()));
13460
+ return [...pinned, ...rest].slice(0, limit);
13304
13461
  }
13305
13462
  const qLower = term.toLowerCase();
13306
13463
  const prefix = [];
@@ -13410,12 +13567,16 @@ var CSS8 = `
13410
13567
  var PAGE = 100;
13411
13568
  var SymbolPicker = class {
13412
13569
  constructor(opts) {
13570
+ this.opts = opts;
13413
13571
  this.source = () => [];
13414
13572
  this.rows = [];
13415
13573
  this.highlighted = 0;
13416
13574
  this.seed = "";
13417
13575
  this.activeTab = "All";
13418
13576
  this.visible = PAGE;
13577
+ /** The ranked pool cache — `key` fingerprints the raw pool the ranking ran on. */
13578
+ this.ranked = null;
13579
+ this.ranking = false;
13419
13580
  const doc = (opts.host ?? document.body).ownerDocument;
13420
13581
  injectStyles(STYLE_ID7, CSS8, doc);
13421
13582
  this.input = doc.createElement("input");
@@ -13513,10 +13674,19 @@ var SymbolPicker = class {
13513
13674
  } else delete el.dataset.highlighted;
13514
13675
  });
13515
13676
  }
13677
+ /** The picker's pool: the source, shaped by the registered symbol ranking. Cached —
13678
+ * the hook runs when the pool CHANGES (an index lands or refreshes), never per
13679
+ * keystroke; an async hook resolves onto the next repaint (stale-while-revalidate
13680
+ * in between, the raw pool before the first resolve). */
13681
+ pool() {
13682
+ const raw = this.source();
13683
+ return raw;
13684
+ }
13516
13685
  computeRows() {
13517
13686
  const TAB_TYPES = { Crypto: ["crypto"], Stocks: ["stock"], ETFs: ["etf"], Forex: ["forex"], Commodities: ["commodity"] };
13518
- 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");
13519
- return filterSymbols(pool, this.input.value, this.visible);
13687
+ const all = this.pool();
13688
+ const pool = this.activeTab === "All" ? all : all.filter((s) => TAB_TYPES[this.activeTab]?.includes((s.type ?? "").toLowerCase()) || this.activeTab === "Crypto" && (s.type ?? "").toLowerCase() === "futures");
13689
+ return filterSymbols(pool, this.input.value, this.visible, TOP_TICKERS);
13520
13690
  }
13521
13691
  refresh() {
13522
13692
  const doc = this.list.ownerDocument;
@@ -13547,7 +13717,7 @@ var SymbolPicker = class {
13547
13717
  row.dataset.ticker = s.ticker;
13548
13718
  const venue = s.prefix ?? s.provider;
13549
13719
  if (venue) row.dataset.venue = venue;
13550
- const av = tickerIconEl(doc, baseOf(s), s.ticker, "vela-sp-avatar");
13720
+ const av = tickerIconEl(doc, baseOf(s), s.ticker, "vela-sp-avatar", this.opts.iconFor?.(s));
13551
13721
  const main = doc.createElement("span");
13552
13722
  main.className = "vela-sp-main";
13553
13723
  const t = doc.createElement("span");
@@ -14212,10 +14382,11 @@ var MobileBar = class {
14212
14382
  const indicators = onIndicators ? item("vela-mb-indicators", "Indicators", onIndicators, "indicators") : null;
14213
14383
  this.actionsHost = doc.createElement("span");
14214
14384
  this.actionsHost.className = "vela-mb-actions";
14215
- const drawings = item("vela-mb-drawings", "Drawings", opts.onDrawingsClick, "pen");
14385
+ const onDrawings = opts.onDrawingsClick;
14386
+ const drawings = onDrawings ? item("vela-mb-drawings", "Drawings", onDrawings, "pen") : null;
14216
14387
  const more = item("vela-mb-more", "More", opts.onMoreClick, "kebab");
14217
14388
  const settings = item("vela-mb-settings", "Chart settings", opts.onSettingsClick, "gear");
14218
- this.el.append(this.symbolEl, this.tfEl, ...indicators ? [indicators] : [], this.actionsHost, drawings, more, settings);
14389
+ this.el.append(this.symbolEl, this.tfEl, ...indicators ? [indicators] : [], this.actionsHost, ...drawings ? [drawings] : [], more, settings);
14219
14390
  host.appendChild(this.el);
14220
14391
  this.renderActions();
14221
14392
  }
@@ -14227,7 +14398,8 @@ var MobileBar = class {
14227
14398
  if (!ctx) return;
14228
14399
  const doc = this.el.ownerDocument;
14229
14400
  this.actionsHost.replaceChildren();
14230
- for (const action of widgetActions("topbar", ctx).filter((a) => a.align === "left")) {
14401
+ const builtin = new Set(TOPBAR_BUILTIN_IDS);
14402
+ for (const action of widgetActions("topbar", ctx).filter((a) => a.align === "left" && !builtin.has(a.id))) {
14231
14403
  const b = doc.createElement("button");
14232
14404
  b.className = "vela-mb-item";
14233
14405
  b.setAttribute("aria-label", action.label);
@@ -14756,10 +14928,10 @@ var MoreDrawer = class {
14756
14928
  });
14757
14929
  actions.appendChild(b);
14758
14930
  };
14759
- action("undo", "Undo", this.opts.canUndo(), this.opts.onUndo);
14760
- action("redo", "Redo", this.opts.canRedo(), this.opts.onRedo);
14761
- action("camera", "Screenshot", true, this.opts.onScreenshot);
14762
- this.drawer.body.appendChild(actions);
14931
+ if (this.opts.onUndo) action("undo", "Undo", this.opts.canUndo(), this.opts.onUndo);
14932
+ if (this.opts.onRedo) action("redo", "Redo", this.opts.canRedo(), this.opts.onRedo);
14933
+ if (this.opts.onScreenshot) action("camera", "Screenshot", true, this.opts.onScreenshot);
14934
+ if (actions.childElementCount > 0) this.drawer.body.appendChild(actions);
14763
14935
  const list = doc.createElement("div");
14764
14936
  list.className = "vela-md-list";
14765
14937
  const current = this.opts.priceStyles().find((s) => s.id === this.opts.priceStyle());
@@ -14780,8 +14952,10 @@ var MoreDrawer = class {
14780
14952
  })
14781
14953
  );
14782
14954
  }
14783
- const alertCount = this.opts.alerts().length;
14784
- list.appendChild(this.row(doc, "Alerts", { icon: "bell", value: alertCount > 0 ? String(alertCount) : void 0, chevron: true, onClick: () => this.show("alerts") }));
14955
+ if (this.opts.alerts) {
14956
+ const alertCount = this.opts.alerts().length;
14957
+ list.appendChild(this.row(doc, "Alerts", { icon: "bell", value: alertCount > 0 ? String(alertCount) : void 0, chevron: true, onClick: () => this.show("alerts") }));
14958
+ }
14785
14959
  for (const act of this.opts.actions()) {
14786
14960
  list.appendChild(
14787
14961
  this.row(doc, act.label, {
@@ -14862,7 +15036,7 @@ var MoreDrawer = class {
14862
15036
  this.drawer.body.appendChild(list);
14863
15037
  }
14864
15038
  renderAlerts(doc) {
14865
- const alerts = this.opts.alerts();
15039
+ const alerts = this.opts.alerts?.() ?? [];
14866
15040
  if (alerts.length === 0) {
14867
15041
  const empty = doc.createElement("div");
14868
15042
  empty.className = "vela-md-empty";
@@ -17030,6 +17204,35 @@ function rendererDefaults() {
17030
17204
  return Object.fromEntries(defaults);
17031
17205
  }
17032
17206
 
17207
+ // src/core/price-styles/heikin-ashi.ts
17208
+ function heikinAshiNext(raw, prevHa) {
17209
+ const haClose = (raw.open + raw.high + raw.low + raw.close) / 4;
17210
+ const haOpen = prevHa ? (prevHa.open + prevHa.close) / 2 : (raw.open + raw.close) / 2;
17211
+ return {
17212
+ time: raw.time,
17213
+ open: haOpen,
17214
+ high: Math.max(raw.high, haOpen, haClose),
17215
+ low: Math.min(raw.low, haOpen, haClose),
17216
+ close: haClose,
17217
+ ...raw.volume != null ? { volume: raw.volume } : {}
17218
+ };
17219
+ }
17220
+ function heikinAshiFull(raw) {
17221
+ const out = new Array(raw.length);
17222
+ let prev2;
17223
+ for (let i = 0; i < raw.length; i += 1) {
17224
+ prev2 = heikinAshiNext(raw[i], prev2);
17225
+ out[i] = prev2;
17226
+ }
17227
+ return out;
17228
+ }
17229
+
17230
+ // src/chart-types/builtins.ts
17231
+ var HEIKIN_ASHI = { full: heikinAshiFull, next: heikinAshiNext };
17232
+ function registerBuiltinChartTypes() {
17233
+ registerChartType({ id: "heikinashi", label: "Heikin Ashi", barTransform: HEIKIN_ASHI });
17234
+ }
17235
+
17033
17236
  // src/workspace/sync.ts
17034
17237
  function syncTargets(originId, setting, cellIds) {
17035
17238
  if (setting == null || setting === false) return [];
@@ -17089,8 +17292,18 @@ function sanitizeState(doc) {
17089
17292
  if (tracks) out.trackSizes = tracks;
17090
17293
  const panels2 = sanitizePanels(d.panels);
17091
17294
  if (panels2) out.panels = panels2;
17295
+ const ext = sanitizeExt(d.ext);
17296
+ if (ext) out.ext = ext;
17092
17297
  return out;
17093
17298
  }
17299
+ function sanitizeExt(raw) {
17300
+ if (raw == null || typeof raw !== "object" || Array.isArray(raw)) return null;
17301
+ const out = {};
17302
+ for (const [key, value] of Object.entries(raw)) {
17303
+ if (key.length > 0 && value !== void 0) out[key] = value;
17304
+ }
17305
+ return Object.keys(out).length > 0 ? out : null;
17306
+ }
17094
17307
  function sanitizeCell(raw) {
17095
17308
  if (raw == null || typeof raw !== "object") return null;
17096
17309
  const c = raw;
@@ -17112,6 +17325,8 @@ function sanitizeCell(raw) {
17112
17325
  const natives = Array.isArray(ind.natives) ? ind.natives.filter((n) => typeof n === "string") : [];
17113
17326
  out.indicators = { manifest, natives };
17114
17327
  }
17328
+ const ext = sanitizeExt(c.ext);
17329
+ if (ext) out.ext = ext;
17115
17330
  return out;
17116
17331
  }
17117
17332
  function sanitizeSync(raw) {
@@ -18444,7 +18659,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
18444
18659
  this.emitScriptRun(id, cause, first);
18445
18660
  },
18446
18661
  onAlert: (a) => {
18447
- this.events.emit("alert", a);
18662
+ const indicator = record.options?.title ?? record.prepared?.meta.title ?? record.title;
18663
+ this.events.emit("alert", { ...a, indicator });
18448
18664
  handle.emit("alert", { id: a.id, message: a.message, title: a.title, time: a.time });
18449
18665
  },
18450
18666
  onWarning: (w) => this.events.emit("warning", w),
@@ -18492,6 +18708,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
18492
18708
  overlay: d.overlay,
18493
18709
  paneHint: d.paneHint,
18494
18710
  native: { type: record.native.type },
18711
+ ...out.paneAxis != null ? { paneAxis: out.paneAxis } : {},
18495
18712
  series: out.series ?? [],
18496
18713
  fills: out.fills ?? [],
18497
18714
  backgrounds: out.backgrounds ?? [],
@@ -19086,6 +19303,24 @@ var RendererControl = class {
19086
19303
  this.renderer.setSettingsSections?.(sections);
19087
19304
  return this;
19088
19305
  }
19306
+ /**
19307
+ * Set the settings-dialog visibility policy: `hidden` lists setting ids to hide —
19308
+ * a tab (`'canvas'`), a group (`'canvas.grid'`), or a single row
19309
+ * (`'canvas.grid.vertical'`); an id hides its whole subtree, and a tab with nothing
19310
+ * left disappears from the rail. Presentation-only: hidden values keep being stored
19311
+ * and applied (e.g. hide `'advanced'` while forcing the widget's `bars` option).
19312
+ * Seeded from `VelaOptions.settings`; silent no-op without a settings dialog.
19313
+ */
19314
+ setSettingsVisibility(policy) {
19315
+ this.renderer.setSettingsVisibility?.(policy);
19316
+ return this;
19317
+ }
19318
+ /** Every addressable setting id of this chart (built-in tabs/groups/rows, chart-type
19319
+ * sections, host sections) — enumerate these to build a `hidden` list instead of
19320
+ * reading contributor source. Empty on a renderer without a settings dialog. */
19321
+ listSettingsIds() {
19322
+ return this.renderer.listSettingsIds?.() ?? [];
19323
+ }
19089
19324
  /** Tell the renderer's own chrome which size class the host shell is in —
19090
19325
  * `'mobile'` switches its dialogs/toolbars to the touch-first presentation.
19091
19326
  * Silent no-op on a renderer without adaptive chrome. */
@@ -19200,6 +19435,12 @@ var DataControl = class {
19200
19435
  symbols(provider) {
19201
19436
  return this.registry?.symbols(provider) ?? [];
19202
19437
  }
19438
+ /** The icon URL for `symbol` — its owning provider's `resolveSymbolIcon`, routed
19439
+ * through resolution. Undefined while unresolvable, when the provider declares no
19440
+ * resolver, or on a custom `deps.dataFeed` — the shells then show initials. */
19441
+ symbolIcon(symbol) {
19442
+ return this.registry?.symbolIcon(symbol);
19443
+ }
19203
19444
  /** Per-symbol metadata (Pine `syminfo.*`), resolved through the owning provider. */
19204
19445
  symbolInfo(symbol) {
19205
19446
  return this.registry?.symbolInfoFor(symbol) ?? Promise.resolve(void 0);
@@ -21468,658 +21709,6 @@ function supportsWebGL2() {
21468
21709
  return webgl2Probe;
21469
21710
  }
21470
21711
 
21471
- // src/renderers/native/chrome/ticks.ts
21472
- function priceTicks(min, max, target = 6) {
21473
- if (!(max > min) || !Number.isFinite(min) || !Number.isFinite(max)) return [];
21474
- const raw = (max - min) / Math.max(1, target);
21475
- const mag = Math.pow(10, Math.floor(Math.log10(raw)));
21476
- const norm = raw / mag;
21477
- const step = (norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10) * mag;
21478
- const decimals = Math.max(0, -Math.floor(Math.log10(step)) + 1);
21479
- const out = [];
21480
- const start = Math.ceil(min / step) * step;
21481
- for (let v = start; v <= max + step * 1e-6; v += step) {
21482
- out.push(Number(v.toFixed(decimals)));
21483
- }
21484
- return out;
21485
- }
21486
- function priceDecimals(min, max, target = 6) {
21487
- if (!(max > min)) return 2;
21488
- const raw = (max - min) / Math.max(1, target);
21489
- const mag = Math.pow(10, Math.floor(Math.log10(raw)));
21490
- const norm = raw / mag;
21491
- const step = (norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10) * mag;
21492
- return Math.max(0, Math.min(8, -Math.floor(Math.log10(step)) + 1));
21493
- }
21494
- function logPriceTicks(min, max, target = 6) {
21495
- if (min <= 0 || !(max > min) || !Number.isFinite(min) || !Number.isFinite(max)) return [];
21496
- if (Math.log10(max) - Math.log10(min) < 1.1) return priceTicks(min, max, target);
21497
- const out = [];
21498
- const startExp = Math.floor(Math.log10(min));
21499
- const endExp = Math.ceil(Math.log10(max));
21500
- for (let e = startExp; e <= endExp; e += 1) {
21501
- for (const m of [1, 2, 5]) {
21502
- const v = m * Math.pow(10, e);
21503
- if (v >= min && v <= max) out.push(v);
21504
- }
21505
- }
21506
- return out;
21507
- }
21508
- function valueDecimals(v) {
21509
- const a = Math.abs(v);
21510
- if (a >= 100) return 0;
21511
- if (a >= 1) return 2;
21512
- if (a >= 0.01) return 4;
21513
- return 6;
21514
- }
21515
- function tickDecimals(tick) {
21516
- if (!(tick > 0) || !Number.isFinite(tick)) return 2;
21517
- for (let d = 0; d <= 8; d += 1) {
21518
- if (Math.abs(Number(tick.toFixed(d)) - tick) <= tick * 1e-6) return d;
21519
- }
21520
- return 8;
21521
- }
21522
- function axisDecimals(scale, heightPx, mintick) {
21523
- const d = mintick != null && mintick > 0 ? tickDecimals(mintick) : priceDecimals(scale.min, scale.max, tickCount(heightPx));
21524
- return d === 0 ? 2 : d;
21525
- }
21526
- function tickCount(paneHeightPx) {
21527
- return Math.max(2, Math.min(16, Math.round(paneHeightPx / 50)));
21528
- }
21529
- function paneTicks(scale, heightPx) {
21530
- return scale.log ? logPriceTicks(scale.min, scale.max, tickCount(heightPx)) : priceTicks(scale.min, scale.max, tickCount(heightPx));
21531
- }
21532
- function toPct(price, baseline) {
21533
- return (price / baseline - 1) * 100;
21534
- }
21535
- function toIndex(price, baseline) {
21536
- return price / baseline * 100;
21537
- }
21538
- function formatPct(pct) {
21539
- const sign = pct >= 0 ? "+" : "-";
21540
- return `${sign}${Math.abs(pct).toFixed(2)}%`;
21541
- }
21542
- function formatIndex(idx) {
21543
- return idx.toFixed(2);
21544
- }
21545
- function formatCompactValue(v) {
21546
- const a = Math.abs(v);
21547
- if (a >= 1e9) return `${trimZeros(v / 1e9)}B`;
21548
- if (a >= 1e6) return `${trimZeros(v / 1e6)}M`;
21549
- if (a >= 1e3) return `${trimZeros(v / 1e3)}K`;
21550
- return trimZeros(v);
21551
- }
21552
- function trimZeros(v) {
21553
- return Number(v.toFixed(2)).toString();
21554
- }
21555
- function paneAxisTicks(scale, heightPx, pct, mintick, format) {
21556
- if (pct) {
21557
- const { baseline, indexed } = pct;
21558
- const lo = Math.min(scale.min, scale.max);
21559
- const hi = Math.max(scale.min, scale.max);
21560
- if (indexed) {
21561
- const iLo = toIndex(lo, baseline);
21562
- const iHi = toIndex(hi, baseline);
21563
- return priceTicks(iLo, iHi, tickCount(heightPx)).map((idx) => ({ price: baseline * idx / 100, label: formatIndex(idx) }));
21564
- }
21565
- const pLo = toPct(lo, baseline);
21566
- const pHi = toPct(hi, baseline);
21567
- return priceTicks(pLo, pHi, tickCount(heightPx)).map((p) => ({ price: baseline * (1 + p / 100), label: formatPct(p) }));
21568
- }
21569
- if (format === "volume") {
21570
- return paneTicks(scale, heightPx).map((price) => ({ price, label: formatCompactValue(price) }));
21571
- }
21572
- return paneTicks(scale, heightPx).map((price) => ({ price, label: formatPriceLabel(scale, heightPx, price, mintick) }));
21573
- }
21574
- function formatAxisValue(scale, heightPx, value, pct, mintick, format) {
21575
- if (pct) return pct.indexed ? formatIndex(toIndex(value, pct.baseline)) : formatPct(toPct(value, pct.baseline));
21576
- if (format === "volume") return formatCompactValue(value);
21577
- return formatPriceLabel(scale, heightPx, value, mintick);
21578
- }
21579
- function formatPriceLabel(scale, heightPx, value, mintick) {
21580
- const wideLog = scale.log && Math.log10(scale.max) - Math.log10(scale.min) >= 1.1;
21581
- if (wideLog) {
21582
- const d = valueDecimals(value);
21583
- return value.toFixed(d === 0 ? 2 : d);
21584
- }
21585
- return value.toFixed(axisDecimals(scale, heightPx, mintick));
21586
- }
21587
- var SEC = 1e3;
21588
- var MIN = 60 * SEC;
21589
- var HOUR = 60 * MIN;
21590
- var DAY = 24 * HOUR;
21591
- var WEEK = 7 * DAY;
21592
- var MONTH = 30 * DAY;
21593
- var YEAR = 365 * DAY;
21594
- var STEP_LADDER = [
21595
- SEC,
21596
- 5 * SEC,
21597
- 15 * SEC,
21598
- 30 * SEC,
21599
- MIN,
21600
- 5 * MIN,
21601
- 15 * MIN,
21602
- 30 * MIN,
21603
- HOUR,
21604
- 2 * HOUR,
21605
- 4 * HOUR,
21606
- 6 * HOUR,
21607
- 12 * HOUR,
21608
- DAY,
21609
- 2 * DAY,
21610
- WEEK,
21611
- MONTH,
21612
- 3 * MONTH,
21613
- YEAR
21614
- ];
21615
- var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
21616
- var pad2 = (n) => n < 10 ? `0${n}` : String(n);
21617
- function pickStep(targetMs) {
21618
- for (const step of STEP_LADDER) if (step >= targetMs) return step;
21619
- return STEP_LADDER[STEP_LADDER.length - 1];
21620
- }
21621
- function timeTicks(fromMs, toMs, target = 8, offsetMs = 0) {
21622
- const span = toMs - fromMs;
21623
- if (!(span > 0)) return [];
21624
- const step = pickStep(span / Math.max(1, target));
21625
- const zFrom = fromMs + offsetMs;
21626
- const zTo = toMs + offsetMs;
21627
- const first = Math.ceil(zFrom / step) * step;
21628
- const out = [];
21629
- for (let zt = first; zt <= zTo; zt += step) {
21630
- const t = zt - offsetMs;
21631
- const d = new Date(zt);
21632
- let label;
21633
- let major = false;
21634
- if (step < DAY) {
21635
- const h = d.getUTCHours();
21636
- const m = d.getUTCMinutes();
21637
- if (h === 0 && m === 0) {
21638
- label = `${MONTHS[d.getUTCMonth()]} ${d.getUTCDate()}`;
21639
- major = true;
21640
- } else {
21641
- label = `${pad2(h)}:${pad2(m)}`;
21642
- }
21643
- } else if (step < YEAR) {
21644
- label = `${MONTHS[d.getUTCMonth()]} ${d.getUTCDate()}`;
21645
- if (d.getUTCDate() === 1) {
21646
- label = MONTHS[d.getUTCMonth()];
21647
- major = true;
21648
- }
21649
- } else {
21650
- label = String(d.getUTCFullYear());
21651
- major = true;
21652
- }
21653
- out.push({ time: t, label, major });
21654
- }
21655
- return out;
21656
- }
21657
-
21658
- // src/renderers/shared/trade-markers.ts
21659
- var TRADE_LONG_COLOR = TRADE_LONG;
21660
- var TRADE_SHORT_COLOR = TRADE_SHORT;
21661
- var TRADE_EXIT_COLOR = TRADE_EXIT;
21662
- function defaultTradeMarkersState() {
21663
- return {
21664
- visible: true,
21665
- labels: true,
21666
- qty: true,
21667
- colors: { long: TRADE_LONG_COLOR, short: TRADE_SHORT_COLOR, exit: TRADE_EXIT_COLOR }
21668
- };
21669
- }
21670
- function mergeTradeMarkersState(base, patch) {
21671
- const p = patch && typeof patch === "object" ? patch : {};
21672
- const c = p.colors && typeof p.colors === "object" ? p.colors : {};
21673
- const bool = (v, fb) => typeof v === "boolean" ? v : fb;
21674
- const color = (v, fb) => typeof v === "string" && v.trim().length > 0 ? v : fb;
21675
- return {
21676
- visible: bool(p.visible, base.visible),
21677
- labels: bool(p.labels, base.labels),
21678
- qty: bool(p.qty, base.qty),
21679
- colors: {
21680
- long: color(c.long, base.colors.long),
21681
- short: color(c.short, base.colors.short),
21682
- exit: color(c.exit, base.colors.exit)
21683
- }
21684
- };
21685
- }
21686
- var BAR_GAP = 10;
21687
- var ARROW_W = 9;
21688
- var HEAD_H = 6;
21689
- var ARROW_H = 14;
21690
- var STEM_W = 3;
21691
- var CAP_H = 2;
21692
- var CAP_GAP = 2;
21693
- var TEXT_GAP = 3;
21694
- var UNIT_GAP = 6;
21695
- var TICK_W = 6;
21696
- var TICK_H = 8;
21697
- function lineHeightOf(fontSize) {
21698
- return fontSize + 4;
21699
- }
21700
- function qtyText(exec) {
21701
- if (exec.qty == null || !Number.isFinite(exec.qty)) return null;
21702
- const magnitude = Number(Math.abs(exec.qty).toFixed(8));
21703
- return `${exec.side === "buy" ? "+" : "-"}${magnitude}`;
21704
- }
21705
- function unitOf(exec, state, lineH) {
21706
- const lines = [];
21707
- if (state.labels && exec.label) lines.push(exec.label);
21708
- if (state.qty) {
21709
- const q = qtyText(exec);
21710
- if (q) lines.push(q);
21711
- }
21712
- return { exec, lines, height: ARROW_H + (lines.length ? TEXT_GAP + lines.length * lineH : 0) };
21713
- }
21714
- function stacksFor(trades, state, deps, from, to, lineH) {
21715
- const byBar = /* @__PURE__ */ new Map();
21716
- for (const exec of trades) {
21717
- const logical = Math.round(deps.timeToLogical(exec.time));
21718
- if (logical < from || logical > to) continue;
21719
- let stack = byBar.get(logical);
21720
- if (!stack) {
21721
- stack = { logical, buys: [], sells: [] };
21722
- byBar.set(logical, stack);
21723
- }
21724
- (exec.side === "buy" ? stack.buys : stack.sells).push(unitOf(exec, state, lineH));
21725
- }
21726
- return [...byBar.values()];
21727
- }
21728
- function stackExtent(units) {
21729
- if (units.length === 0) return 0;
21730
- let px = BAR_GAP;
21731
- for (const u of units) px += u.height;
21732
- return px + (units.length - 1) * UNIT_GAP;
21733
- }
21734
- function tradesPriceHints(trades, state, deps, from, to, fontSize) {
21735
- if (trades.length === 0) return null;
21736
- const lineH = lineHeightOf(fontSize);
21737
- let min = Infinity;
21738
- let max = -Infinity;
21739
- let abovePx = 0;
21740
- let belowPx = 0;
21741
- for (const stack of stacksFor(trades, state, deps, Math.floor(from), Math.ceil(to), lineH)) {
21742
- const bar = deps.barAt(stack.logical);
21743
- if (!bar) continue;
21744
- if (bar.low < min) min = bar.low;
21745
- if (bar.high > max) max = bar.high;
21746
- belowPx = Math.max(belowPx, stackExtent(stack.buys));
21747
- abovePx = Math.max(abovePx, stackExtent(stack.sells));
21748
- }
21749
- if (!Number.isFinite(min) || !Number.isFinite(max)) return null;
21750
- return { min, max, abovePx, belowPx };
21751
- }
21752
- function renderTradeMarkers(ctx, trades, state, deps, xOf, yOf, text, width, barHalfPx) {
21753
- if (trades.length === 0) return;
21754
- const lineH = lineHeightOf(text.fontSize);
21755
- const stacks = stacksFor(trades, state, deps, -Infinity, Infinity, lineH);
21756
- if (stacks.length === 0) return;
21757
- ctx.save();
21758
- ctx.font = `${text.fontSize}px ${text.fontFamily}`;
21759
- ctx.textAlign = "center";
21760
- ctx.textBaseline = "middle";
21761
- for (const stack of stacks) {
21762
- const x = xOf(stack.logical);
21763
- if (x < -150 || x > width + 150) continue;
21764
- const bar = deps.barAt(stack.logical);
21765
- if (!bar) continue;
21766
- const yBottom = Math.max(yOf(bar.low), yOf(bar.high));
21767
- const yTop = Math.min(yOf(bar.low), yOf(bar.high));
21768
- let y = yBottom + BAR_GAP;
21769
- for (const unit of stack.buys) {
21770
- ctx.fillStyle = colorOf(unit.exec, state.colors);
21771
- drawArrowUp(ctx, x, y, unit.exec.kind === "exit");
21772
- drawFillTick(ctx, unit.exec.side, x, yOf(unit.exec.price), barHalfPx);
21773
- drawTextLines(ctx, unit.lines, x, y + ARROW_H + TEXT_GAP + lineH / 2, lineH, text.color);
21774
- y += unit.height + UNIT_GAP;
21775
- }
21776
- y = yTop - BAR_GAP;
21777
- for (const unit of stack.sells) {
21778
- ctx.fillStyle = colorOf(unit.exec, state.colors);
21779
- drawArrowDown(ctx, x, y, unit.exec.kind === "exit");
21780
- drawFillTick(ctx, unit.exec.side, x, yOf(unit.exec.price), barHalfPx);
21781
- drawTextLines(ctx, unit.lines, x, y - ARROW_H - TEXT_GAP - lineH / 2, -lineH, text.color);
21782
- y -= unit.height + UNIT_GAP;
21783
- }
21784
- }
21785
- ctx.restore();
21786
- }
21787
- function colorOf(exec, colors) {
21788
- if (exec.kind === "exit") return colors.exit;
21789
- return exec.side === "buy" ? colors.long : colors.short;
21790
- }
21791
- function drawArrowUp(ctx, x, yTip, capped) {
21792
- ctx.beginPath();
21793
- ctx.moveTo(x, yTip);
21794
- ctx.lineTo(x - ARROW_W / 2, yTip + HEAD_H);
21795
- ctx.lineTo(x + ARROW_W / 2, yTip + HEAD_H);
21796
- ctx.closePath();
21797
- ctx.fill();
21798
- ctx.fillRect(x - STEM_W / 2, yTip + HEAD_H, STEM_W, ARROW_H - HEAD_H);
21799
- if (capped) ctx.fillRect(x - ARROW_W / 2, yTip - CAP_GAP - CAP_H, ARROW_W, CAP_H);
21800
- }
21801
- function drawArrowDown(ctx, x, yTip, capped) {
21802
- ctx.beginPath();
21803
- ctx.moveTo(x, yTip);
21804
- ctx.lineTo(x - ARROW_W / 2, yTip - HEAD_H);
21805
- ctx.lineTo(x + ARROW_W / 2, yTip - HEAD_H);
21806
- ctx.closePath();
21807
- ctx.fill();
21808
- ctx.fillRect(x - STEM_W / 2, yTip - ARROW_H, STEM_W, ARROW_H - HEAD_H);
21809
- if (capped) ctx.fillRect(x - ARROW_W / 2, yTip + CAP_GAP, ARROW_W, CAP_H);
21810
- }
21811
- function drawFillTick(ctx, side, x, yFill, barHalfPx) {
21812
- const edge = side === "buy" ? x - barHalfPx : x + barHalfPx;
21813
- const back = side === "buy" ? edge - TICK_W : edge + TICK_W;
21814
- ctx.beginPath();
21815
- ctx.moveTo(edge, yFill);
21816
- ctx.lineTo(back, yFill - TICK_H / 2);
21817
- ctx.lineTo(back, yFill + TICK_H / 2);
21818
- ctx.closePath();
21819
- ctx.fill();
21820
- }
21821
- function drawTextLines(ctx, lines, x, firstY, step, color) {
21822
- if (lines.length === 0) return;
21823
- ctx.fillStyle = color;
21824
- for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], x, firstY + i * step);
21825
- }
21826
-
21827
- // src/renderers/native/core/SceneGraph.ts
21828
- var SceneGraph = class {
21829
- constructor() {
21830
- this.panes = /* @__PURE__ */ new Map();
21831
- this.indicators = /* @__PURE__ */ new Map();
21832
- this.bars = [];
21833
- /** Volume-layer config pushed by the volume native indicator (null ⇒ layer off). Ephemeral. */
21834
- this.volumeLayer = null;
21835
- /** Generic native-data channels for SDK renderer layers (`setNativeData(id, …)`). Ephemeral. */
21836
- this.nativeData = /* @__PURE__ */ new Map();
21837
- /** Loading ranges per channel (`setNativeData(id + '-pending', …)`). Ephemeral. */
21838
- this.nativePending = /* @__PURE__ */ new Map();
21839
- /** VPVR-layer config pushed by the VPVR native indicator (null ⇒ layer off). Ephemeral. */
21840
- this.vpvrLayer = null;
21841
- this.crosshair = null;
21842
- /** How the base price series is drawn on the price pane (candles by default). */
21843
- this.priceStyle = "candles";
21844
- /** Price-series base painting for the ACTIVE style (see ChartTypeDefinition.basePainting). */
21845
- this.basePainting = "candles";
21846
- /** The ACTIVE style's own candle cosmetics (`chartTypes.<id>.candle*`) when it is a
21847
- * candle-based plugin type; null ⇒ paint with the shared `style.candle` block. */
21848
- this.candleOverride = null;
21849
- /** Explicit baseline reference price for `priceStyle:'baseline'`; when null the
21850
- * baseline follows `style.baseline.baselineLevel` as a percent of the visible pane
21851
- * range (resolved per frame via `baselinePriceFor`). */
21852
- this.baselineValue = null;
21853
- /** Draw the dashed horizontal line at the latest price (price pane). Independent
21854
- * of the axis label chip (`showPriceLabel`) — either can show without the other. */
21855
- this.showPriceLine = true;
21856
- /** Draw the last-price label chip on the price axis. Independent of the line. */
21857
- this.showPriceLabel = true;
21858
- /** Draw the countdown-to-bar-close chip on the price axis. When the price label is
21859
- * also shown, the two merge into one stacked block (countdown under the label);
21860
- * when either shows alone it's centered on the latest price level. */
21861
- this.showCountdown = true;
21862
- /** Logarithmic price scale on the price pane. */
21863
- this.logScale = false;
21864
- /** Inverted price axis on the price pane (high at the bottom). Study panes carry their own. */
21865
- this.invertScale = false;
21866
- /** Exchange tick size for the active symbol (e.g. 0.01), when known. Drives the
21867
- * price-axis decimals — the instrument's true precision instead of the zoom-derived
21868
- * formula. Undefined until symbol metadata loads (the formula is the fallback). */
21869
- this.priceMintick = void 0;
21870
- /** Price-axis mode on the price pane: `'price'` (absolute) or `'percent'` (change
21871
- * vs `percentBaseline`). Gridlines, axis labels and crosshair chip all follow it. */
21872
- this.scaleMode = "price";
21873
- /** Reference price for percent mode (first visible bar's close); recomputed per frame. */
21874
- this.percentBaseline = 0;
21875
- /** IANA time zone for the time axis + crosshair/data-window stamps (`'UTC'` default). */
21876
- this.timezone = "UTC";
21877
- /** Draw the background gridlines (price + time). Master toggle (`gridlines`
21878
- * feature); per-axis visibility + colors live in `style.gridVert`/`gridHorz`. */
21879
- this.showGrid = true;
21880
- /** Comprehensive cosmetic config (item 15): grid colors, crosshair, candle
21881
- * border/wick, fonts, separators. Serialized via the renderer's `getConfig()`/
21882
- * `applyConfig()`; every draw layer reads its knobs from here, falling back to
21883
- * the theme for any value left at its inherit default. */
21884
- this.style = defaultChartStyle();
21885
- /** Draw the price/time axis tick labels. */
21886
- this.showAxisLabels = true;
21887
- /** Strategy trade-marker display (the `tradeMarkers` feature): master toggle, the
21888
- * two text lines, and the palette. Trade markers always paint on the price pane. */
21889
- this.tradeMarkers = defaultTradeMarkersState();
21890
- /** Renderer-owned shaded time bands (session highlighting), behind grid + data. */
21891
- this.highlights = [];
21892
- /** Pre/post-market bands pushed by the host (`sessionZones` feature); null ⇒ no sessions. */
21893
- this.sessionZones = null;
21894
- /** Draw-order key of the price candles, relative to indicator series z (see `seriesZ`).
21895
- * Indicators with z below this draw BEHIND the candles; at/above draw in front.
21896
- * Default 0 with indicators mounting at z < 0 ⇒ the price reads on top of every overlay,
21897
- * and user drawings (z ≥ 1 by default) on top of the price. */
21898
- this.candleZ = 0;
21899
- /** Hide the base price series (candles/bars/line/area) without removing it — overlay
21900
- * indicators keep drawing and the pane autoscales to them. Toggled from the object tree. */
21901
- this.candlesHidden = false;
21902
- /** Per-indicator foreground draw-order key (series layer), keyed by indicator id.
21903
- * Higher = drawn later (in front). Assigned on mount to the current BOTTOM of the stack,
21904
- * so each indicator arrives behind the candles (and behind older indicators);
21905
- * `setIndicatorZ`/`bringToFront`/`sendToBack` change it. */
21906
- this.seriesZ = /* @__PURE__ */ new Map();
21907
- /** Per-pane raster layers of user drawings interleaved into the series stack — each is a
21908
- * prepainted canvas the backend composites just before the series carrying `beforeZ`.
21909
- * Rebuilt by the renderer per data frame; empty when every drawing sits over the stack. */
21910
- this.drawingSlices = /* @__PURE__ */ new Map();
21911
- /** Per-model index offset: the chart bar index of the model's `anchorTime` — its
21912
- * index-aligned payloads (dense series arrays, `bar_index` drawings) count from that
21913
- * bar. Only nonzero for models computed over a SUFFIX of the bars (whole-chart models,
21914
- * the norm, aren't stored). Recomputed by the renderer on setBars + mount/patch. */
21915
- this.anchorOffsets = /* @__PURE__ */ new Map();
21916
- /** Per-indicator private price windows (merged indicators drawn on their own scale
21917
- * column). Populated per frame for models flagged `ownScale`; absent ⇒ the model
21918
- * shares its pane's master scale. */
21919
- this.indicatorScales = /* @__PURE__ */ new Map();
21920
- /** Cached sort of `panes` by order; invalidated on add/remove/reorder. */
21921
- this.orderedCache = null;
21922
- }
21923
- /** Panes sorted top-to-bottom by `order`. Cached — callers must NOT mutate the array. */
21924
- orderedPanes() {
21925
- if (!this.orderedCache) this.orderedCache = [...this.panes.values()].sort((a, b) => a.order - b.order);
21926
- return this.orderedCache;
21927
- }
21928
- /** The session zones resolved into colored bands (pre/post-market washes from the
21929
- * config's session colors) — consumed by the same painting path as {@link highlights}. */
21930
- sessionHighlightBands() {
21931
- if (!this.sessionZones) return [];
21932
- const out = [];
21933
- for (const [from, to] of this.sessionZones.pre) out.push({ from, to, color: this.style.sessions.premarketColor });
21934
- for (const [from, to] of this.sessionZones.post) out.push({ from, to, color: this.style.sessions.postmarketColor });
21935
- return out;
21936
- }
21937
- indicatorsForPane(paneId) {
21938
- const out = [];
21939
- for (const model of this.indicators.values()) if (model.paneId === paneId) out.push(model);
21940
- return out;
21941
- }
21942
- /** Merged (own-scale) indicators on a pane, ordered by z — one axis column each. */
21943
- ownScaleIndicatorsForPane(paneId) {
21944
- return this.orderedIndicatorsForPane(paneId).filter((m) => m.ownScale === true);
21945
- }
21946
- /** Ensure a merged indicator has a private scale slot (seeded from the pane if given). */
21947
- ensureIndicatorScale(id, seed) {
21948
- let s = this.indicatorScales.get(id);
21949
- if (!s) {
21950
- const base = seed ?? { min: 0, max: 1 };
21951
- s = { scale: { ...base }, scaleTarget: { ...base }, initialized: false, manualScale: null };
21952
- this.indicatorScales.set(id, s);
21953
- }
21954
- return s;
21955
- }
21956
- dropIndicatorScale(id) {
21957
- this.indicatorScales.delete(id);
21958
- }
21959
- /** The price window a model renders on: its own scale when merged (`ownScale`), else the pane's. */
21960
- scaleFor(model, pane) {
21961
- if (model.ownScale === true) {
21962
- const s = this.indicatorScales.get(model.id);
21963
- if (s) return s.scale;
21964
- }
21965
- return pane.scale;
21966
- }
21967
- /** Apply a new top-to-bottom pane order (ids not present are ignored). */
21968
- orderPanes(orderedIds) {
21969
- orderedIds.forEach((id, i) => {
21970
- const pane = this.panes.get(id);
21971
- if (pane) pane.order = i;
21972
- });
21973
- this.orderedCache = null;
21974
- }
21975
- /** Indicators on a pane sorted by foreground z (ascending). Array#sort is stable,
21976
- * so equal-z models keep their insertion order (the default). */
21977
- orderedIndicatorsForPane(paneId) {
21978
- return this.indicatorsForPane(paneId).sort((a, b) => this.zOf(a.id) - this.zOf(b.id));
21979
- }
21980
- /** The foreground draw-order key of an indicator (0 when never assigned). */
21981
- zOf(id) {
21982
- return this.seriesZ.get(id) ?? 0;
21983
- }
21984
- /** The model's index offset: chart bar index its index-aligned payloads count from (0 = whole-chart). */
21985
- offsetOf(id) {
21986
- return this.anchorOffsets.get(id) ?? 0;
21987
- }
21988
- /** Offsets are SIGNED. Positive: the model starts after the chart's first bar (it ran
21989
- * over a suffix) — readers skip its leading chart bars. Negative: the model starts
21990
- * BEFORE it (the chart's head moved forward under a mounted model) — readers skip the
21991
- * model's own leading points, `points[i - off]` reaching further in. Storing only the
21992
- * positive case silently pinned such a model at index 0, i.e. drew it shifted. */
21993
- setAnchorOffset(id, offset) {
21994
- if (offset !== 0 && Number.isFinite(offset)) this.anchorOffsets.set(id, offset);
21995
- else this.anchorOffsets.delete(id);
21996
- }
21997
- forgetAnchorOffset(id) {
21998
- this.anchorOffsets.delete(id);
21999
- }
22000
- /** Resolve the baseline reference price for the given pane window: the explicit
22001
- * `baselineValue` when set, else `style.baseline.baselineLevel` as the price that sits
22002
- * at that fraction of the pane height. Interpolated in the same space the pane renders
22003
- * in (log when `scale.log`, else linear) so `level%` always lands at `level%` of the
22004
- * height — matching `CoordinateSystem.yToPrice`. */
22005
- baselinePriceFor(scale) {
22006
- if (this.baselineValue != null) return this.baselineValue;
22007
- const t = this.style.baseline.baselineLevel / 100;
22008
- if (scale.log && scale.min > 0 && scale.max > scale.min) {
22009
- const lo = Math.log(scale.min);
22010
- return Math.exp(lo + t * (Math.log(scale.max) - lo));
22011
- }
22012
- return scale.min + (scale.max - scale.min) * t;
22013
- }
22014
- /** Assign a default z on mount: the current bottom of the stack, so a new indicator
22015
- * paints behind the candles and behind every indicator already there — the price stays
22016
- * the top of the pile until the user restacks it. No-op if the indicator already has one. */
22017
- assignIndicatorZ(id) {
22018
- if (!this.seriesZ.has(id)) this.seriesZ.set(id, this.bottomZ() - 1);
22019
- }
22020
- /** Mount-time default for a LAYER-BACKED native (it paints on a canvas stacked above the
22021
- * data canvas by default): top of the stack, so the recorded order tells the truth from
22022
- * the first frame. Keeps an existing key, so a restored stack survives the remount. */
22023
- assignIndicatorZTop(id) {
22024
- if (!this.seriesZ.has(id)) this.seriesZ.set(id, this.topZ() + 1);
22025
- }
22026
- forgetIndicatorZ(id) {
22027
- this.seriesZ.delete(id);
22028
- }
22029
- setIndicatorZ(id, z) {
22030
- this.seriesZ.set(id, z);
22031
- }
22032
- /** Snapshot of the current ordering for a UI/read API: `{ id, z }` sorted by z. */
22033
- indicatorZOrder() {
22034
- return [...this.seriesZ.entries()].map(([id, z]) => ({ id, z })).sort((a, b) => a.z - b.z);
22035
- }
22036
- /** The pane's series z keys (each indicator, plus the candles on the price pane), sorted
22037
- * ascending and de-duplicated — the boundaries a user drawing's z is slotted against. */
22038
- seriesBoundaries(paneId) {
22039
- const keys = /* @__PURE__ */ new Set();
22040
- if (paneId === "price") keys.add(this.candleZ);
22041
- for (const m of this.indicatorsForPane(paneId)) keys.add(this.zOf(m.id));
22042
- return [...keys].sort((a, b) => a - b);
22043
- }
22044
- /** Raise an indicator above every other layer (other indicators AND the candles). */
22045
- bringIndicatorToFront(id) {
22046
- this.seriesZ.set(id, this.topZ() + 1);
22047
- }
22048
- /** Drop an indicator below every other layer (other indicators AND the candles). */
22049
- sendIndicatorToBack(id) {
22050
- this.seriesZ.set(id, this.bottomZ() - 1);
22051
- }
22052
- topZ() {
22053
- let max = this.candleZ;
22054
- for (const z of this.seriesZ.values()) if (z > max) max = z;
22055
- return max;
22056
- }
22057
- bottomZ() {
22058
- let min = this.candleZ;
22059
- for (const z of this.seriesZ.values()) if (z < min) min = z;
22060
- return min;
22061
- }
22062
- ensurePane(id, kind, order, heightWeight) {
22063
- this.orderedCache = null;
22064
- const existing = this.panes.get(id);
22065
- if (existing) {
22066
- existing.order = order;
22067
- existing.heightWeight = heightWeight;
22068
- existing.kind = kind;
22069
- return existing;
22070
- }
22071
- const pane = { id, kind, order, heightWeight, bounds: { top: 0, height: 0 }, scale: { min: 0, max: 1 }, scaleTarget: { min: 0, max: 1 }, initialized: false, manualScale: null, collapsed: false, percentBaseline: 0 };
22072
- this.panes.set(id, pane);
22073
- return pane;
22074
- }
22075
- removePane(id) {
22076
- this.panes.delete(id);
22077
- this.orderedCache = null;
22078
- }
22079
- };
22080
- function paneScaleMode(scene, pane) {
22081
- return pane.kind === "price" ? scene.scaleMode : pane.scaleMode ?? "price";
22082
- }
22083
- function paneLogScale(scene, pane) {
22084
- return pane.kind === "price" ? scene.logScale : pane.logScale ?? false;
22085
- }
22086
- function paneInvert(scene, pane) {
22087
- return pane.kind === "price" ? scene.invertScale : pane.invert ?? false;
22088
- }
22089
- function percentScaleFor(scene, pane) {
22090
- const mode = paneScaleMode(scene, pane);
22091
- if (mode !== "percent" && mode !== "indexed") return void 0;
22092
- const baseline = pane.percentBaseline;
22093
- if (!Number.isFinite(baseline) || baseline === 0) return void 0;
22094
- return { baseline, indexed: mode === "indexed" };
22095
- }
22096
-
22097
- // src/renderers/native/chrome/tz.ts
22098
- function tzOffsetMs(ms, timeZone) {
22099
- if (!timeZone || timeZone === "UTC") return 0;
22100
- try {
22101
- const dtf = new Intl.DateTimeFormat("en-US", {
22102
- timeZone,
22103
- hourCycle: "h23",
22104
- year: "numeric",
22105
- month: "2-digit",
22106
- day: "2-digit",
22107
- hour: "2-digit",
22108
- minute: "2-digit",
22109
- second: "2-digit"
22110
- });
22111
- const parts = dtf.formatToParts(new Date(ms));
22112
- const get = (t) => Number(parts.find((p) => p.type === t)?.value);
22113
- const asUTC = Date.UTC(get("year"), get("month") - 1, get("day"), get("hour") % 24, get("minute"), get("second"));
22114
- return asUTC - ms;
22115
- } catch {
22116
- return 0;
22117
- }
22118
- }
22119
- function zonedDate(ms, timeZone) {
22120
- return new Date(ms + tzOffsetMs(ms, timeZone));
22121
- }
22122
-
22123
21712
  // src/renderers/native/backend/candle-lod.ts
22124
21713
  var CANDLE_BODY_MIN_SPACING = 3;
22125
21714
  var CANDLE_WICK_W = 1.5;
@@ -22409,11 +21998,6 @@ var DASH = {
22409
21998
  dashed: [6, 4],
22410
21999
  dotted: [2, 3]
22411
22000
  };
22412
- function crispHairline(cssCenter, dpr) {
22413
- const w = Math.max(1, Math.round(dpr));
22414
- const edge = Math.round(cssCenter * dpr - w / 2);
22415
- return { pos: edge / dpr, size: w / dpr };
22416
- }
22417
22001
  function joinSegments(hw) {
22418
22002
  return Math.max(6, Math.min(20, Math.round(hw * 4)));
22419
22003
  }
@@ -22423,7 +22007,6 @@ var WebGL2Backend = class {
22423
22007
  this.modelAlpha = 1;
22424
22008
  this.candleBodyAlpha = 1;
22425
22009
  this.candleStructureAlpha = 1;
22426
- this.gridAlpha = 1;
22427
22010
  this.candleBodyScale = 1;
22428
22011
  /** Set by the renderer; invoked after a context restore to request a repaint. */
22429
22012
  this.onNeedsRedraw = null;
@@ -22653,11 +22236,7 @@ var WebGL2Backend = class {
22653
22236
  }
22654
22237
  };
22655
22238
  b.alpha = 1;
22656
- if (pane.collapsed) {
22657
- this.emitGrid(b, coords, theme, dataW, pane, scene);
22658
- } else {
22659
- this.emitHighlights(b, scene, pane, coords);
22660
- this.emitGrid(b, coords, theme, dataW, pane, scene);
22239
+ if (!pane.collapsed) {
22661
22240
  const models = scene.orderedIndicatorsForPane(pane.id);
22662
22241
  const isPrice = pane.kind === "price";
22663
22242
  const effPane = (m) => {
@@ -22842,49 +22421,6 @@ var WebGL2Backend = class {
22842
22421
  this.screenProgram = null;
22843
22422
  }
22844
22423
  // ── geometry emit (mirrors Canvas2dBackend, in CSS-px space) ──
22845
- emitGrid(b, coords, theme, dataW, pane, scene) {
22846
- const top = pane.bounds.top;
22847
- const bot = pane.bounds.top + pane.bounds.height;
22848
- const { gridVert, gridHorz } = scene.style;
22849
- const dpr = coords.dpr;
22850
- b.alpha = this.gridAlpha;
22851
- if (scene.showGrid && gridVert.visible && !pane.collapsed) {
22852
- const grid = parseColor(gridVert.color ?? theme.gridColor);
22853
- const tr = coords.visibleTimeRange();
22854
- const offset = tzOffsetMs((tr.from + tr.to) / 2, scene.timezone);
22855
- for (const tick of timeTicks(tr.from, tr.to, 8, offset)) {
22856
- const x = coords.timeToX(tick.time);
22857
- if (x < 0 || x > dataW) continue;
22858
- const g = crispHairline(x, dpr);
22859
- b.rect(g.pos, top, g.size, bot - top, grid);
22860
- }
22861
- }
22862
- if (scene.showGrid && gridHorz.visible && !pane.collapsed) {
22863
- const grid = parseColor(gridHorz.color ?? theme.gridColor);
22864
- const pct = percentScaleFor(scene, pane);
22865
- for (const t of paneAxisTicks(pane.scale, pane.bounds.height, pct)) {
22866
- const y = coords.priceToY(t.price, pane.scale, pane.bounds);
22867
- if (y < top || y > bot) continue;
22868
- const g = crispHairline(y, dpr);
22869
- b.rect(0, g.pos, dataW, g.size, grid);
22870
- }
22871
- }
22872
- }
22873
- /** Renderer-owned session highlight bands, clipped per-pane (scissor reconstructs full height).
22874
- * Session-zone washes (pre/post-market) paint first, host highlights on top. */
22875
- emitHighlights(b, scene, pane, coords) {
22876
- const bands = [...scene.sessionHighlightBands(), ...scene.highlights];
22877
- if (bands.length === 0) return;
22878
- for (const band of bands) {
22879
- const x1 = coords.timeToX(band.from);
22880
- const x2 = coords.timeToX(band.to);
22881
- if (x2 < 0 || x1 > coords.width || x2 <= x1) continue;
22882
- const cx = Math.max(0, x1);
22883
- const cw = Math.min(coords.width, x2) - cx;
22884
- if (cw <= 0) continue;
22885
- b.rect(cx, pane.bounds.top, cw, pane.bounds.height, parseColor(band.color));
22886
- }
22887
- }
22888
22424
  emitBackground(b, bg, pane, coords) {
22889
22425
  const x1 = coords.timeToX(bg.from);
22890
22426
  const x2 = coords.timeToX(bg.to);
@@ -24207,6 +23743,445 @@ var KeyboardController = class {
24207
23743
  }
24208
23744
  };
24209
23745
 
23746
+ // src/renderers/shared/trade-markers.ts
23747
+ var TRADE_LONG_COLOR = TRADE_LONG;
23748
+ var TRADE_SHORT_COLOR = TRADE_SHORT;
23749
+ var TRADE_EXIT_COLOR = TRADE_EXIT;
23750
+ function defaultTradeMarkersState() {
23751
+ return {
23752
+ visible: true,
23753
+ labels: true,
23754
+ qty: true,
23755
+ colors: { long: TRADE_LONG_COLOR, short: TRADE_SHORT_COLOR, exit: TRADE_EXIT_COLOR }
23756
+ };
23757
+ }
23758
+ function mergeTradeMarkersState(base, patch) {
23759
+ const p = patch && typeof patch === "object" ? patch : {};
23760
+ const c = p.colors && typeof p.colors === "object" ? p.colors : {};
23761
+ const bool = (v, fb) => typeof v === "boolean" ? v : fb;
23762
+ const color = (v, fb) => typeof v === "string" && v.trim().length > 0 ? v : fb;
23763
+ return {
23764
+ visible: bool(p.visible, base.visible),
23765
+ labels: bool(p.labels, base.labels),
23766
+ qty: bool(p.qty, base.qty),
23767
+ colors: {
23768
+ long: color(c.long, base.colors.long),
23769
+ short: color(c.short, base.colors.short),
23770
+ exit: color(c.exit, base.colors.exit)
23771
+ }
23772
+ };
23773
+ }
23774
+ var BAR_GAP = 10;
23775
+ var ARROW_W = 9;
23776
+ var HEAD_H = 6;
23777
+ var ARROW_H = 14;
23778
+ var STEM_W = 3;
23779
+ var CAP_H = 2;
23780
+ var CAP_GAP = 2;
23781
+ var TEXT_GAP = 3;
23782
+ var UNIT_GAP = 6;
23783
+ var TICK_W = 6;
23784
+ var TICK_H = 8;
23785
+ function lineHeightOf(fontSize) {
23786
+ return fontSize + 4;
23787
+ }
23788
+ function qtyText(exec) {
23789
+ if (exec.qty == null || !Number.isFinite(exec.qty)) return null;
23790
+ const magnitude = Number(Math.abs(exec.qty).toFixed(8));
23791
+ return `${exec.side === "buy" ? "+" : "-"}${magnitude}`;
23792
+ }
23793
+ function unitOf(exec, state, lineH) {
23794
+ const lines = [];
23795
+ if (state.labels && exec.label) lines.push(exec.label);
23796
+ if (state.qty) {
23797
+ const q = qtyText(exec);
23798
+ if (q) lines.push(q);
23799
+ }
23800
+ return { exec, lines, height: ARROW_H + (lines.length ? TEXT_GAP + lines.length * lineH : 0) };
23801
+ }
23802
+ function stacksFor(trades, state, deps, from, to, lineH) {
23803
+ const byBar = /* @__PURE__ */ new Map();
23804
+ for (const exec of trades) {
23805
+ const logical = Math.round(deps.timeToLogical(exec.time));
23806
+ if (logical < from || logical > to) continue;
23807
+ let stack = byBar.get(logical);
23808
+ if (!stack) {
23809
+ stack = { logical, buys: [], sells: [] };
23810
+ byBar.set(logical, stack);
23811
+ }
23812
+ (exec.side === "buy" ? stack.buys : stack.sells).push(unitOf(exec, state, lineH));
23813
+ }
23814
+ return [...byBar.values()];
23815
+ }
23816
+ function stackExtent(units) {
23817
+ if (units.length === 0) return 0;
23818
+ let px = BAR_GAP;
23819
+ for (const u of units) px += u.height;
23820
+ return px + (units.length - 1) * UNIT_GAP;
23821
+ }
23822
+ function tradesPriceHints(trades, state, deps, from, to, fontSize) {
23823
+ if (trades.length === 0) return null;
23824
+ const lineH = lineHeightOf(fontSize);
23825
+ let min = Infinity;
23826
+ let max = -Infinity;
23827
+ let abovePx = 0;
23828
+ let belowPx = 0;
23829
+ for (const stack of stacksFor(trades, state, deps, Math.floor(from), Math.ceil(to), lineH)) {
23830
+ const bar = deps.barAt(stack.logical);
23831
+ if (!bar) continue;
23832
+ if (bar.low < min) min = bar.low;
23833
+ if (bar.high > max) max = bar.high;
23834
+ belowPx = Math.max(belowPx, stackExtent(stack.buys));
23835
+ abovePx = Math.max(abovePx, stackExtent(stack.sells));
23836
+ }
23837
+ if (!Number.isFinite(min) || !Number.isFinite(max)) return null;
23838
+ return { min, max, abovePx, belowPx };
23839
+ }
23840
+ function renderTradeMarkers(ctx, trades, state, deps, xOf, yOf, text, width, barHalfPx) {
23841
+ if (trades.length === 0) return;
23842
+ const lineH = lineHeightOf(text.fontSize);
23843
+ const stacks = stacksFor(trades, state, deps, -Infinity, Infinity, lineH);
23844
+ if (stacks.length === 0) return;
23845
+ ctx.save();
23846
+ ctx.font = `${text.fontSize}px ${text.fontFamily}`;
23847
+ ctx.textAlign = "center";
23848
+ ctx.textBaseline = "middle";
23849
+ for (const stack of stacks) {
23850
+ const x = xOf(stack.logical);
23851
+ if (x < -150 || x > width + 150) continue;
23852
+ const bar = deps.barAt(stack.logical);
23853
+ if (!bar) continue;
23854
+ const yBottom = Math.max(yOf(bar.low), yOf(bar.high));
23855
+ const yTop = Math.min(yOf(bar.low), yOf(bar.high));
23856
+ let y = yBottom + BAR_GAP;
23857
+ for (const unit of stack.buys) {
23858
+ ctx.fillStyle = colorOf(unit.exec, state.colors);
23859
+ drawArrowUp(ctx, x, y, unit.exec.kind === "exit");
23860
+ drawFillTick(ctx, unit.exec.side, x, yOf(unit.exec.price), barHalfPx);
23861
+ drawTextLines(ctx, unit.lines, x, y + ARROW_H + TEXT_GAP + lineH / 2, lineH, text.color);
23862
+ y += unit.height + UNIT_GAP;
23863
+ }
23864
+ y = yTop - BAR_GAP;
23865
+ for (const unit of stack.sells) {
23866
+ ctx.fillStyle = colorOf(unit.exec, state.colors);
23867
+ drawArrowDown(ctx, x, y, unit.exec.kind === "exit");
23868
+ drawFillTick(ctx, unit.exec.side, x, yOf(unit.exec.price), barHalfPx);
23869
+ drawTextLines(ctx, unit.lines, x, y - ARROW_H - TEXT_GAP - lineH / 2, -lineH, text.color);
23870
+ y -= unit.height + UNIT_GAP;
23871
+ }
23872
+ }
23873
+ ctx.restore();
23874
+ }
23875
+ function colorOf(exec, colors) {
23876
+ if (exec.kind === "exit") return colors.exit;
23877
+ return exec.side === "buy" ? colors.long : colors.short;
23878
+ }
23879
+ function drawArrowUp(ctx, x, yTip, capped) {
23880
+ ctx.beginPath();
23881
+ ctx.moveTo(x, yTip);
23882
+ ctx.lineTo(x - ARROW_W / 2, yTip + HEAD_H);
23883
+ ctx.lineTo(x + ARROW_W / 2, yTip + HEAD_H);
23884
+ ctx.closePath();
23885
+ ctx.fill();
23886
+ ctx.fillRect(x - STEM_W / 2, yTip + HEAD_H, STEM_W, ARROW_H - HEAD_H);
23887
+ if (capped) ctx.fillRect(x - ARROW_W / 2, yTip - CAP_GAP - CAP_H, ARROW_W, CAP_H);
23888
+ }
23889
+ function drawArrowDown(ctx, x, yTip, capped) {
23890
+ ctx.beginPath();
23891
+ ctx.moveTo(x, yTip);
23892
+ ctx.lineTo(x - ARROW_W / 2, yTip - HEAD_H);
23893
+ ctx.lineTo(x + ARROW_W / 2, yTip - HEAD_H);
23894
+ ctx.closePath();
23895
+ ctx.fill();
23896
+ ctx.fillRect(x - STEM_W / 2, yTip - ARROW_H, STEM_W, ARROW_H - HEAD_H);
23897
+ if (capped) ctx.fillRect(x - ARROW_W / 2, yTip + CAP_GAP, ARROW_W, CAP_H);
23898
+ }
23899
+ function drawFillTick(ctx, side, x, yFill, barHalfPx) {
23900
+ const edge = side === "buy" ? x - barHalfPx : x + barHalfPx;
23901
+ const back = side === "buy" ? edge - TICK_W : edge + TICK_W;
23902
+ ctx.beginPath();
23903
+ ctx.moveTo(edge, yFill);
23904
+ ctx.lineTo(back, yFill - TICK_H / 2);
23905
+ ctx.lineTo(back, yFill + TICK_H / 2);
23906
+ ctx.closePath();
23907
+ ctx.fill();
23908
+ }
23909
+ function drawTextLines(ctx, lines, x, firstY, step, color) {
23910
+ if (lines.length === 0) return;
23911
+ ctx.fillStyle = color;
23912
+ for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], x, firstY + i * step);
23913
+ }
23914
+
23915
+ // src/renderers/native/core/SceneGraph.ts
23916
+ var SceneGraph = class {
23917
+ constructor() {
23918
+ this.panes = /* @__PURE__ */ new Map();
23919
+ this.indicators = /* @__PURE__ */ new Map();
23920
+ this.bars = [];
23921
+ /** Volume-layer config pushed by the volume native indicator (null ⇒ layer off). Ephemeral. */
23922
+ this.volumeLayer = null;
23923
+ /** Generic native-data channels for SDK renderer layers (`setNativeData(id, …)`). Ephemeral. */
23924
+ this.nativeData = /* @__PURE__ */ new Map();
23925
+ /** Loading ranges per channel (`setNativeData(id + '-pending', …)`). Ephemeral. */
23926
+ this.nativePending = /* @__PURE__ */ new Map();
23927
+ /** VPVR-layer config pushed by the VPVR native indicator (null ⇒ layer off). Ephemeral. */
23928
+ this.vpvrLayer = null;
23929
+ this.crosshair = null;
23930
+ /** How the base price series is drawn on the price pane (candles by default). */
23931
+ this.priceStyle = "candles";
23932
+ /** Price-series base painting for the ACTIVE style (see ChartTypeDefinition.basePainting). */
23933
+ this.basePainting = "candles";
23934
+ /** The ACTIVE style's own candle cosmetics (`chartTypes.<id>.candle*`) when it is a
23935
+ * candle-based plugin type; null ⇒ paint with the shared `style.candle` block. */
23936
+ this.candleOverride = null;
23937
+ /** Explicit baseline reference price for `priceStyle:'baseline'`; when null the
23938
+ * baseline follows `style.baseline.baselineLevel` as a percent of the visible pane
23939
+ * range (resolved per frame via `baselinePriceFor`). */
23940
+ this.baselineValue = null;
23941
+ /** Draw the dashed horizontal line at the latest price (price pane). Independent
23942
+ * of the axis label chip (`showPriceLabel`) — either can show without the other. */
23943
+ this.showPriceLine = true;
23944
+ /** Draw the last-price label chip on the price axis. Independent of the line. */
23945
+ this.showPriceLabel = true;
23946
+ /** Draw the countdown-to-bar-close chip on the price axis. When the price label is
23947
+ * also shown, the two merge into one stacked block (countdown under the label);
23948
+ * when either shows alone it's centered on the latest price level. */
23949
+ this.showCountdown = true;
23950
+ /** Logarithmic price scale on the price pane. */
23951
+ this.logScale = false;
23952
+ /** Inverted price axis on the price pane (high at the bottom). Study panes carry their own. */
23953
+ this.invertScale = false;
23954
+ /** Exchange tick size for the active symbol (e.g. 0.01), when known. Drives the
23955
+ * price-axis decimals — the instrument's true precision instead of the zoom-derived
23956
+ * formula. Undefined until symbol metadata loads (the formula is the fallback). */
23957
+ this.priceMintick = void 0;
23958
+ /** Price-axis mode on the price pane: `'price'` (absolute) or `'percent'` (change
23959
+ * vs `percentBaseline`). Gridlines, axis labels and crosshair chip all follow it. */
23960
+ this.scaleMode = "price";
23961
+ /** Reference price for percent mode (first visible bar's close); recomputed per frame. */
23962
+ this.percentBaseline = 0;
23963
+ /** IANA time zone for the time axis + crosshair/data-window stamps (`'UTC'` default). */
23964
+ this.timezone = "UTC";
23965
+ /** Draw the background gridlines (price + time). Master toggle (`gridlines`
23966
+ * feature); per-axis visibility + colors live in `style.gridVert`/`gridHorz`. */
23967
+ this.showGrid = true;
23968
+ /** Comprehensive cosmetic config (item 15): grid colors, crosshair, candle
23969
+ * border/wick, fonts, separators. Serialized via the renderer's `getConfig()`/
23970
+ * `applyConfig()`; every draw layer reads its knobs from here, falling back to
23971
+ * the theme for any value left at its inherit default. */
23972
+ this.style = defaultChartStyle();
23973
+ /** Draw the price/time axis tick labels. */
23974
+ this.showAxisLabels = true;
23975
+ /** Strategy trade-marker display (the `tradeMarkers` feature): master toggle, the
23976
+ * two text lines, and the palette. Trade markers always paint on the price pane. */
23977
+ this.tradeMarkers = defaultTradeMarkersState();
23978
+ /** Renderer-owned shaded time bands (session highlighting), behind grid + data. */
23979
+ this.highlights = [];
23980
+ /** Pre/post-market bands pushed by the host (`sessionZones` feature); null ⇒ no sessions. */
23981
+ this.sessionZones = null;
23982
+ /** Draw-order key of the price candles, relative to indicator series z (see `seriesZ`).
23983
+ * Indicators with z below this draw BEHIND the candles; at/above draw in front.
23984
+ * Default 0 with indicators mounting at z < 0 ⇒ the price reads on top of every overlay,
23985
+ * and user drawings (z ≥ 1 by default) on top of the price. */
23986
+ this.candleZ = 0;
23987
+ /** Hide the base price series (candles/bars/line/area) without removing it — overlay
23988
+ * indicators keep drawing and the pane autoscales to them. Toggled from the object tree. */
23989
+ this.candlesHidden = false;
23990
+ /** Per-indicator foreground draw-order key (series layer), keyed by indicator id.
23991
+ * Higher = drawn later (in front). Assigned on mount to the current BOTTOM of the stack,
23992
+ * so each indicator arrives behind the candles (and behind older indicators);
23993
+ * `setIndicatorZ`/`bringToFront`/`sendToBack` change it. */
23994
+ this.seriesZ = /* @__PURE__ */ new Map();
23995
+ /** Per-pane raster layers of user drawings interleaved into the series stack — each is a
23996
+ * prepainted canvas the backend composites just before the series carrying `beforeZ`.
23997
+ * Rebuilt by the renderer per data frame; empty when every drawing sits over the stack. */
23998
+ this.drawingSlices = /* @__PURE__ */ new Map();
23999
+ /** Per-model index offset: the chart bar index of the model's `anchorTime` — its
24000
+ * index-aligned payloads (dense series arrays, `bar_index` drawings) count from that
24001
+ * bar. Only nonzero for models computed over a SUFFIX of the bars (whole-chart models,
24002
+ * the norm, aren't stored). Recomputed by the renderer on setBars + mount/patch. */
24003
+ this.anchorOffsets = /* @__PURE__ */ new Map();
24004
+ /** Per-indicator private price windows (merged indicators drawn on their own scale
24005
+ * column). Populated per frame for models flagged `ownScale`; absent ⇒ the model
24006
+ * shares its pane's master scale. */
24007
+ this.indicatorScales = /* @__PURE__ */ new Map();
24008
+ /** Cached sort of `panes` by order; invalidated on add/remove/reorder. */
24009
+ this.orderedCache = null;
24010
+ }
24011
+ /** Panes sorted top-to-bottom by `order`. Cached — callers must NOT mutate the array. */
24012
+ orderedPanes() {
24013
+ if (!this.orderedCache) this.orderedCache = [...this.panes.values()].sort((a, b) => a.order - b.order);
24014
+ return this.orderedCache;
24015
+ }
24016
+ /** The session zones resolved into colored bands (pre/post-market washes from the
24017
+ * config's session colors) — consumed by the same painting path as {@link highlights}. */
24018
+ sessionHighlightBands() {
24019
+ if (!this.sessionZones) return [];
24020
+ const out = [];
24021
+ for (const [from, to] of this.sessionZones.pre) out.push({ from, to, color: this.style.sessions.premarketColor });
24022
+ for (const [from, to] of this.sessionZones.post) out.push({ from, to, color: this.style.sessions.postmarketColor });
24023
+ return out;
24024
+ }
24025
+ indicatorsForPane(paneId) {
24026
+ const out = [];
24027
+ for (const model of this.indicators.values()) if (model.paneId === paneId) out.push(model);
24028
+ return out;
24029
+ }
24030
+ /** Merged (own-scale) indicators on a pane, ordered by z — one axis column each. */
24031
+ ownScaleIndicatorsForPane(paneId) {
24032
+ return this.orderedIndicatorsForPane(paneId).filter((m) => m.ownScale === true);
24033
+ }
24034
+ /** Ensure a merged indicator has a private scale slot (seeded from the pane if given). */
24035
+ ensureIndicatorScale(id, seed) {
24036
+ let s = this.indicatorScales.get(id);
24037
+ if (!s) {
24038
+ const base = seed ?? { min: 0, max: 1 };
24039
+ s = { scale: { ...base }, scaleTarget: { ...base }, initialized: false, manualScale: null };
24040
+ this.indicatorScales.set(id, s);
24041
+ }
24042
+ return s;
24043
+ }
24044
+ dropIndicatorScale(id) {
24045
+ this.indicatorScales.delete(id);
24046
+ }
24047
+ /** The price window a model renders on: its own scale when merged (`ownScale`), else the pane's. */
24048
+ scaleFor(model, pane) {
24049
+ if (model.ownScale === true) {
24050
+ const s = this.indicatorScales.get(model.id);
24051
+ if (s) return s.scale;
24052
+ }
24053
+ return pane.scale;
24054
+ }
24055
+ /** Apply a new top-to-bottom pane order (ids not present are ignored). */
24056
+ orderPanes(orderedIds) {
24057
+ orderedIds.forEach((id, i) => {
24058
+ const pane = this.panes.get(id);
24059
+ if (pane) pane.order = i;
24060
+ });
24061
+ this.orderedCache = null;
24062
+ }
24063
+ /** Indicators on a pane sorted by foreground z (ascending). Array#sort is stable,
24064
+ * so equal-z models keep their insertion order (the default). */
24065
+ orderedIndicatorsForPane(paneId) {
24066
+ return this.indicatorsForPane(paneId).sort((a, b) => this.zOf(a.id) - this.zOf(b.id));
24067
+ }
24068
+ /** The foreground draw-order key of an indicator (0 when never assigned). */
24069
+ zOf(id) {
24070
+ return this.seriesZ.get(id) ?? 0;
24071
+ }
24072
+ /** The model's index offset: chart bar index its index-aligned payloads count from (0 = whole-chart). */
24073
+ offsetOf(id) {
24074
+ return this.anchorOffsets.get(id) ?? 0;
24075
+ }
24076
+ /** Offsets are SIGNED. Positive: the model starts after the chart's first bar (it ran
24077
+ * over a suffix) — readers skip its leading chart bars. Negative: the model starts
24078
+ * BEFORE it (the chart's head moved forward under a mounted model) — readers skip the
24079
+ * model's own leading points, `points[i - off]` reaching further in. Storing only the
24080
+ * positive case silently pinned such a model at index 0, i.e. drew it shifted. */
24081
+ setAnchorOffset(id, offset) {
24082
+ if (offset !== 0 && Number.isFinite(offset)) this.anchorOffsets.set(id, offset);
24083
+ else this.anchorOffsets.delete(id);
24084
+ }
24085
+ forgetAnchorOffset(id) {
24086
+ this.anchorOffsets.delete(id);
24087
+ }
24088
+ /** Resolve the baseline reference price for the given pane window: the explicit
24089
+ * `baselineValue` when set, else `style.baseline.baselineLevel` as the price that sits
24090
+ * at that fraction of the pane height. Interpolated in the same space the pane renders
24091
+ * in (log when `scale.log`, else linear) so `level%` always lands at `level%` of the
24092
+ * height — matching `CoordinateSystem.yToPrice`. */
24093
+ baselinePriceFor(scale) {
24094
+ if (this.baselineValue != null) return this.baselineValue;
24095
+ const t = this.style.baseline.baselineLevel / 100;
24096
+ if (scale.log && scale.min > 0 && scale.max > scale.min) {
24097
+ const lo = Math.log(scale.min);
24098
+ return Math.exp(lo + t * (Math.log(scale.max) - lo));
24099
+ }
24100
+ return scale.min + (scale.max - scale.min) * t;
24101
+ }
24102
+ /** Assign a default z on mount: the current bottom of the stack, so a new indicator
24103
+ * paints behind the candles and behind every indicator already there — the price stays
24104
+ * the top of the pile until the user restacks it. No-op if the indicator already has one. */
24105
+ assignIndicatorZ(id) {
24106
+ if (!this.seriesZ.has(id)) this.seriesZ.set(id, this.bottomZ() - 1);
24107
+ }
24108
+ /** Mount-time default for a LAYER-BACKED native (it paints on a canvas stacked above the
24109
+ * data canvas by default): top of the stack, so the recorded order tells the truth from
24110
+ * the first frame. Keeps an existing key, so a restored stack survives the remount. */
24111
+ assignIndicatorZTop(id) {
24112
+ if (!this.seriesZ.has(id)) this.seriesZ.set(id, this.topZ() + 1);
24113
+ }
24114
+ forgetIndicatorZ(id) {
24115
+ this.seriesZ.delete(id);
24116
+ }
24117
+ setIndicatorZ(id, z) {
24118
+ this.seriesZ.set(id, z);
24119
+ }
24120
+ /** Snapshot of the current ordering for a UI/read API: `{ id, z }` sorted by z. */
24121
+ indicatorZOrder() {
24122
+ return [...this.seriesZ.entries()].map(([id, z]) => ({ id, z })).sort((a, b) => a.z - b.z);
24123
+ }
24124
+ /** The pane's series z keys (each indicator, plus the candles on the price pane), sorted
24125
+ * ascending and de-duplicated — the boundaries a user drawing's z is slotted against. */
24126
+ seriesBoundaries(paneId) {
24127
+ const keys = /* @__PURE__ */ new Set();
24128
+ if (paneId === "price") keys.add(this.candleZ);
24129
+ for (const m of this.indicatorsForPane(paneId)) keys.add(this.zOf(m.id));
24130
+ return [...keys].sort((a, b) => a - b);
24131
+ }
24132
+ /** Raise an indicator above every other layer (other indicators AND the candles). */
24133
+ bringIndicatorToFront(id) {
24134
+ this.seriesZ.set(id, this.topZ() + 1);
24135
+ }
24136
+ /** Drop an indicator below every other layer (other indicators AND the candles). */
24137
+ sendIndicatorToBack(id) {
24138
+ this.seriesZ.set(id, this.bottomZ() - 1);
24139
+ }
24140
+ topZ() {
24141
+ let max = this.candleZ;
24142
+ for (const z of this.seriesZ.values()) if (z > max) max = z;
24143
+ return max;
24144
+ }
24145
+ bottomZ() {
24146
+ let min = this.candleZ;
24147
+ for (const z of this.seriesZ.values()) if (z < min) min = z;
24148
+ return min;
24149
+ }
24150
+ ensurePane(id, kind, order, heightWeight) {
24151
+ this.orderedCache = null;
24152
+ const existing = this.panes.get(id);
24153
+ if (existing) {
24154
+ existing.order = order;
24155
+ existing.heightWeight = heightWeight;
24156
+ existing.kind = kind;
24157
+ return existing;
24158
+ }
24159
+ 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 };
24160
+ this.panes.set(id, pane);
24161
+ return pane;
24162
+ }
24163
+ removePane(id) {
24164
+ this.panes.delete(id);
24165
+ this.orderedCache = null;
24166
+ }
24167
+ };
24168
+ function paneScaleMode(scene, pane) {
24169
+ return pane.kind === "price" ? scene.scaleMode : pane.scaleMode ?? "price";
24170
+ }
24171
+ function paneLogScale(scene, pane) {
24172
+ return pane.kind === "price" ? scene.logScale : pane.logScale ?? false;
24173
+ }
24174
+ function paneInvert(scene, pane) {
24175
+ return pane.kind === "price" ? scene.invertScale : pane.invert ?? false;
24176
+ }
24177
+ function percentScaleFor(scene, pane) {
24178
+ const mode = paneScaleMode(scene, pane);
24179
+ if (mode !== "percent" && mode !== "indexed") return void 0;
24180
+ const baseline = pane.percentBaseline;
24181
+ if (!Number.isFinite(baseline) || baseline === 0) return void 0;
24182
+ return { baseline, indexed: mode === "indexed" };
24183
+ }
24184
+
24210
24185
  // src/renderers/native/backend/Canvas2dBackend.ts
24211
24186
  var Canvas2dBackend = class {
24212
24187
  constructor() {
@@ -24214,7 +24189,6 @@ var Canvas2dBackend = class {
24214
24189
  this.modelAlpha = 1;
24215
24190
  this.candleBodyAlpha = 1;
24216
24191
  this.candleStructureAlpha = 1;
24217
- this.gridAlpha = 1;
24218
24192
  this.candleBodyScale = 1;
24219
24193
  this.canvas = null;
24220
24194
  this.ctx = null;
@@ -24241,8 +24215,6 @@ var Canvas2dBackend = class {
24241
24215
  if (i1 < i0) return;
24242
24216
  const barColorMap = mergeBarColors2(scene.indicators);
24243
24217
  const panes = scene.orderedPanes();
24244
- this.drawHighlights(ctx, scene, coords);
24245
- this.drawGrid(ctx, scene, coords, theme, dataW);
24246
24218
  for (const pane of panes) {
24247
24219
  if (pane.collapsed) continue;
24248
24220
  const models = scene.orderedIndicatorsForPane(pane.id);
@@ -24849,22 +24821,6 @@ var Canvas2dBackend = class {
24849
24821
  ctx.fillStyle = bg.color;
24850
24822
  ctx.fillRect(x1, pane.bounds.top, x2 - x1, pane.bounds.height);
24851
24823
  }
24852
- /** Renderer-owned session highlight bands: full-height (all panes), behind grid + data.
24853
- * Session-zone washes (pre/post-market) paint first, host highlights on top. */
24854
- drawHighlights(ctx, scene, coords) {
24855
- const bands = [...scene.sessionHighlightBands(), ...scene.highlights];
24856
- if (bands.length === 0) return;
24857
- for (const band of bands) {
24858
- const x1 = coords.timeToX(band.from);
24859
- const x2 = coords.timeToX(band.to);
24860
- if (x2 < 0 || x1 > coords.width || x2 <= x1) continue;
24861
- const cx = Math.max(0, x1);
24862
- const cw = Math.min(coords.width, x2) - cx;
24863
- if (cw <= 0) continue;
24864
- ctx.fillStyle = band.color;
24865
- ctx.fillRect(cx, 0, cw, coords.height);
24866
- }
24867
- }
24868
24824
  drawHline(ctx, pl, pane, coords, dataW, theme) {
24869
24825
  const y = Math.round(coords.priceToY(pl.price, pane.scale, pane.bounds)) + 0.5;
24870
24826
  if (y < pane.bounds.top || y > pane.bounds.top + pane.bounds.height) return;
@@ -24877,46 +24833,6 @@ var Canvas2dBackend = class {
24877
24833
  ctx.stroke();
24878
24834
  setDash(ctx, "solid");
24879
24835
  }
24880
- // ── grid (L0, behind data) ── vert/horz gate on `scene.showGrid` AND their own
24881
- // per-axis visibility (style); each uses its own color. Pane separators are drawn on the
24882
- // chrome layer (full-width, above the data) so series never overpaint them.
24883
- drawGrid(ctx, scene, coords, theme, dataW) {
24884
- const panes = scene.orderedPanes();
24885
- const { gridVert, gridHorz } = scene.style;
24886
- const vertColor = gridVert.color ?? theme.gridColor;
24887
- const horzColor = gridHorz.color ?? theme.gridColor;
24888
- ctx.lineWidth = 1;
24889
- if (scene.showGrid && gridVert.visible) {
24890
- ctx.globalAlpha = this.gridAlpha;
24891
- ctx.strokeStyle = vertColor;
24892
- const tr = coords.visibleTimeRange();
24893
- const offset = tzOffsetMs((tr.from + tr.to) / 2, scene.timezone);
24894
- ctx.beginPath();
24895
- for (const tick of timeTicks(tr.from, tr.to, 8, offset)) {
24896
- const x = Math.round(coords.timeToX(tick.time)) + 0.5;
24897
- if (x < 0 || x > dataW) continue;
24898
- ctx.moveTo(x, 0);
24899
- ctx.lineTo(x, coords.height);
24900
- }
24901
- ctx.stroke();
24902
- }
24903
- for (const pane of panes) {
24904
- if (scene.showGrid && gridHorz.visible && !pane.collapsed) {
24905
- ctx.globalAlpha = this.gridAlpha;
24906
- ctx.strokeStyle = horzColor;
24907
- const pct = percentScaleFor(scene, pane);
24908
- ctx.beginPath();
24909
- for (const t of paneAxisTicks(pane.scale, pane.bounds.height, pct)) {
24910
- const y = Math.round(coords.priceToY(t.price, pane.scale, pane.bounds)) + 0.5;
24911
- if (y < pane.bounds.top || y > pane.bounds.top + pane.bounds.height) continue;
24912
- ctx.moveTo(0, y);
24913
- ctx.lineTo(dataW, y);
24914
- }
24915
- ctx.stroke();
24916
- }
24917
- }
24918
- ctx.globalAlpha = 1;
24919
- }
24920
24836
  };
24921
24837
  function setDash(ctx, style) {
24922
24838
  if (style === "dashed") ctx.setLineDash([6, 4]);
@@ -25628,6 +25544,221 @@ var DrawingSceneRenderer = class {
25628
25544
  }
25629
25545
  };
25630
25546
 
25547
+ // src/renderers/native/chrome/ticks.ts
25548
+ function priceTicks(min, max, target = 6) {
25549
+ if (!(max > min) || !Number.isFinite(min) || !Number.isFinite(max)) return [];
25550
+ const raw = (max - min) / Math.max(1, target);
25551
+ const mag = Math.pow(10, Math.floor(Math.log10(raw)));
25552
+ const norm = raw / mag;
25553
+ const step = (norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10) * mag;
25554
+ const decimals = Math.max(0, -Math.floor(Math.log10(step)) + 1);
25555
+ const out = [];
25556
+ const start = Math.ceil(min / step) * step;
25557
+ for (let v = start; v <= max + step * 1e-6; v += step) {
25558
+ out.push(Number(v.toFixed(decimals)));
25559
+ }
25560
+ return out;
25561
+ }
25562
+ function priceDecimals(min, max, target = 6) {
25563
+ if (!(max > min)) return 2;
25564
+ const raw = (max - min) / Math.max(1, target);
25565
+ const mag = Math.pow(10, Math.floor(Math.log10(raw)));
25566
+ const norm = raw / mag;
25567
+ const step = (norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10) * mag;
25568
+ return Math.max(0, Math.min(8, -Math.floor(Math.log10(step)) + 1));
25569
+ }
25570
+ function logPriceTicks(min, max, target = 6) {
25571
+ if (min <= 0 || !(max > min) || !Number.isFinite(min) || !Number.isFinite(max)) return [];
25572
+ if (Math.log10(max) - Math.log10(min) < 1.1) return priceTicks(min, max, target);
25573
+ const out = [];
25574
+ const startExp = Math.floor(Math.log10(min));
25575
+ const endExp = Math.ceil(Math.log10(max));
25576
+ for (let e = startExp; e <= endExp; e += 1) {
25577
+ for (const m of [1, 2, 5]) {
25578
+ const v = m * Math.pow(10, e);
25579
+ if (v >= min && v <= max) out.push(v);
25580
+ }
25581
+ }
25582
+ return out;
25583
+ }
25584
+ function valueDecimals(v) {
25585
+ const a = Math.abs(v);
25586
+ if (a >= 100) return 0;
25587
+ if (a >= 1) return 2;
25588
+ if (a >= 0.01) return 4;
25589
+ return 6;
25590
+ }
25591
+ function tickDecimals(tick) {
25592
+ if (!(tick > 0) || !Number.isFinite(tick)) return 2;
25593
+ for (let d = 0; d <= 8; d += 1) {
25594
+ if (Math.abs(Number(tick.toFixed(d)) - tick) <= tick * 1e-6) return d;
25595
+ }
25596
+ return 8;
25597
+ }
25598
+ function axisDecimals(scale, heightPx, mintick) {
25599
+ const d = mintick != null && mintick > 0 ? tickDecimals(mintick) : priceDecimals(scale.min, scale.max, tickCount(heightPx));
25600
+ return d === 0 ? 2 : d;
25601
+ }
25602
+ function tickCount(paneHeightPx) {
25603
+ return Math.max(2, Math.min(16, Math.round(paneHeightPx / 50)));
25604
+ }
25605
+ function paneTicks(scale, heightPx) {
25606
+ return scale.log ? logPriceTicks(scale.min, scale.max, tickCount(heightPx)) : priceTicks(scale.min, scale.max, tickCount(heightPx));
25607
+ }
25608
+ function toPct(price, baseline) {
25609
+ return (price / baseline - 1) * 100;
25610
+ }
25611
+ function toIndex(price, baseline) {
25612
+ return price / baseline * 100;
25613
+ }
25614
+ function formatPct(pct) {
25615
+ const sign = pct >= 0 ? "+" : "-";
25616
+ return `${sign}${Math.abs(pct).toFixed(2)}%`;
25617
+ }
25618
+ function formatIndex(idx) {
25619
+ return idx.toFixed(2);
25620
+ }
25621
+ function formatCompactValue(v) {
25622
+ const a = Math.abs(v);
25623
+ if (a >= 1e9) return `${trimZeros(v / 1e9)}B`;
25624
+ if (a >= 1e6) return `${trimZeros(v / 1e6)}M`;
25625
+ if (a >= 1e3) return `${trimZeros(v / 1e3)}K`;
25626
+ return trimZeros(v);
25627
+ }
25628
+ function trimZeros(v) {
25629
+ return Number(v.toFixed(2)).toString();
25630
+ }
25631
+ function paneAxisTicks(scale, heightPx, pct, mintick, format) {
25632
+ if (format === "none") return [];
25633
+ if (pct) {
25634
+ const { baseline, indexed } = pct;
25635
+ const lo = Math.min(scale.min, scale.max);
25636
+ const hi = Math.max(scale.min, scale.max);
25637
+ if (indexed) {
25638
+ const iLo = toIndex(lo, baseline);
25639
+ const iHi = toIndex(hi, baseline);
25640
+ return priceTicks(iLo, iHi, tickCount(heightPx)).map((idx) => ({ price: baseline * idx / 100, label: formatIndex(idx) }));
25641
+ }
25642
+ const pLo = toPct(lo, baseline);
25643
+ const pHi = toPct(hi, baseline);
25644
+ return priceTicks(pLo, pHi, tickCount(heightPx)).map((p) => ({ price: baseline * (1 + p / 100), label: formatPct(p) }));
25645
+ }
25646
+ if (format === "volume") {
25647
+ return paneTicks(scale, heightPx).map((price) => ({ price, label: formatCompactValue(price) }));
25648
+ }
25649
+ return paneTicks(scale, heightPx).map((price) => ({ price, label: formatPriceLabel(scale, heightPx, price, mintick) }));
25650
+ }
25651
+ function formatAxisValue(scale, heightPx, value, pct, mintick, format) {
25652
+ if (format === "none") return "";
25653
+ if (pct) return pct.indexed ? formatIndex(toIndex(value, pct.baseline)) : formatPct(toPct(value, pct.baseline));
25654
+ if (format === "volume") return formatCompactValue(value);
25655
+ return formatPriceLabel(scale, heightPx, value, mintick);
25656
+ }
25657
+ function formatPriceLabel(scale, heightPx, value, mintick) {
25658
+ const wideLog = scale.log && Math.log10(scale.max) - Math.log10(scale.min) >= 1.1;
25659
+ if (wideLog) {
25660
+ const d = valueDecimals(value);
25661
+ return value.toFixed(d === 0 ? 2 : d);
25662
+ }
25663
+ return value.toFixed(axisDecimals(scale, heightPx, mintick));
25664
+ }
25665
+ var SEC = 1e3;
25666
+ var MIN = 60 * SEC;
25667
+ var HOUR = 60 * MIN;
25668
+ var DAY = 24 * HOUR;
25669
+ var WEEK = 7 * DAY;
25670
+ var MONTH = 30 * DAY;
25671
+ var YEAR = 365 * DAY;
25672
+ var STEP_LADDER = [
25673
+ SEC,
25674
+ 5 * SEC,
25675
+ 15 * SEC,
25676
+ 30 * SEC,
25677
+ MIN,
25678
+ 5 * MIN,
25679
+ 15 * MIN,
25680
+ 30 * MIN,
25681
+ HOUR,
25682
+ 2 * HOUR,
25683
+ 4 * HOUR,
25684
+ 6 * HOUR,
25685
+ 12 * HOUR,
25686
+ DAY,
25687
+ 2 * DAY,
25688
+ WEEK,
25689
+ MONTH,
25690
+ 3 * MONTH,
25691
+ YEAR
25692
+ ];
25693
+ var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
25694
+ var pad2 = (n) => n < 10 ? `0${n}` : String(n);
25695
+ function pickStep(targetMs) {
25696
+ for (const step of STEP_LADDER) if (step >= targetMs) return step;
25697
+ return STEP_LADDER[STEP_LADDER.length - 1];
25698
+ }
25699
+ function timeTicks(fromMs, toMs, target = 8, offsetMs = 0) {
25700
+ const span = toMs - fromMs;
25701
+ if (!(span > 0)) return [];
25702
+ const step = pickStep(span / Math.max(1, target));
25703
+ const zFrom = fromMs + offsetMs;
25704
+ const zTo = toMs + offsetMs;
25705
+ const first = Math.ceil(zFrom / step) * step;
25706
+ const out = [];
25707
+ for (let zt = first; zt <= zTo; zt += step) {
25708
+ const t = zt - offsetMs;
25709
+ const d = new Date(zt);
25710
+ let label;
25711
+ let major = false;
25712
+ if (step < DAY) {
25713
+ const h = d.getUTCHours();
25714
+ const m = d.getUTCMinutes();
25715
+ if (h === 0 && m === 0) {
25716
+ label = `${MONTHS[d.getUTCMonth()]} ${d.getUTCDate()}`;
25717
+ major = true;
25718
+ } else {
25719
+ label = `${pad2(h)}:${pad2(m)}`;
25720
+ }
25721
+ } else if (step < YEAR) {
25722
+ label = `${MONTHS[d.getUTCMonth()]} ${d.getUTCDate()}`;
25723
+ if (d.getUTCDate() === 1) {
25724
+ label = MONTHS[d.getUTCMonth()];
25725
+ major = true;
25726
+ }
25727
+ } else {
25728
+ label = String(d.getUTCFullYear());
25729
+ major = true;
25730
+ }
25731
+ out.push({ time: t, label, major });
25732
+ }
25733
+ return out;
25734
+ }
25735
+
25736
+ // src/renderers/native/chrome/tz.ts
25737
+ function tzOffsetMs(ms, timeZone) {
25738
+ if (!timeZone || timeZone === "UTC") return 0;
25739
+ try {
25740
+ const dtf = new Intl.DateTimeFormat("en-US", {
25741
+ timeZone,
25742
+ hourCycle: "h23",
25743
+ year: "numeric",
25744
+ month: "2-digit",
25745
+ day: "2-digit",
25746
+ hour: "2-digit",
25747
+ minute: "2-digit",
25748
+ second: "2-digit"
25749
+ });
25750
+ const parts = dtf.formatToParts(new Date(ms));
25751
+ const get = (t) => Number(parts.find((p) => p.type === t)?.value);
25752
+ const asUTC = Date.UTC(get("year"), get("month") - 1, get("day"), get("hour") % 24, get("minute"), get("second"));
25753
+ return asUTC - ms;
25754
+ } catch {
25755
+ return 0;
25756
+ }
25757
+ }
25758
+ function zonedDate(ms, timeZone) {
25759
+ return new Date(ms + tzOffsetMs(ms, timeZone));
25760
+ }
25761
+
25631
25762
  // src/renderers/native/chrome/ChromeRenderer.ts
25632
25763
  var ChromeRenderer = class {
25633
25764
  constructor() {
@@ -25807,6 +25938,13 @@ var ChromeRenderer = class {
25807
25938
  if (y < pane.bounds.top + 6 || y > pane.bounds.top + pane.bounds.height - 4) continue;
25808
25939
  ctx.fillText(t.label, dataW + 6, y);
25809
25940
  }
25941
+ if (pane.axisBands) {
25942
+ for (const b of pane.axisBands) {
25943
+ const y = pane.bounds.top + b.frac * pane.bounds.height;
25944
+ if (y < pane.bounds.top + 6 || y > pane.bounds.top + pane.bounds.height - 4) continue;
25945
+ ctx.fillText(b.label, dataW + 6, y);
25946
+ }
25947
+ }
25810
25948
  }
25811
25949
  ctx.textAlign = "start";
25812
25950
  }
@@ -26048,7 +26186,7 @@ var CrosshairRenderer = class {
26048
26186
  }
26049
26187
  }
26050
26188
  const chipBg = cs.labelBackground ?? theme.borderColor;
26051
- if (pane) {
26189
+ if (pane && pane.axisFormat !== "none") {
26052
26190
  const price = coords.yToPrice(ch.y, pane.scale, pane.bounds);
26053
26191
  this.chip(ctx, dataW + 1, ch.y, formatAxisValue(pane.scale, pane.bounds.height, price, percentScaleFor(scene, pane), scene.priceMintick, pane.axisFormat), chipBg, "left", false, theme.background);
26054
26192
  }
@@ -26096,7 +26234,7 @@ var CrosshairRenderer = class {
26096
26234
  break;
26097
26235
  }
26098
26236
  }
26099
- if (pane) {
26237
+ if (pane && pane.axisFormat !== "none") {
26100
26238
  this.chip(ctx, dataW + 1, ext.y, formatAxisValue(pane.scale, pane.bounds.height, ext.price, percentScaleFor(scene, pane), scene.priceMintick, pane.axisFormat), chipBg, "left", false, theme.background);
26101
26239
  }
26102
26240
  }
@@ -26139,6 +26277,156 @@ function formatStamp(ms, timeZone) {
26139
26277
  return `${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}`;
26140
26278
  }
26141
26279
 
26280
+ // src/renderers/native/chrome/settings-visibility.ts
26281
+ function settingsIdSlug(label) {
26282
+ return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
26283
+ }
26284
+ function settingsIdHidden(id, hidden) {
26285
+ if (hidden.size === 0) return false;
26286
+ let path = id;
26287
+ for (; ; ) {
26288
+ if (hidden.has(path)) return true;
26289
+ const cut = path.lastIndexOf(".");
26290
+ if (cut < 0) return false;
26291
+ path = path.slice(0, cut);
26292
+ }
26293
+ }
26294
+ function settingsRowId(r) {
26295
+ if (r.kind === "heading" || r.kind === "header") return settingsIdSlug(r.label);
26296
+ const n = normalizeSettingsRow(r);
26297
+ if (n.toggle) return n.toggle.key;
26298
+ for (const c of n.controls) {
26299
+ if (c.kind !== "hint") return c.key;
26300
+ }
26301
+ return settingsIdSlug(n.label);
26302
+ }
26303
+ function filterHiddenRows(rows, scope, hidden) {
26304
+ if (hidden.size === 0) return [...rows];
26305
+ const out = [];
26306
+ let skipGroup = false;
26307
+ let skipSub = false;
26308
+ for (const r of rows) {
26309
+ const id = `${scope}.${settingsRowId(r)}`;
26310
+ if (r.kind === "heading") {
26311
+ skipGroup = settingsIdHidden(id, hidden);
26312
+ skipSub = false;
26313
+ if (!skipGroup) out.push(r);
26314
+ continue;
26315
+ }
26316
+ if (r.kind === "header") {
26317
+ if (skipGroup) continue;
26318
+ skipSub = settingsIdHidden(id, hidden);
26319
+ if (!skipSub) out.push(r);
26320
+ continue;
26321
+ }
26322
+ if (skipGroup || skipSub || settingsIdHidden(id, hidden)) continue;
26323
+ out.push(r);
26324
+ }
26325
+ return out;
26326
+ }
26327
+ function hostSectionId(s) {
26328
+ return s.id ?? settingsIdSlug(s.title);
26329
+ }
26330
+ function hostRowId(r) {
26331
+ return r.id ?? settingsIdSlug(r.label);
26332
+ }
26333
+ function filterHiddenHostRows(rows, scope, hidden) {
26334
+ if (hidden.size === 0) return [...rows];
26335
+ const out = [];
26336
+ let skipGroup = false;
26337
+ for (const r of rows) {
26338
+ const id = `${scope}.${hostRowId(r)}`;
26339
+ if (r.kind === "heading") {
26340
+ skipGroup = settingsIdHidden(id, hidden);
26341
+ if (!skipGroup) out.push(r);
26342
+ continue;
26343
+ }
26344
+ if (skipGroup || settingsIdHidden(id, hidden)) continue;
26345
+ out.push(r);
26346
+ }
26347
+ return out;
26348
+ }
26349
+ var BUILTIN_SETTINGS_IDS = [
26350
+ "symbol",
26351
+ "symbol.type",
26352
+ "symbol.style.candles",
26353
+ "symbol.style.candles.body",
26354
+ "symbol.style.candles.borders",
26355
+ "symbol.style.candles.wick",
26356
+ "symbol.style.candles.spacing",
26357
+ "symbol.style.bars",
26358
+ "symbol.style.bars.up-color",
26359
+ "symbol.style.bars.down-color",
26360
+ "symbol.style.bars.spacing",
26361
+ "symbol.style.line",
26362
+ "symbol.style.line.color",
26363
+ "symbol.style.line.width",
26364
+ "symbol.style.area",
26365
+ "symbol.style.area.line-color",
26366
+ "symbol.style.area.width",
26367
+ "symbol.style.area.top-fill",
26368
+ "symbol.style.area.bottom-fill",
26369
+ "symbol.style.baseline",
26370
+ "symbol.style.baseline.top-line",
26371
+ "symbol.style.baseline.bottom-line",
26372
+ "symbol.style.baseline.fill-top",
26373
+ "symbol.style.baseline.fill-bottom",
26374
+ "symbol.style.baseline.base-level",
26375
+ "symbol.style.baseline.width",
26376
+ "symbol.timezone",
26377
+ "scales",
26378
+ "scales.price-scale",
26379
+ "scales.price-scale.mode",
26380
+ "scales.price-scale.invert",
26381
+ "scales.price-scale.last-price-line",
26382
+ "scales.price-scale.last-price-label",
26383
+ "scales.price-scale.countdown",
26384
+ "scales.price-scale.axis-labels",
26385
+ "scales.price-scale.border-color",
26386
+ "scales.crosshair",
26387
+ "scales.crosshair.color",
26388
+ "scales.crosshair.width",
26389
+ "scales.crosshair.style",
26390
+ "canvas",
26391
+ "canvas.background",
26392
+ "canvas.background.color",
26393
+ "canvas.background.text-color",
26394
+ "canvas.background.text-size",
26395
+ "canvas.background.pane-separator",
26396
+ "canvas.grid",
26397
+ "canvas.grid.vertical",
26398
+ "canvas.grid.horizontal",
26399
+ "canvas.theme"
26400
+ ];
26401
+ function settingsIdCatalog(hostSections) {
26402
+ const ids = new Set(BUILTIN_SETTINGS_IDS);
26403
+ for (const def of chartTypes()) {
26404
+ if (hasOwnCandlePaint(def.id)) {
26405
+ const style = `symbol.style.${def.id}`;
26406
+ for (const leaf of ["", ".body", ".borders", ".wick", ".spacing"]) ids.add(style + leaf);
26407
+ }
26408
+ const section = def.settings;
26409
+ if (!section) continue;
26410
+ const scope = `type:${def.id}`;
26411
+ ids.add(scope);
26412
+ const addRows = (rows) => {
26413
+ for (const r of rows) ids.add(`${scope}.${settingsRowId(r)}`);
26414
+ };
26415
+ if (section.rows) addRows(section.rows);
26416
+ for (const inst of section.instances ?? []) addRows(inst.rows);
26417
+ for (const sub of section.subsections ?? []) {
26418
+ ids.add(`${scope}.${settingsIdSlug(sub.title)}`);
26419
+ addRows(sub.rows);
26420
+ }
26421
+ }
26422
+ for (const hs of hostSections) {
26423
+ const scope = hostSectionId(hs);
26424
+ ids.add(scope);
26425
+ for (const r of hs.rows) ids.add(`${scope}.${hostRowId(r)}`);
26426
+ }
26427
+ return [...ids];
26428
+ }
26429
+
26142
26430
  // src/renderers/native/chrome/SettingsDialog.ts
26143
26431
  var BUILTIN_STYLE_LABELS2 = {
26144
26432
  candles: "Candles",
@@ -26257,12 +26545,21 @@ var SettingsDialog = class {
26257
26545
  this.activeSection = null;
26258
26546
  /** Mobile chrome: fullscreen card, burger-opened section sidebar, TOC as top tabs. */
26259
26547
  this.mobileLayout = false;
26548
+ /** The visibility policy: setting ids hidden by the host (subtree semantics). */
26549
+ this.hiddenSettings = /* @__PURE__ */ new Set();
26260
26550
  if (getComputedStyle(container).position === "static") container.style.position = "relative";
26261
26551
  }
26262
26552
  /** Host-app sections (e.g. the widget's Status line tab) — re-shown on next open. */
26263
26553
  setHostSections(sections) {
26264
26554
  this.hostSections = sections;
26265
26555
  }
26556
+ /** Replace the visibility policy — an open dialog rebuilds in place to honor it. */
26557
+ setHiddenSettings(ids) {
26558
+ const next = new Set(ids);
26559
+ const same = next.size === this.hiddenSettings.size && [...next].every((id) => this.hiddenSettings.has(id));
26560
+ this.hiddenSettings = next;
26561
+ if (!same) this.reopenIfLive();
26562
+ }
26266
26563
  /** Configure the Canvas → Theme row (see {@link themeControl}); null hides the row. */
26267
26564
  setThemeControl(current, onSelect) {
26268
26565
  this.themeControl = { current, onSelect };
@@ -26330,8 +26627,12 @@ var SettingsDialog = class {
26330
26627
  }
26331
26628
  const body = document.createElement("div");
26332
26629
  body.style.cssText = "display:flex;flex-direction:column;gap:0;";
26333
- body.append(this.section("Symbol"));
26334
- body.append(this.sectionTitle("Chart type"));
26630
+ const sid = (el, id) => {
26631
+ el.dataset.sdId = id;
26632
+ return el;
26633
+ };
26634
+ body.append(sid(this.section("Symbol"), "symbol"));
26635
+ body.append(sid(this.sectionTitle("Chart type"), "symbol.type"));
26335
26636
  const groups = {};
26336
26637
  const showActive = (style) => {
26337
26638
  const active = style === "heikinashi" ? "candles" : style;
@@ -26340,64 +26641,64 @@ var SettingsDialog = class {
26340
26641
  }
26341
26642
  };
26342
26643
  body.append(
26343
- this.selectRowLabeled("Type", config.series.style, priceStyleIds().map((id) => [id, styleLabel(id)]), (v) => {
26644
+ sid(this.selectRowLabeled("Type", config.series.style, priceStyleIds().map((id) => [id, styleLabel(id)]), (v) => {
26344
26645
  this.emit({ series: { style: v } });
26345
26646
  showActive(v);
26346
26647
  this.syncTypeTabs?.(v);
26347
- })
26648
+ }), "symbol.type")
26348
26649
  );
26349
- const candles = this.group();
26650
+ const candles = sid(this.group(), "symbol.style.candles");
26350
26651
  candles.append(this.sectionTitle("Candles"));
26351
- candles.append(this.toggleRow("Body", config.candles.bodyVisible, (v) => this.emit({ candles: { bodyVisible: v } }), [
26652
+ candles.append(sid(this.toggleRow("Body", config.candles.bodyVisible, (v) => this.emit({ candles: { bodyVisible: v } }), [
26352
26653
  this.swatch(config.candles.upColor, (v) => this.emit({ candles: { upColor: v } })),
26353
26654
  this.swatch(config.candles.downColor, (v) => this.emit({ candles: { downColor: v } }))
26354
- ]));
26355
- candles.append(this.toggleRow("Borders", config.candles.borderVisible, (v) => this.emit({ candles: { borderVisible: v } }), [
26655
+ ]), "symbol.style.candles.body"));
26656
+ candles.append(sid(this.toggleRow("Borders", config.candles.borderVisible, (v) => this.emit({ candles: { borderVisible: v } }), [
26356
26657
  this.swatch(config.candles.borderUpColor, (v) => this.emit({ candles: { borderUpColor: v } })),
26357
26658
  this.swatch(config.candles.borderDownColor, (v) => this.emit({ candles: { borderDownColor: v } }))
26358
- ]));
26359
- candles.append(this.toggleRow("Wick", config.candles.wickVisible, (v) => this.emit({ candles: { wickVisible: v } }), [
26659
+ ]), "symbol.style.candles.borders"));
26660
+ candles.append(sid(this.toggleRow("Wick", config.candles.wickVisible, (v) => this.emit({ candles: { wickVisible: v } }), [
26360
26661
  this.swatch(config.candles.wickUpColor, (v) => this.emit({ candles: { wickUpColor: v } })),
26361
26662
  this.swatch(config.candles.wickDownColor, (v) => this.emit({ candles: { wickDownColor: v } }))
26362
- ]));
26363
- candles.append(this.numberRow("Spacing", config.series.spacing, 0.1, 10, 0.1, (v) => this.emit({ series: { spacing: v } })));
26663
+ ]), "symbol.style.candles.wick"));
26664
+ candles.append(sid(this.numberRow("Spacing", config.series.spacing, 0.1, 10, 0.1, (v) => this.emit({ series: { spacing: v } })), "symbol.style.candles.spacing"));
26364
26665
  groups.candles = candles;
26365
26666
  body.append(candles);
26366
- const bars = this.group();
26667
+ const bars = sid(this.group(), "symbol.style.bars");
26367
26668
  bars.append(this.sectionTitle("Bars"));
26368
- bars.append(this.colorRow("Color Up", config.bars.upColor, (v) => this.emit({ bars: { upColor: v } })));
26369
- bars.append(this.colorRow("Color Down", config.bars.downColor, (v) => this.emit({ bars: { downColor: v } })));
26370
- bars.append(this.numberRow("Spacing", config.series.spacing, 0.1, 10, 0.1, (v) => this.emit({ series: { spacing: v } })));
26669
+ bars.append(sid(this.colorRow("Color Up", config.bars.upColor, (v) => this.emit({ bars: { upColor: v } })), "symbol.style.bars.up-color"));
26670
+ bars.append(sid(this.colorRow("Color Down", config.bars.downColor, (v) => this.emit({ bars: { downColor: v } })), "symbol.style.bars.down-color"));
26671
+ bars.append(sid(this.numberRow("Spacing", config.series.spacing, 0.1, 10, 0.1, (v) => this.emit({ series: { spacing: v } })), "symbol.style.bars.spacing"));
26371
26672
  groups.bars = bars;
26372
26673
  body.append(bars);
26373
- const line = this.group();
26674
+ const line = sid(this.group(), "symbol.style.line");
26374
26675
  line.append(this.sectionTitle("Line"));
26375
- line.append(this.colorRow("Color", config.line.color, (v) => this.emit({ line: { color: v } })));
26376
- line.append(this.numberRow("Width", config.line.width, 1, 10, 1, (v) => this.emit({ line: { width: v } })));
26676
+ line.append(sid(this.colorRow("Color", config.line.color, (v) => this.emit({ line: { color: v } })), "symbol.style.line.color"));
26677
+ line.append(sid(this.numberRow("Width", config.line.width, 1, 10, 1, (v) => this.emit({ line: { width: v } })), "symbol.style.line.width"));
26377
26678
  groups.line = line;
26378
26679
  body.append(line);
26379
- const area = this.group();
26680
+ const area = sid(this.group(), "symbol.style.area");
26380
26681
  area.append(this.sectionTitle("Area"));
26381
- area.append(this.colorRow("Line color", config.area.lineColor, (v) => this.emit({ area: { lineColor: v } })));
26382
- area.append(this.numberRow("Width", config.area.width, 1, 10, 1, (v) => this.emit({ area: { width: v } })));
26383
- area.append(this.colorRow("Top fill", config.area.topColor, (v) => this.emit({ area: { topColor: v } })));
26384
- area.append(this.colorRow("Bottom fill", config.area.bottomColor, (v) => this.emit({ area: { bottomColor: v } })));
26682
+ area.append(sid(this.colorRow("Line color", config.area.lineColor, (v) => this.emit({ area: { lineColor: v } })), "symbol.style.area.line-color"));
26683
+ area.append(sid(this.numberRow("Width", config.area.width, 1, 10, 1, (v) => this.emit({ area: { width: v } })), "symbol.style.area.width"));
26684
+ area.append(sid(this.colorRow("Top fill", config.area.topColor, (v) => this.emit({ area: { topColor: v } })), "symbol.style.area.top-fill"));
26685
+ area.append(sid(this.colorRow("Bottom fill", config.area.bottomColor, (v) => this.emit({ area: { bottomColor: v } })), "symbol.style.area.bottom-fill"));
26385
26686
  groups.area = area;
26386
26687
  body.append(area);
26387
- const baseline = this.group();
26688
+ const baseline = sid(this.group(), "symbol.style.baseline");
26388
26689
  baseline.append(this.sectionTitle("Baseline"));
26389
- baseline.append(this.rowWith("Top line", [this.swatch(config.baseline.topLineColor, (v) => this.emit({ baseline: { topLineColor: v } }))]));
26390
- baseline.append(this.rowWith("Bottom line", [this.swatch(config.baseline.bottomLineColor, (v) => this.emit({ baseline: { bottomLineColor: v } }))]));
26391
- baseline.append(this.rowWith("Fill top area", [
26690
+ baseline.append(sid(this.rowWith("Top line", [this.swatch(config.baseline.topLineColor, (v) => this.emit({ baseline: { topLineColor: v } }))]), "symbol.style.baseline.top-line"));
26691
+ baseline.append(sid(this.rowWith("Bottom line", [this.swatch(config.baseline.bottomLineColor, (v) => this.emit({ baseline: { bottomLineColor: v } }))]), "symbol.style.baseline.bottom-line"));
26692
+ baseline.append(sid(this.rowWith("Fill top area", [
26392
26693
  this.swatch(config.baseline.topFillColor, (v) => this.emit({ baseline: { topFillColor: v } })),
26393
26694
  this.swatch(config.baseline.topFillColor2, (v) => this.emit({ baseline: { topFillColor2: v } }))
26394
- ]));
26395
- baseline.append(this.rowWith("Fill bottom area", [
26695
+ ]), "symbol.style.baseline.fill-top"));
26696
+ baseline.append(sid(this.rowWith("Fill bottom area", [
26396
26697
  this.swatch(config.baseline.bottomFillColor2, (v) => this.emit({ baseline: { bottomFillColor2: v } })),
26397
26698
  this.swatch(config.baseline.bottomFillColor, (v) => this.emit({ baseline: { bottomFillColor: v } }))
26398
- ]));
26399
- baseline.append(this.numberRow("Base level %", config.baseline.baselineLevel, 0, 100, 1, (v) => this.emit({ baseline: { baselineLevel: v } })));
26400
- baseline.append(this.numberRow("Width", config.baseline.width, 1, 10, 1, (v) => this.emit({ baseline: { width: v } })));
26699
+ ]), "symbol.style.baseline.fill-bottom"));
26700
+ baseline.append(sid(this.numberRow("Base level %", config.baseline.baselineLevel, 0, 100, 1, (v) => this.emit({ baseline: { baselineLevel: v } })), "symbol.style.baseline.base-level"));
26701
+ baseline.append(sid(this.numberRow("Width", config.baseline.width, 1, 10, 1, (v) => this.emit({ baseline: { width: v } })), "symbol.style.baseline.width"));
26401
26702
  groups.baseline = baseline;
26402
26703
  body.append(baseline);
26403
26704
  for (const def of chartTypes()) {
@@ -26405,32 +26706,36 @@ var SettingsDialog = class {
26405
26706
  const bag = config.chartTypes[def.id] ?? {};
26406
26707
  const colorOf2 = (key, shared) => typeof bag[key] === "string" && bag[key] !== "" ? bag[key] : shared;
26407
26708
  const boolOf = (key, shared) => typeof bag[key] === "boolean" ? bag[key] : shared;
26408
- const g = this.group();
26709
+ const g = sid(this.group(), `symbol.style.${def.id}`);
26409
26710
  g.append(this.sectionTitle("Candles"));
26410
- g.append(this.toggleRow("Body", boolOf("candleBodyVisible", config.candles.bodyVisible), (v) => this.emitType(def.id, "candleBodyVisible", v), [
26711
+ g.append(sid(this.toggleRow("Body", boolOf("candleBodyVisible", config.candles.bodyVisible), (v) => this.emitType(def.id, "candleBodyVisible", v), [
26411
26712
  this.swatch(colorOf2("candleUpColor", config.candles.upColor), (v) => this.emitType(def.id, "candleUpColor", v)),
26412
26713
  this.swatch(colorOf2("candleDownColor", config.candles.downColor), (v) => this.emitType(def.id, "candleDownColor", v))
26413
- ]));
26414
- g.append(this.toggleRow("Borders", boolOf("candleBorderVisible", config.candles.borderVisible), (v) => this.emitType(def.id, "candleBorderVisible", v), [
26714
+ ]), `symbol.style.${def.id}.body`));
26715
+ g.append(sid(this.toggleRow("Borders", boolOf("candleBorderVisible", config.candles.borderVisible), (v) => this.emitType(def.id, "candleBorderVisible", v), [
26415
26716
  this.swatch(colorOf2("candleBorderUpColor", config.candles.borderUpColor), (v) => this.emitType(def.id, "candleBorderUpColor", v)),
26416
26717
  this.swatch(colorOf2("candleBorderDownColor", config.candles.borderDownColor), (v) => this.emitType(def.id, "candleBorderDownColor", v))
26417
- ]));
26418
- g.append(this.toggleRow("Wick", boolOf("candleWickVisible", config.candles.wickVisible), (v) => this.emitType(def.id, "candleWickVisible", v), [
26718
+ ]), `symbol.style.${def.id}.borders`));
26719
+ g.append(sid(this.toggleRow("Wick", boolOf("candleWickVisible", config.candles.wickVisible), (v) => this.emitType(def.id, "candleWickVisible", v), [
26419
26720
  this.swatch(colorOf2("candleWickUpColor", config.candles.wickUpColor), (v) => this.emitType(def.id, "candleWickUpColor", v)),
26420
26721
  this.swatch(colorOf2("candleWickDownColor", config.candles.wickDownColor), (v) => this.emitType(def.id, "candleWickDownColor", v))
26421
- ]));
26422
- g.append(this.numberRow("Spacing", config.series.spacing, 0.1, 10, 0.1, (v) => this.emit({ series: { spacing: v } })));
26722
+ ]), `symbol.style.${def.id}.wick`));
26723
+ g.append(sid(this.numberRow("Spacing", config.series.spacing, 0.1, 10, 0.1, (v) => this.emit({ series: { spacing: v } })), `symbol.style.${def.id}.spacing`));
26423
26724
  groups[def.id] = g;
26424
26725
  body.append(g);
26425
26726
  }
26426
26727
  showActive(config.series.style);
26427
- body.append(this.sectionTitle("Time zone"));
26428
- body.append(this.selectRowLabeled("Time zone", normalizeTimezone(config.timeScale.timezone), timezoneOptions(config.timeScale.timezone), (v) => this.emit({ timeScale: { timezone: v } })));
26728
+ body.append(sid(this.sectionTitle("Time zone"), "symbol.timezone"));
26729
+ body.append(sid(this.selectRowLabeled("Time zone", normalizeTimezone(config.timeScale.timezone), timezoneOptions(config.timeScale.timezone), (v) => this.emit({ timeScale: { timezone: v } })), "symbol.timezone"));
26429
26730
  const renderHostSections = (placement) => {
26430
26731
  for (const hs of this.hostSections) {
26431
26732
  if ((hs.placement ?? "after-symbol") !== placement) continue;
26733
+ const scope = hostSectionId(hs);
26734
+ if (settingsIdHidden(scope, this.hiddenSettings)) continue;
26735
+ const rows = filterHiddenHostRows(hs.rows, scope, this.hiddenSettings);
26736
+ if (rows.length === 0) continue;
26432
26737
  body.append(placement === "symbol" ? this.sectionTitle(hs.title) : this.section(hs.title));
26433
- for (const hr of hs.rows) {
26738
+ for (const hr of rows) {
26434
26739
  if (hr.kind === "heading") body.append(this.sectionTitle(hr.label));
26435
26740
  else if (hr.kind === "toggle") body.append(this.boolRow(hr.label, hr.get(), (v) => hr.set(v)));
26436
26741
  else if (hr.kind === "color") body.append(this.colorRow(hr.label, hr.get(), (v) => hr.set(v)));
@@ -26443,53 +26748,69 @@ var SettingsDialog = class {
26443
26748
  const typeSettings = def.settings;
26444
26749
  if (!typeSettings) continue;
26445
26750
  if ((typeSettings.placement ?? "end") !== placement) continue;
26751
+ if (settingsIdHidden(`type:${def.id}`, this.hiddenSettings)) continue;
26446
26752
  this.chartTypeSection(def.id, typeSettings, config, body);
26447
26753
  }
26448
26754
  };
26449
26755
  renderHostSections("symbol");
26450
26756
  renderChartTypeSections("after-symbol");
26451
26757
  renderHostSections("after-symbol");
26452
- body.append(this.section("Scales and lines"));
26453
- body.append(this.sectionTitle("Price scale"));
26758
+ body.append(sid(this.section("Scales and lines"), "scales"));
26759
+ body.append(sid(this.sectionTitle("Price scale"), "scales.price-scale"));
26454
26760
  body.append(
26455
- this.selectRowLabeled(
26761
+ sid(this.selectRowLabeled(
26456
26762
  "Mode",
26457
26763
  config.priceScale.log ? "log" : config.priceScale.mode,
26458
26764
  [["price", "Regular"], ["percent", "Percent"], ["indexed", "Indexed to 100"], ["log", "Logarithmic"]],
26459
26765
  (v) => this.emit({ priceScale: v === "log" ? { mode: "price", log: true } : { mode: v, log: false } })
26460
- )
26766
+ ), "scales.price-scale.mode")
26461
26767
  );
26462
- body.append(this.boolRow("Invert scale", config.priceScale.invert, (v) => this.emit({ priceScale: { invert: v } })));
26463
- body.append(this.separator());
26464
- body.append(this.boolRow("Last Price Line", config.priceScale.currentPriceLine, (v) => this.emit({ priceScale: { currentPriceLine: v } })));
26465
- body.append(this.boolRow("Last price label", config.priceScale.priceLabel, (v) => this.emit({ priceScale: { priceLabel: v } })));
26466
- body.append(this.boolRow("Countdown to bar close", config.priceScale.countdown, (v) => this.emit({ priceScale: { countdown: v } })));
26467
- body.append(this.boolRow("Axis labels", config.priceScale.labelsVisible, (v) => this.emit({ priceScale: { labelsVisible: v } })));
26468
- body.append(this.colorRow("Scale border color", config.priceScale.borderColor, (v) => this.emit({ priceScale: { borderColor: v } })));
26469
- body.append(this.sectionTitle("Crosshair"));
26470
- body.append(this.colorRow("Color", config.crosshair.color, (v) => this.emit({ crosshair: { color: v } })));
26471
- body.append(this.numberRow("Width", config.crosshair.width, 0.5, 8, 0.5, (v) => this.emit({ crosshair: { width: v } })));
26472
- body.append(this.selectRowLabeled("Style", config.crosshair.style, [["solid", "Solid"], ["dashed", "Dashed"], ["dotted", "Dotted"]], (v) => this.emit({ crosshair: { style: v } })));
26473
- body.append(this.section("Canvas"));
26474
- body.append(this.sectionTitle("Background & text"));
26475
- body.append(this.colorRow("Background", config.layout.background, (v) => this.emit({ layout: { background: v } })));
26476
- body.append(this.colorRow("Text color", config.layout.textColor, (v) => this.emit({ layout: { textColor: v } })));
26477
- body.append(this.numberRow("Text size", config.layout.fontSize, 6, 32, 1, (v) => this.emit({ layout: { fontSize: v } })));
26478
- body.append(this.colorRow("Pane separator color", config.panes.separatorColor, (v) => this.emit({ panes: { separatorColor: v } })));
26479
- body.append(this.sectionTitle("Grid"));
26480
- body.append(this.toggleRow("Vertical", config.grid.vertLines.visible, (v) => this.emit({ grid: { vertLines: { visible: v } } }), [
26768
+ body.append(sid(this.boolRow("Invert scale", config.priceScale.invert, (v) => this.emit({ priceScale: { invert: v } })), "scales.price-scale.invert"));
26769
+ body.append(sid(this.separator(), "scales.price-scale"));
26770
+ body.append(sid(this.boolRow("Last Price Line", config.priceScale.currentPriceLine, (v) => this.emit({ priceScale: { currentPriceLine: v } })), "scales.price-scale.last-price-line"));
26771
+ body.append(sid(this.boolRow("Last price label", config.priceScale.priceLabel, (v) => this.emit({ priceScale: { priceLabel: v } })), "scales.price-scale.last-price-label"));
26772
+ body.append(sid(this.boolRow("Countdown to bar close", config.priceScale.countdown, (v) => this.emit({ priceScale: { countdown: v } })), "scales.price-scale.countdown"));
26773
+ body.append(sid(this.boolRow("Axis labels", config.priceScale.labelsVisible, (v) => this.emit({ priceScale: { labelsVisible: v } })), "scales.price-scale.axis-labels"));
26774
+ body.append(sid(this.colorRow("Scale border color", config.priceScale.borderColor, (v) => this.emit({ priceScale: { borderColor: v } })), "scales.price-scale.border-color"));
26775
+ body.append(sid(this.sectionTitle("Crosshair"), "scales.crosshair"));
26776
+ body.append(sid(this.colorRow("Color", config.crosshair.color, (v) => this.emit({ crosshair: { color: v } })), "scales.crosshair.color"));
26777
+ body.append(sid(this.numberRow("Width", config.crosshair.width, 0.5, 8, 0.5, (v) => this.emit({ crosshair: { width: v } })), "scales.crosshair.width"));
26778
+ body.append(sid(this.selectRowLabeled("Style", config.crosshair.style, [["solid", "Solid"], ["dashed", "Dashed"], ["dotted", "Dotted"]], (v) => this.emit({ crosshair: { style: v } })), "scales.crosshair.style"));
26779
+ body.append(sid(this.section("Canvas"), "canvas"));
26780
+ body.append(sid(this.sectionTitle("Background & text"), "canvas.background"));
26781
+ body.append(sid(this.colorRow("Background", config.layout.background, (v) => this.emit({ layout: { background: v } })), "canvas.background.color"));
26782
+ body.append(sid(this.colorRow("Text color", config.layout.textColor, (v) => this.emit({ layout: { textColor: v } })), "canvas.background.text-color"));
26783
+ body.append(sid(this.numberRow("Text size", config.layout.fontSize, 6, 32, 1, (v) => this.emit({ layout: { fontSize: v } })), "canvas.background.text-size"));
26784
+ body.append(sid(this.colorRow("Pane separator color", config.panes.separatorColor, (v) => this.emit({ panes: { separatorColor: v } })), "canvas.background.pane-separator"));
26785
+ body.append(sid(this.sectionTitle("Grid"), "canvas.grid"));
26786
+ body.append(sid(this.toggleRow("Vertical", config.grid.vertLines.visible, (v) => this.emit({ grid: { vertLines: { visible: v } } }), [
26481
26787
  this.swatch(config.grid.vertLines.color, (v) => this.emit({ grid: { vertLines: { color: v } } }))
26482
- ]));
26483
- body.append(this.toggleRow("Horizontal", config.grid.horzLines.visible, (v) => this.emit({ grid: { horzLines: { visible: v } } }), [
26788
+ ]), "canvas.grid.vertical"));
26789
+ body.append(sid(this.toggleRow("Horizontal", config.grid.horzLines.visible, (v) => this.emit({ grid: { horzLines: { visible: v } } }), [
26484
26790
  this.swatch(config.grid.horzLines.color, (v) => this.emit({ grid: { horzLines: { color: v } } }))
26485
- ]));
26791
+ ]), "canvas.grid.horizontal"));
26486
26792
  if (this.themeControl) {
26487
26793
  const tc = this.themeControl;
26488
- body.append(this.sectionTitle("Theme"));
26489
- body.append(this.selectRow("Color theme", tc.current === "dark" ? "Dark" : "Light", ["Dark", "Light"], (v) => tc.onSelect(v === "Dark" ? "dark" : "light")));
26794
+ body.append(sid(this.sectionTitle("Theme"), "canvas.theme"));
26795
+ body.append(sid(this.selectRow("Color theme", tc.current === "dark" ? "Dark" : "Light", ["Dark", "Light"], (v) => tc.onSelect(v === "Dark" ? "dark" : "light")), "canvas.theme"));
26490
26796
  }
26491
26797
  renderChartTypeSections("end");
26492
26798
  renderHostSections("end");
26799
+ if (this.hiddenSettings.size > 0) {
26800
+ let skipTab = false;
26801
+ for (const child of [...body.children]) {
26802
+ if (child.dataset.sdTab !== void 0) {
26803
+ skipTab = child.dataset.sdId !== void 0 && settingsIdHidden(child.dataset.sdId, this.hiddenSettings);
26804
+ }
26805
+ if (skipTab || child.dataset.sdId !== void 0 && settingsIdHidden(child.dataset.sdId, this.hiddenSettings)) {
26806
+ child.remove();
26807
+ continue;
26808
+ }
26809
+ for (const el of [...child.querySelectorAll("[data-sd-id]")]) {
26810
+ if (settingsIdHidden(el.dataset.sdId, this.hiddenSettings)) el.remove();
26811
+ }
26812
+ }
26813
+ }
26493
26814
  const shell = document.createElement("div");
26494
26815
  shell.style.cssText = mobile ? "display:flex;min-height:0;flex:1 1 auto;position:relative;overflow:hidden;" : "display:flex;min-height:360px;max-height:calc(70vh - 100px);flex:1 1 auto;";
26495
26816
  const rail = document.createElement("div");
@@ -26527,6 +26848,9 @@ var SettingsDialog = class {
26527
26848
  }
26528
26849
  if (current) current.appendChild(child);
26529
26850
  }
26851
+ for (let i = panes.length - 1; i >= 0; i--) {
26852
+ if (panes[i].el.childElementCount === 0) panes.splice(i, 1);
26853
+ }
26530
26854
  this.syncTypeTabs = (active) => {
26531
26855
  let hidActive = false;
26532
26856
  panes.forEach((pn, idx) => {
@@ -26666,19 +26990,25 @@ var SettingsDialog = class {
26666
26990
  this.emitType(typeId, key, v);
26667
26991
  for (const r of refreshers) r();
26668
26992
  };
26993
+ const scope = `type:${typeId}`;
26669
26994
  if (section.instances && section.instances.length > 0) {
26670
- body.append(this.instancesBlock(typeId, section.instances, bag, put, refreshers));
26995
+ const instances = section.instances.map((inst) => ({ ...inst, rows: filterHiddenRows(inst.rows, scope, this.hiddenSettings) }));
26996
+ body.append(this.instancesBlock(typeId, instances, bag, put, refreshers));
26671
26997
  } else if (section.rows) {
26672
- if (section.layout === "grouped") body.append(this.groupedRows(`${typeId}/rows`, section.rows, bag, put, refreshers));
26673
- else this.flatTypeRows(section.rows, bag, put, refreshers, body);
26998
+ const rows = filterHiddenRows(section.rows, scope, this.hiddenSettings);
26999
+ if (section.layout === "grouped") body.append(this.groupedRows(`${typeId}/rows`, rows, bag, put, refreshers));
27000
+ else this.flatTypeRows(rows, bag, put, refreshers, body);
26674
27001
  }
26675
27002
  for (const sub of section.subsections ?? []) {
27003
+ if (settingsIdHidden(`${scope}.${settingsIdSlug(sub.title)}`, this.hiddenSettings)) continue;
27004
+ const rows = filterHiddenRows(sub.rows, scope, this.hiddenSettings);
27005
+ if (rows.length === 0) continue;
26676
27006
  const subMarker = this.section(sub.title);
26677
27007
  subMarker.dataset.sdStyle = typeId;
26678
27008
  subMarker.dataset.sdVisibility = section.visibility ?? "active";
26679
27009
  subMarker.dataset.sdSub = "1";
26680
27010
  body.append(subMarker);
26681
- body.append(this.groupedRows(`${typeId}/${sub.title}`, sub.rows, bag, put, refreshers, sub.enableKey));
27011
+ body.append(this.groupedRows(`${typeId}/${sub.title}`, rows, bag, put, refreshers, sub.enableKey));
26682
27012
  }
26683
27013
  for (const r of refreshers) r();
26684
27014
  }
@@ -30968,6 +31298,94 @@ function resizeSplit(split, dyTotal, minPx = MIN_PANE_PX) {
30968
31298
  return { above, below: combinedWeight - above };
30969
31299
  }
30970
31300
 
31301
+ // src/renderers/native/backdrop/BackdropRenderer.ts
31302
+ var BackdropRenderer = class {
31303
+ constructor() {
31304
+ this.canvas = null;
31305
+ this.ctx = null;
31306
+ }
31307
+ mount(canvas) {
31308
+ this.canvas = canvas;
31309
+ this.ctx = canvas.getContext("2d");
31310
+ }
31311
+ destroy() {
31312
+ this.canvas = null;
31313
+ this.ctx = null;
31314
+ }
31315
+ /** Paint one frame: highlight bands first, gridlines on top (the order they had inside
31316
+ * the data canvas). `gridAlpha` fades the gridlines as a reveal-under layer opens. */
31317
+ render(scene, coords, theme, gridAlpha) {
31318
+ const ctx = this.ctx;
31319
+ const canvas = this.canvas;
31320
+ if (!ctx || !canvas) return;
31321
+ const dpr = coords.dpr;
31322
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
31323
+ ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
31324
+ const n = coords.barCount;
31325
+ if (n === 0) return;
31326
+ const vr = coords.visibleLogicalRange();
31327
+ if (Math.min(n - 1, Math.ceil(vr.to)) < Math.max(0, Math.floor(vr.from))) return;
31328
+ this.drawHighlights(ctx, scene, coords);
31329
+ this.drawGrid(ctx, scene, coords, theme, coords.width, gridAlpha);
31330
+ }
31331
+ /** Renderer-owned session highlight bands: full-height (all panes), behind the grid.
31332
+ * Session-zone washes (pre/post-market) paint first, host highlights on top. */
31333
+ drawHighlights(ctx, scene, coords) {
31334
+ const bands = [...scene.sessionHighlightBands(), ...scene.highlights];
31335
+ if (bands.length === 0) return;
31336
+ for (const band of bands) {
31337
+ const x1 = coords.timeToX(band.from);
31338
+ const x2 = coords.timeToX(band.to);
31339
+ if (x2 < 0 || x1 > coords.width || x2 <= x1) continue;
31340
+ const cx = Math.max(0, x1);
31341
+ const cw = Math.min(coords.width, x2) - cx;
31342
+ if (cw <= 0) continue;
31343
+ ctx.fillStyle = band.color;
31344
+ ctx.fillRect(cx, 0, cw, coords.height);
31345
+ }
31346
+ }
31347
+ // ── grid ── vert/horz gate on `scene.showGrid` AND their own per-axis visibility
31348
+ // (style); each uses its own color. Pane separators are drawn on the chrome layer
31349
+ // (full-width, above the data) so series never overpaint them.
31350
+ drawGrid(ctx, scene, coords, theme, dataW, gridAlpha) {
31351
+ const panes = scene.orderedPanes();
31352
+ const { gridVert, gridHorz } = scene.style;
31353
+ const vertColor = gridVert.color ?? theme.gridColor;
31354
+ const horzColor = gridHorz.color ?? theme.gridColor;
31355
+ ctx.lineWidth = 1;
31356
+ if (scene.showGrid && gridVert.visible) {
31357
+ ctx.globalAlpha = gridAlpha;
31358
+ ctx.strokeStyle = vertColor;
31359
+ const tr = coords.visibleTimeRange();
31360
+ const offset = tzOffsetMs((tr.from + tr.to) / 2, scene.timezone);
31361
+ ctx.beginPath();
31362
+ for (const tick of timeTicks(tr.from, tr.to, 8, offset)) {
31363
+ const x = Math.round(coords.timeToX(tick.time)) + 0.5;
31364
+ if (x < 0 || x > dataW) continue;
31365
+ ctx.moveTo(x, 0);
31366
+ ctx.lineTo(x, coords.height);
31367
+ }
31368
+ ctx.stroke();
31369
+ }
31370
+ for (const pane of panes) {
31371
+ if (scene.showGrid && gridHorz.visible && !pane.collapsed) {
31372
+ ctx.globalAlpha = gridAlpha;
31373
+ ctx.strokeStyle = horzColor;
31374
+ const pct = percentScaleFor(scene, pane);
31375
+ ctx.beginPath();
31376
+ for (const t of paneAxisTicks(pane.scale, pane.bounds.height, pct, void 0, pane.axisFormat)) {
31377
+ const y = Math.round(coords.priceToY(t.price, pane.scale, pane.bounds)) + 0.5;
31378
+ if (y < pane.bounds.top || y > pane.bounds.top + pane.bounds.height) continue;
31379
+ ctx.moveTo(0, y);
31380
+ ctx.lineTo(dataW, y);
31381
+ }
31382
+ ctx.stroke();
31383
+ }
31384
+ }
31385
+ ctx.globalAlpha = 1;
31386
+ }
31387
+ };
31388
+
30971
31389
  // src/renderers/native/volume/paintVolume.ts
30972
31390
  var VOLUME_FILL_ALPHA = 0.5;
30973
31391
  function paintVolume(ctx, bars, geom, colors) {
@@ -31443,6 +31861,7 @@ var NativeRenderer = class {
31443
31861
  // px reserved on the left for the docked drawings toolbar (0 when hidden)
31444
31862
  this.mountContainer = null;
31445
31863
  this.userDrawings = null;
31864
+ this.backdropRenderer = new BackdropRenderer();
31446
31865
  this.volumeRenderer = new VolumeRenderer();
31447
31866
  /** SDK renderer layers instantiated at mount ({@link registerRendererLayer}). */
31448
31867
  this.extLayers = [];
@@ -31570,6 +31989,9 @@ var NativeRenderer = class {
31570
31989
  // bar under the crosshair; null when off a bar
31571
31990
  // ── settings dialog (rich, serializable config — item 15) ──
31572
31991
  this.settingsDialog = null;
31992
+ /** The host's visibility policy (setting ids hidden from the dialog) — instance
31993
+ * state, never part of the persisted config. */
31994
+ this.hiddenSettings = [];
31573
31995
  /** Where modal dialogs mount — a HOST override (multi-chart shells pass their root
31574
31996
  * so dialogs center globally instead of clipping inside one cell). Null = the plot. */
31575
31997
  this.dialogHost = null;
@@ -32242,6 +32664,7 @@ var NativeRenderer = class {
32242
32664
  }
32243
32665
  this.settingsDialog.setTheme(this.theme);
32244
32666
  this.settingsDialog.setHostSections(this.hostSettingsSections);
32667
+ this.settingsDialog.setHiddenSettings(this.hiddenSettings);
32245
32668
  this.syncThemeControl();
32246
32669
  this.settingsDialog.toggle(
32247
32670
  this.getConfig(),
@@ -32489,6 +32912,8 @@ var NativeRenderer = class {
32489
32912
  this.wrapper = document.createElement("div");
32490
32913
  Object.assign(this.wrapper.style, { position: "relative", width: "100%", height: "100%", overflow: "hidden", cursor: "crosshair", userSelect: "none", webkitUserSelect: "none" });
32491
32914
  applyChromeTokens(this.wrapper, this.chromeTheme());
32915
+ this.backdropCanvas = document.createElement("canvas");
32916
+ Object.assign(this.backdropCanvas.style, { position: "absolute", inset: "0", width: "100%", height: "100%", pointerEvents: "none" });
32492
32917
  this.volumeCanvas = document.createElement("canvas");
32493
32918
  Object.assign(this.volumeCanvas.style, { position: "absolute", inset: "0", width: "100%", height: "100%", pointerEvents: "none" });
32494
32919
  this.dataCanvas = this.createGeometryBackend();
@@ -32511,7 +32936,7 @@ var NativeRenderer = class {
32511
32936
  });
32512
32937
  const below = this.extLayers.filter((l) => l.def.placement === "below-data").map((l) => l.canvas);
32513
32938
  const above = this.extLayers.filter((l) => l.def.placement !== "below-data").map((l) => l.canvas);
32514
- this.plot.append(...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above, this.chromeCanvas, this.drawingsCanvas, this.cursorCanvas, this.overlayRoot);
32939
+ this.plot.append(this.backdropCanvas, ...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above, this.chromeCanvas, this.drawingsCanvas, this.cursorCanvas, this.overlayRoot);
32515
32940
  this.layerOrderSig = "";
32516
32941
  this.wrapper.appendChild(this.plot);
32517
32942
  this.factoryConfig = this.getConfig();
@@ -32521,6 +32946,7 @@ var NativeRenderer = class {
32521
32946
  this.wrapper.appendChild(this.attributionEl);
32522
32947
  container.appendChild(this.wrapper);
32523
32948
  this.applyBackground();
32949
+ this.backdropRenderer.mount(this.backdropCanvas);
32524
32950
  this.volumeRenderer.mount(this.volumeCanvas);
32525
32951
  this.vpvrRenderer.mount(this.vpvrCanvas);
32526
32952
  for (const l of this.extLayers) l.instance.mount(l.canvas);
@@ -32810,6 +33236,7 @@ var NativeRenderer = class {
32810
33236
  this.scrollButton = null;
32811
33237
  for (const l of this.extLayers) l.instance.destroy?.();
32812
33238
  this.extLayers = [];
33239
+ this.backdropRenderer.destroy();
32813
33240
  this.volumeRenderer.destroy();
32814
33241
  this.vpvrRenderer.destroy();
32815
33242
  this.backend.destroy();
@@ -33189,6 +33616,13 @@ var NativeRenderer = class {
33189
33616
  this.hostSettingsSections = sections;
33190
33617
  this.settingsDialog?.setHostSections(sections);
33191
33618
  }
33619
+ setSettingsVisibility(policy) {
33620
+ this.hiddenSettings = [...policy.hidden ?? []];
33621
+ this.settingsDialog?.setHiddenSettings(this.hiddenSettings);
33622
+ }
33623
+ listSettingsIds() {
33624
+ return settingsIdCatalog(this.hostSettingsSections);
33625
+ }
33192
33626
  onChartTypeSettingsChange(cb) {
33193
33627
  this.chartTypeSettingsCbs.add(cb);
33194
33628
  return () => this.chartTypeSettingsCbs.delete(cb);
@@ -33895,7 +34329,7 @@ var NativeRenderer = class {
33895
34329
  this.backend.modelAlpha = this.modelAlpha;
33896
34330
  this.backend.candleBodyAlpha = this.candleBodyAlpha;
33897
34331
  this.backend.candleStructureAlpha = this.candleStructureAlpha;
33898
- this.backend.gridAlpha = 1;
34332
+ let gridAlpha = 1;
33899
34333
  const candleBodyScale = 1;
33900
34334
  this.backend.candleBodyScale = candleBodyScale;
33901
34335
  const pane = this.scene.panes.get(PRICE_PANE_ID2);
@@ -33934,7 +34368,7 @@ var NativeRenderer = class {
33934
34368
  if (folded) {
33935
34369
  if (folded.candleBodyScale != null) this.backend.candleBodyScale = clamp012(folded.candleBodyScale) || 0.01;
33936
34370
  if (folded.candleBodyAlpha != null) this.backend.candleBodyAlpha = clamp012(folded.candleBodyAlpha) * this.candleBodyAlpha;
33937
- if (folded.gridAlpha != null) this.backend.gridAlpha = clamp012(folded.gridAlpha);
34371
+ if (folded.gridAlpha != null) gridAlpha = clamp012(folded.gridAlpha);
33938
34372
  }
33939
34373
  }
33940
34374
  const li = this.bars.length - 1;
@@ -33942,6 +34376,7 @@ var NativeRenderer = class {
33942
34376
  const easeLive = !!liveActual && this.liveEaseTime === liveActual.time && (liveActual.high !== this.liveEaseHigh || liveActual.low !== this.liveEaseLow || liveActual.close !== this.liveEaseClose);
33943
34377
  if (easeLive && liveActual) this.bars[li] = { ...liveActual, high: this.liveEaseHigh, low: this.liveEaseLow, close: this.liveEaseClose };
33944
34378
  this.scene.drawingSlices = this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map();
34379
+ this.backdropRenderer.render(this.scene, this.coords, this.theme, gridAlpha);
33945
34380
  this.backend.render(this.scene, this.coords, this.theme);
33946
34381
  this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
33947
34382
  this.userDrawings?.render();
@@ -34084,6 +34519,7 @@ var NativeRenderer = class {
34084
34519
  pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
34085
34520
  pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
34086
34521
  pane.axisFormat = void 0;
34522
+ pane.axisBands = void 0;
34087
34523
  if (this.volumeOwnsPane(pane, masterModels)) {
34088
34524
  const maxVol = this.maxVisibleVolume(i0, i1);
34089
34525
  if (maxVol > 0) {
@@ -34093,6 +34529,11 @@ var NativeRenderer = class {
34093
34529
  } else if (this.layerNativesOwnPane(pane, masterModels)) {
34094
34530
  pane.scaleTarget = computePaneScale([], this.bars, true, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
34095
34531
  pane.percentBaseline = this.bars[i0]?.close ?? 0;
34532
+ if (masterModels.every((m) => m.paneAxis != null)) {
34533
+ pane.axisFormat = "none";
34534
+ const banded = masterModels.find((m) => typeof m.paneAxis === "object");
34535
+ pane.axisBands = banded ? banded.paneAxis.bands : void 0;
34536
+ }
34096
34537
  }
34097
34538
  if (pane === pricePane && this.scene.tradeMarkers.visible && pane.bounds.height > 0) {
34098
34539
  const th = this.tradesScaleHints(vr);
@@ -34213,11 +34654,11 @@ var NativeRenderer = class {
34213
34654
  );
34214
34655
  return { below: below.map((id) => byId.get(id)), above: above.map((id) => byId.get(id)) };
34215
34656
  }
34216
- /** The full canvas pile in paint order (layers + data/volume/vpvr) — what the DOM
34217
- * stacking and the screenshot compositor must both follow. */
34657
+ /** The full canvas pile in paint order (backdrop + layers + data/volume/vpvr) — what
34658
+ * the DOM stacking and the screenshot compositor must both follow. */
34218
34659
  canvasPile() {
34219
34660
  const { below, above } = this.orderedLayerCanvases();
34220
- return [...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above];
34661
+ return [this.backdropCanvas, ...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above];
34221
34662
  }
34222
34663
  /** Re-slot the SDK layer canvases in the plot when the computed order changed (a z
34223
34664
  * write, a restored config, an indicator mount/remove/restack). Runs at the top of
@@ -34534,6 +34975,8 @@ var NativeRenderer = class {
34534
34975
  const ph = h;
34535
34976
  this.dataCanvas.width = Math.round(pw * dpr);
34536
34977
  this.dataCanvas.height = Math.round(ph * dpr);
34978
+ this.backdropCanvas.width = this.dataCanvas.width;
34979
+ this.backdropCanvas.height = this.dataCanvas.height;
34537
34980
  this.volumeCanvas.width = this.dataCanvas.width;
34538
34981
  this.volumeCanvas.height = this.dataCanvas.height;
34539
34982
  for (const l of this.extLayers) {
@@ -34675,35 +35118,6 @@ function normalizeBars(bars) {
34675
35118
  return out;
34676
35119
  }
34677
35120
 
34678
- // src/core/price-styles/heikin-ashi.ts
34679
- function heikinAshiNext(raw, prevHa) {
34680
- const haClose = (raw.open + raw.high + raw.low + raw.close) / 4;
34681
- const haOpen = prevHa ? (prevHa.open + prevHa.close) / 2 : (raw.open + raw.close) / 2;
34682
- return {
34683
- time: raw.time,
34684
- open: haOpen,
34685
- high: Math.max(raw.high, haOpen, haClose),
34686
- low: Math.min(raw.low, haOpen, haClose),
34687
- close: haClose,
34688
- ...raw.volume != null ? { volume: raw.volume } : {}
34689
- };
34690
- }
34691
- function heikinAshiFull(raw) {
34692
- const out = new Array(raw.length);
34693
- let prev2;
34694
- for (let i = 0; i < raw.length; i += 1) {
34695
- prev2 = heikinAshiNext(raw[i], prev2);
34696
- out[i] = prev2;
34697
- }
34698
- return out;
34699
- }
34700
-
34701
- // src/chart-types/builtins.ts
34702
- var HEIKIN_ASHI = { full: heikinAshiFull, next: heikinAshiNext };
34703
- function registerBuiltinChartTypes() {
34704
- registerChartType({ id: "heikinashi", label: "Heikin Ashi", barTransform: HEIKIN_ASHI });
34705
- }
34706
-
34707
35121
  // src/core/native-indicators/volume/VolumeIndicator.ts
34708
35122
  var DEFAULT_UP = BULLISH;
34709
35123
  var DEFAULT_DOWN = BEARISH;
@@ -34900,6 +35314,7 @@ var Vela = class {
34900
35314
  // the volume indicator is on by default
34901
35315
  };
34902
35316
  this.rendererControl = new RendererControl(renderer);
35317
+ if (options.settings) this.rendererControl.setSettingsVisibility(options.settings);
34903
35318
  this.dataControl = new DataControl(feed);
34904
35319
  this.orchestrator = new EngineOrchestrator(element, renderer, feed, engines, config, this.dataControl);
34905
35320
  const defaults2 = rendererDefaults();
@@ -35374,8 +35789,9 @@ function statuslineInkOf(renderer, priceStyle) {
35374
35789
  }
35375
35790
  }
35376
35791
  var Statusline = class {
35377
- constructor(host, symbol) {
35792
+ constructor(host, symbol, iconFor) {
35378
35793
  this.host = host;
35794
+ this.iconFor = iconFor;
35379
35795
  this.parts = { name: true, market: true, ohlc: true, change: true };
35380
35796
  this.lastBar = null;
35381
35797
  this.hoverBar = null;
@@ -35399,7 +35815,7 @@ var Statusline = class {
35399
35815
  this.el.className = "vela-statusline";
35400
35816
  this.el.dataset.velaScreenshot = "1";
35401
35817
  const ticker = parseSymbol(symbol).ticker;
35402
- this.avatarEl = tickerIconEl(doc, baseOfTicker(ticker), ticker, "vela-sl-avatar");
35818
+ this.avatarEl = tickerIconEl(doc, baseOfTicker(ticker), ticker, "vela-sl-avatar", this.iconFor?.(symbol));
35403
35819
  this.symbolEl = doc.createElement("span");
35404
35820
  this.symbolEl.className = "vela-sl-symbol";
35405
35821
  this.symbolEl.textContent = ticker;
@@ -35420,7 +35836,7 @@ var Statusline = class {
35420
35836
  setSymbol(symbol) {
35421
35837
  const ticker = parseSymbol(symbol).ticker;
35422
35838
  this.symbolEl.textContent = ticker;
35423
- const fresh = tickerIconEl(this.el.ownerDocument, baseOfTicker(ticker), ticker, "vela-sl-avatar");
35839
+ const fresh = tickerIconEl(this.el.ownerDocument, baseOfTicker(ticker), ticker, "vela-sl-avatar", this.iconFor?.(symbol));
35424
35840
  this.avatarEl.replaceWith(fresh);
35425
35841
  this.avatarEl = fresh;
35426
35842
  this.fit();
@@ -36030,8 +36446,8 @@ function seedDefaults(opts) {
36030
36446
  };
36031
36447
  }
36032
36448
  function cellChartDefaults(opts) {
36033
- const { renderer, defaultLanguage, currentPriceLine, logScale, animations, glow, upColor, downColor, drawings } = opts;
36034
- return { renderer, defaultLanguage, currentPriceLine, logScale, animations, glow, upColor, downColor, drawings };
36449
+ const { renderer, defaultLanguage, currentPriceLine, logScale, animations, glow, upColor, downColor, drawings, settings } = opts;
36450
+ return { renderer, defaultLanguage, currentPriceLine, logScale, animations, glow, upColor, downColor, drawings, settings };
36035
36451
  }
36036
36452
  function cellDrawings(opt) {
36037
36453
  if (opt === false) return false;
@@ -36044,7 +36460,11 @@ var ChartCell = class {
36044
36460
  this.deps = deps;
36045
36461
  /** This cell's unified app+drawings undo timeline (the shared Ctrl+Z routes here). */
36046
36462
  this.history = new WidgetHistory(() => this.inner);
36047
- /** Live manifest-indicator instances on this cell (the SAME entry may repeat). */
36463
+ /** Live manifest-indicator instances on this cell (the SAME entry may repeat).
36464
+ * `external` marks instances added through the public seam (`ctx.addIndicator`)
36465
+ * rather than the shell manifest — they share the undo/redo and picker plumbing
36466
+ * but stay OUT of the persisted ledger (their names would never resolve against
36467
+ * the manifest); persisting them is their plugin's job (`registerStatePersistence`). */
36048
36468
  this.instances = [];
36049
36469
  /** The native-indicator catalog with this cell's live supported/present flags. */
36050
36470
  this.nativeCatalog = [];
@@ -36069,6 +36489,14 @@ var ChartCell = class {
36069
36489
  this.presentNatives = [];
36070
36490
  this.rangeBars = 0;
36071
36491
  this.pendingRange = null;
36492
+ /** Last symbol we toasted "no provider serves this" for — once per symbol (the
36493
+ * core re-reports on every provider-index settle). */
36494
+ this.unresolvedToasted = null;
36495
+ /** The cell's third-party state bag (`ext` of the persisted per-chart state) —
36496
+ * seeded from the boot/restored document, refreshed by handler `serialize` calls at
36497
+ * dehydrate time. Entries with no registered handler this session ride along
36498
+ * verbatim, so a document never loses a plugin's state in the plugin's absence. */
36499
+ this.extState = {};
36072
36500
  /** Indicator titles (this cell's in-chart legend rows) shown. */
36073
36501
  this.indicatorTitlesOn = true;
36074
36502
  /** Plot values beside this cell's legend titles shown. */
@@ -36135,9 +36563,16 @@ var ChartCell = class {
36135
36563
  this.pendingManifestNames = [...seed.indicators.manifest];
36136
36564
  }
36137
36565
  this.volumeIntent = seed.indicators ? seed.indicators.natives.includes("volume") : deps.volume;
36566
+ this.extState = { ...seed.ext ?? {} };
36138
36567
  this.inner.on("load:end", () => {
36139
36568
  this.volumeMayBePending = false;
36140
36569
  });
36570
+ this.inner.on("data:unresolved", ({ symbol: symbol2, providers }) => {
36571
+ if (this.unresolvedToasted === symbol2) return;
36572
+ this.unresolvedToasted = symbol2;
36573
+ const list = providers.length > 0 ? providers.join(", ") : "none";
36574
+ this.deps.toast(`No registered provider serves "${symbol2}" (registered: ${list})`, "error", 6e3);
36575
+ });
36141
36576
  this.inner.on("load:start", () => this.watermark?.setLoading(true));
36142
36577
  this.inner.on("load:end", () => this.watermark?.setLoading(false));
36143
36578
  const tz = deps.timezone();
@@ -36149,12 +36584,15 @@ var ChartCell = class {
36149
36584
  this.watermarkOn = seed.watermark ?? deps.watermark;
36150
36585
  this.watermark = deps.watermark ? new Watermark(this.host, symbol ?? "", seed.timeframe ?? "60") : null;
36151
36586
  if (!this.watermarkOn) this.watermark?.setVisible(false);
36152
- this.statusline = deps.statusline ? new Statusline(this.host, symbol ?? "") : null;
36587
+ this.statusline = deps.statusline ? new Statusline(this.host, symbol ?? "", (sym) => this.inner?.data.symbolIcon(sym)) : null;
36153
36588
  this.statusline?.setMeta(seed.timeframe ?? "60", this.state.provider ?? "");
36154
36589
  this.statusline?.onChart(this.inner);
36155
36590
  this.marketStatus = this.statusline ? new MarketStatusTracker((s) => this.statusline?.setMarketStatus(s)) : null;
36156
36591
  void this.inner.data.ready().then(() => {
36157
- if (this.inner && this.state.symbol) this.statusline?.setMeta(this.state.timeframe ?? "60", this.inner.data.displayPrefix(this.state.symbol) ?? this.state.provider ?? "");
36592
+ if (this.inner && this.state.symbol) {
36593
+ this.statusline?.setSymbol(this.state.symbol);
36594
+ this.statusline?.setMeta(this.state.timeframe ?? "60", this.inner.data.displayPrefix(this.state.symbol) ?? this.state.provider ?? "");
36595
+ }
36158
36596
  this.refreshSessionAvailable();
36159
36597
  if (this.inner && this.state.symbol) this.marketStatus?.track(this.inner.data, this.state.symbol);
36160
36598
  });
@@ -36298,11 +36736,13 @@ var ChartCell = class {
36298
36736
  const eth = "Extended hours (ETH)";
36299
36737
  const sessionSection = {
36300
36738
  title: "Trading session",
36739
+ id: "trading-session",
36301
36740
  placement: "symbol",
36302
36741
  rows: [
36303
36742
  {
36304
36743
  kind: "select",
36305
36744
  label: "Session",
36745
+ id: "session",
36306
36746
  options: [rth, eth],
36307
36747
  get: () => this.session === "extended" ? eth : rth,
36308
36748
  set: (v) => this.setSession(v === eth ? "extended" : "regular")
@@ -36310,12 +36750,14 @@ var ChartCell = class {
36310
36750
  {
36311
36751
  kind: "color",
36312
36752
  label: "Pre-market",
36753
+ id: "premarket-color",
36313
36754
  get: () => this.sessionShadeColor("premarketColor"),
36314
36755
  set: (v) => this.setSessionShadeColor("premarketColor", v)
36315
36756
  },
36316
36757
  {
36317
36758
  kind: "color",
36318
36759
  label: "Post-market",
36760
+ id: "postmarket-color",
36319
36761
  get: () => this.sessionShadeColor("postmarketColor"),
36320
36762
  set: (v) => this.setSessionShadeColor("postmarketColor", v)
36321
36763
  }
@@ -36323,11 +36765,13 @@ var ChartCell = class {
36323
36765
  };
36324
36766
  const advanced = {
36325
36767
  title: "Advanced",
36768
+ id: "advanced",
36326
36769
  placement: "end",
36327
36770
  rows: [
36328
36771
  {
36329
36772
  kind: "select",
36330
36773
  label: "Bars to fetch",
36774
+ id: "bars",
36331
36775
  options: ["500", "1000", "2000", "5000", "10000", "20000"],
36332
36776
  get: () => String(this.state.bars ?? 1e3),
36333
36777
  set: (v) => {
@@ -36340,11 +36784,13 @@ var ChartCell = class {
36340
36784
  };
36341
36785
  const watermarkSection = {
36342
36786
  title: "Watermark",
36787
+ id: "watermark",
36343
36788
  placement: "symbol",
36344
36789
  rows: [
36345
36790
  {
36346
36791
  kind: "toggle",
36347
36792
  label: "Symbol watermark",
36793
+ id: "visible",
36348
36794
  get: () => this.watermarkOn,
36349
36795
  set: (v) => this.setWatermarkVisible(v)
36350
36796
  }
@@ -36355,22 +36801,25 @@ var ChartCell = class {
36355
36801
  const sl = this.statusline;
36356
36802
  sections.push({
36357
36803
  title: "Status line",
36804
+ id: "status-line",
36358
36805
  rows: [
36359
- { kind: "heading", label: "Status line" },
36360
- { kind: "toggle", label: "Symbol name", get: () => sl.partVisible("name"), set: (v) => sl.setPartVisible("name", v) },
36361
- { kind: "toggle", label: "Market status", get: () => sl.partVisible("market"), set: (v) => sl.setPartVisible("market", v) },
36362
- { kind: "toggle", label: "OHLC values", get: () => sl.partVisible("ohlc"), set: (v) => sl.setPartVisible("ohlc", v) },
36363
- { kind: "toggle", label: "Bar change values", get: () => sl.partVisible("change"), set: (v) => sl.setPartVisible("change", v) },
36364
- { kind: "heading", label: "Indicators" },
36806
+ { kind: "heading", label: "Status line", id: "parts" },
36807
+ { kind: "toggle", label: "Symbol name", id: "name", get: () => sl.partVisible("name"), set: (v) => sl.setPartVisible("name", v) },
36808
+ { kind: "toggle", label: "Market status", id: "market", get: () => sl.partVisible("market"), set: (v) => sl.setPartVisible("market", v) },
36809
+ { kind: "toggle", label: "OHLC values", id: "ohlc", get: () => sl.partVisible("ohlc"), set: (v) => sl.setPartVisible("ohlc", v) },
36810
+ { kind: "toggle", label: "Bar change values", id: "change", get: () => sl.partVisible("change"), set: (v) => sl.setPartVisible("change", v) },
36811
+ { kind: "heading", label: "Indicators", id: "indicators" },
36365
36812
  {
36366
36813
  kind: "toggle",
36367
36814
  label: "Titles",
36815
+ id: "indicator-titles",
36368
36816
  get: () => this.indicatorTitlesOn,
36369
36817
  set: (v) => this.setIndicatorTitlesVisible(v)
36370
36818
  },
36371
36819
  {
36372
36820
  kind: "toggle",
36373
36821
  label: "Values",
36822
+ id: "indicator-values",
36374
36823
  get: () => this.indicatorValuesOn,
36375
36824
  set: (v) => this.setIndicatorValuesVisible(v)
36376
36825
  }
@@ -36423,6 +36872,7 @@ var ChartCell = class {
36423
36872
  /** Switch this cell's market in place (the chart instance survives). */
36424
36873
  setSymbol(symbol) {
36425
36874
  if (!this.inner || symbol === this.symbol) return;
36875
+ this.unresolvedToasted = null;
36426
36876
  void this.inner.setMarket({ symbol });
36427
36877
  }
36428
36878
  setTimeframe(timeframe) {
@@ -36513,6 +36963,41 @@ var ChartCell = class {
36513
36963
  for (const entry of list) if (entry.enabled) this.addManifestInstance(entry, { record: false });
36514
36964
  }
36515
36965
  }
36966
+ /**
36967
+ * Replace the indicator ledger: natives converge to the listed set (volume
36968
+ * included — removing it sticks, the core's auto-add respects the opt-out), and
36969
+ * manifest instances are re-created by name, held until the shared manifest
36970
+ * resolves. Convergence is state application, not user edits — nothing enters the
36971
+ * undo timeline.
36972
+ */
36973
+ applyIndicatorLedger(led) {
36974
+ const chart = this.inner;
36975
+ if (!chart) return;
36976
+ this.volumeIntent = led.natives.includes("volume");
36977
+ this.history.silently(() => {
36978
+ const present = chart.presentNativeIndicators();
36979
+ for (const type of led.natives) {
36980
+ if (!present.includes(type)) chart.addNativeIndicator(type);
36981
+ }
36982
+ for (const type of present) {
36983
+ if (!led.natives.includes(type)) chart.addNativeIndicator(type).remove();
36984
+ }
36985
+ for (const it of [...this.instances]) this.dropInstance(it);
36986
+ if (this.manifest.length > 0) {
36987
+ for (const name of led.manifest) {
36988
+ const entry = this.manifest.find((e) => e.name === name);
36989
+ if (entry) this.addManifestInstance(entry, { record: false });
36990
+ }
36991
+ this.pendingManifestNames = null;
36992
+ } else if (!this.deps.manifestSettled()) {
36993
+ this.pendingManifestNames = [...led.manifest];
36994
+ } else {
36995
+ this.pendingManifestNames = null;
36996
+ }
36997
+ });
36998
+ this.syncPresentNatives();
36999
+ this.refreshNativeCatalog();
37000
+ }
36516
37001
  /** The picker's library rows: supported natives first, then the manifest. */
36517
37002
  libraryRows() {
36518
37003
  return [
@@ -36542,10 +37027,20 @@ var ChartCell = class {
36542
37027
  if (index < present.length) this.removeNative(present[index].type);
36543
37028
  else this.removeInstance(index - present.length);
36544
37029
  }
37030
+ /**
37031
+ * Add a script indicator through the PUBLIC seam (`ctx.addIndicator`) — same undo/
37032
+ * redo and picker plumbing as a manifest entry, but flagged `external` so the
37033
+ * persisted ledger never records a name the manifest can't resolve (the plugin owns
37034
+ * persistence via `registerStatePersistence`). Recording follows the ambient mute:
37035
+ * a persistence handler's `restore` runs silently, a user-driven call records.
37036
+ */
37037
+ addExternalIndicator(entry) {
37038
+ this.addManifestInstance({ ...entry, enabled: true }, { external: true });
37039
+ }
36545
37040
  /** Add ONE instance of a manifest entry (repeatable — duplicates are legitimate). */
36546
37041
  addManifestInstance(entry, opts = {}) {
36547
37042
  if (this.destroyed) return;
36548
- const it = { entry, handle: this.addToChart(entry) };
37043
+ const it = { entry, handle: this.addToChart(entry), ...opts.external ? { external: true } : {} };
36549
37044
  this.instances.push(it);
36550
37045
  this.deps.onIndicatorsChanged(this.id);
36551
37046
  if (opts.record === false) return;
@@ -36626,12 +37121,97 @@ var ChartCell = class {
36626
37121
  return null;
36627
37122
  }
36628
37123
  }
37124
+ // ── third-party state (the `ext` seam) ──
37125
+ /** The cell-bound surface persistence handlers work against (built per call — the
37126
+ * widget-context rule; nothing here may be cached by a handler). Its add methods
37127
+ * are ALWAYS muted — a `restore` that fetches before adding escapes the sync mute
37128
+ * of {@link restorePersistedExt}, and a state application must never enter the
37129
+ * undo timeline, however late its continuation lands. */
37130
+ stateContext() {
37131
+ return {
37132
+ cellId: this.id,
37133
+ chart: this.chart,
37134
+ addIndicator: (entry) => this.history.silently(() => this.addExternalIndicator(entry)),
37135
+ addNativeIndicator: (type) => this.history.silently(() => this.addNative(type))
37136
+ };
37137
+ }
37138
+ /**
37139
+ * Run the registered cell-scope `restore` handlers against the cell's restored
37140
+ * `ext` bag — the workspace calls this AFTER the core state is in place (chart
37141
+ * alive and wired, indicator ledger converged). Muted: nothing a restore does
37142
+ * enters the undo timeline. Handlers only see keys the document carries; a failing
37143
+ * handler is contained (one broken plugin must not take the cell down).
37144
+ */
37145
+ restorePersistedExt() {
37146
+ if (this.destroyed) return;
37147
+ for (const h of statePersistenceHandlers("cell")) {
37148
+ if (!(h.key in this.extState)) continue;
37149
+ try {
37150
+ this.history.silently(() => h.restore(this.extState[h.key], this.stateContext()));
37151
+ } catch (err) {
37152
+ console.warn(`[vela] state persistence "${h.key}" restore failed:`, err);
37153
+ }
37154
+ }
37155
+ }
37156
+ /** Assemble the cell's `ext` bag: fresh handler snapshots merged OVER the preserved
37157
+ * entries — a key with no handler this session rides along verbatim; a registered
37158
+ * handler returning `undefined` withdraws its entry. */
37159
+ dehydrateExt() {
37160
+ const ext = { ...this.extState };
37161
+ for (const h of statePersistenceHandlers("cell")) {
37162
+ try {
37163
+ const value = h.serialize(this.stateContext());
37164
+ if (value === void 0) delete ext[h.key];
37165
+ else ext[h.key] = value;
37166
+ } catch (err) {
37167
+ console.warn(`[vela] state persistence "${h.key}" serialize failed:`, err);
37168
+ }
37169
+ }
37170
+ this.extState = ext;
37171
+ return Object.keys(ext).length > 0 ? ext : void 0;
37172
+ }
36629
37173
  // ── lifecycle ──
37174
+ /**
37175
+ * Apply a restored cell state IN PLACE — the chart instance survives (the market
37176
+ * switches via `setMarket`) while cosmetics, renderer config, drawings, and the
37177
+ * indicator ledger converge to the document. The workspace takes this path when a
37178
+ * state document lands on a grid of the same shape (async-storage boot, host
37179
+ * `applyState`), so chart references, indicator handles, event subscriptions, and
37180
+ * the cell host all stay valid.
37181
+ */
37182
+ rehydrate(cs) {
37183
+ if (!this.inner || this.destroyed) return;
37184
+ if (cs.priceStyle && cs.priceStyle !== this.priceStyle) this.setPriceStyle(cs.priceStyle);
37185
+ if (cs.watermark !== void 0 && cs.watermark !== this.watermarkOn) this.setWatermarkVisible(cs.watermark);
37186
+ if (cs.indicatorTitles !== void 0 && cs.indicatorTitles !== this.indicatorTitlesOn) this.setIndicatorTitlesVisible(cs.indicatorTitles);
37187
+ if (cs.indicatorValues !== void 0 && cs.indicatorValues !== this.indicatorValuesOn) this.setIndicatorValuesVisible(cs.indicatorValues);
37188
+ if (cs.rendererConfig != null) this.inner.renderer.applyConfig(cs.rendererConfig);
37189
+ if (cs.drawings != null) this.inner.drawings.fromJSON(cs.drawings);
37190
+ if (cs.indicators) this.applyIndicatorLedger(cs.indicators);
37191
+ this.extState = { ...cs.ext ?? {} };
37192
+ this.restorePersistedExt();
37193
+ const symbol = prefixedSymbol(cs);
37194
+ const session = normalizeSession(cs.session) ?? "regular";
37195
+ const bars = typeof cs.bars === "number" && Number.isFinite(cs.bars) && cs.bars > 0 ? cs.bars : 0;
37196
+ const next = {};
37197
+ if (symbol && symbol !== this.symbol) next.symbol = symbol;
37198
+ if (cs.timeframe && cs.timeframe !== this.timeframe) next.timeframe = cs.timeframe;
37199
+ if (session !== this.session) {
37200
+ this.state.session = session;
37201
+ next.session = session;
37202
+ }
37203
+ if (bars > 0 && bars !== this.state.bars) {
37204
+ this.state.bars = bars;
37205
+ next.bars = Math.max(bars, this.rangeBars);
37206
+ }
37207
+ if (Object.keys(next).length > 0) void this.inner.setMarket(next);
37208
+ }
36630
37209
  /** Snapshot everything the pool needs to restore this slot later. The market fields
36631
37210
  * come from the LIVE config (`chart.market`) — the requested identity — so a switch
36632
37211
  * still loading when the snapshot is taken (persist-on-close) is not lost. */
36633
37212
  dehydrate() {
36634
37213
  const live = this.inner?.market;
37214
+ const ext = this.inner ? this.dehydrateExt() : Object.keys(this.extState).length > 0 ? { ...this.extState } : void 0;
36635
37215
  return {
36636
37216
  ...this.state,
36637
37217
  ...live ? { symbol: live.symbol, provider: live.provider, timeframe: live.timeframe } : {},
@@ -36644,14 +37224,17 @@ var ChartCell = class {
36644
37224
  // Natives from the chart's SYNC registry read — an async catalog mirror here
36645
37225
  // lost unload-time saves, and the old empty-set fallbacks resurrected removed
36646
37226
  // indicators. Manifest names fall back to the restored ledger only until the
36647
- // shared manifest settles. See {@link indicatorLedger}.
37227
+ // shared manifest settles. See {@link indicatorLedger}. External instances
37228
+ // (`ctx.addIndicator`) stay out: their names would never resolve against the
37229
+ // manifest — their plugin persists them via the `ext` seam instead.
36648
37230
  indicators: indicatorLedger({
36649
37231
  present: this.inner ? this.inner.presentNativeIndicators() : [],
36650
- instanceNames: this.instances.map((it) => it.entry.name),
37232
+ instanceNames: this.instances.filter((it) => !it.external).map((it) => it.entry.name),
36651
37233
  pendingManifest: this.pendingManifestNames,
36652
37234
  manifestSettled: this.deps.manifestSettled(),
36653
37235
  volumePending: this.volumeMayBePending && this.volumeIntent
36654
- })
37236
+ }),
37237
+ ...ext ? { ext } : {}
36655
37238
  };
36656
37239
  }
36657
37240
  destroy() {
@@ -36688,6 +37271,9 @@ function buildContext(host) {
36688
37271
  togglePanel: (id, open2) => host.togglePanel(id, open2),
36689
37272
  host: host.root,
36690
37273
  toast: (message, kind) => host.toast(message, kind),
37274
+ addIndicator: (entry) => host.active()?.addExternalIndicator(entry),
37275
+ addNativeIndicator: (type) => host.active()?.addNative(type),
37276
+ stateChanged: () => host.stateDirty(),
36691
37277
  cells: host.cells().map((c) => ({ id: c.id, chart: c.chart, symbol: c.symbol, timeframe: c.timeframe })),
36692
37278
  activeCellId: active?.id ?? "",
36693
37279
  setActiveCell: (id) => host.setActiveCell(id)
@@ -37049,6 +37635,10 @@ var VelaWorkspace = class {
37049
37635
  /** Plot-local y of the last price-axis long-press (targets the pane under the finger). */
37050
37636
  this.priceScalePressY = 0;
37051
37637
  this.attachmentDisposers = /* @__PURE__ */ new Map();
37638
+ /** The document-level third-party state bag (`state.ext`) — seeded from the restored
37639
+ * document, refreshed by global-scope handler `serialize` calls at snapshot time.
37640
+ * Entries with no registered handler this session ride along verbatim. */
37641
+ this.extState = {};
37052
37642
  /** The single grid-wide attribution mark — re-inked on a live theme swap. */
37053
37643
  this.attributionMark = null;
37054
37644
  this.onRootKeydown = (ev) => this.routeTyping(ev);
@@ -37063,6 +37653,7 @@ var VelaWorkspace = class {
37063
37653
  state: () => ({ ...this.syncOpts })
37064
37654
  };
37065
37655
  registerBuiltinLayouts();
37656
+ registerBuiltinChartTypes();
37066
37657
  const hostEl = typeof container === "string" ? document.querySelector(container) : container;
37067
37658
  if (!hostEl) throw new Error(`VelaWorkspace: container not found: ${String(container)}`);
37068
37659
  this.opts = opts;
@@ -37086,8 +37677,15 @@ var VelaWorkspace = class {
37086
37677
  for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) {
37087
37678
  this.applySyncSetting(kind, sync?.[kind]);
37088
37679
  }
37089
- this.def = this.resolveLayout(boot?.layout && ensureLayout(boot.layout) ? boot.layout : opts.layout ?? "4");
37680
+ this.monoLayout = opts.layout === false;
37681
+ const optLayout = opts.layout === false || opts.layout === void 0 ? "4" : opts.layout;
37682
+ this.def = this.resolveLayout(this.monoLayout ? "1" : boot?.layout && ensureLayout(boot.layout) ? boot.layout : optLayout);
37683
+ this.alertCap = Math.max(1, opts.alertCap ?? ALERT_CAP);
37684
+ this.topbarComp = resolveTopbarComposition(opts.topbar);
37685
+ this.indicatorsOverride = topbarActionOverride("indicators");
37686
+ this.screenshotOverride = topbarActionOverride("screenshot");
37090
37687
  if (boot?.trackSizes) for (const [id, ts] of Object.entries(boot.trackSizes)) this.trackSizes.set(id, ts);
37688
+ if (boot?.ext) this.extState = { ...boot.ext };
37091
37689
  if (boot?.charts) for (const { id, ...cs } of boot.charts) this.pool.set(id, cs);
37092
37690
  this.order = boot?.charts ? boot.charts.map((c) => c.id) : declaredOrder(opts.cells);
37093
37691
  const bootActive = boot?.activeCellId ?? null;
@@ -37102,6 +37700,8 @@ var VelaWorkspace = class {
37102
37700
  });
37103
37701
  this.symbolPicker = new SymbolPicker({
37104
37702
  host: this.root,
37703
+ // Row icons come from each descriptor's OWNING provider (resolveSymbolIcon).
37704
+ iconFor: (d) => this.feed.symbolIconOf(d),
37105
37705
  onSelect: (ticker) => this.active.setSymbol(ticker),
37106
37706
  onOpenChange: (open2) => {
37107
37707
  if (open2) for (const cell of this.cells()) cell.chart.renderer.closeDialogs();
@@ -37109,7 +37709,7 @@ var VelaWorkspace = class {
37109
37709
  }
37110
37710
  });
37111
37711
  this.symbolPicker.setSource(() => this.feed.symbols());
37112
- this.indicatorPicker = opts.indicatorPicker !== false ? new IndicatorPicker({
37712
+ this.indicatorPicker = opts.indicatorPicker !== false && !this.indicatorsOverride && topbarHas(this.topbarComp, "indicators") ? new IndicatorPicker({
37113
37713
  host: this.root,
37114
37714
  library: () => this.active.libraryRows(),
37115
37715
  onChart: () => this.active.onChartRows(),
@@ -37128,6 +37728,9 @@ var VelaWorkspace = class {
37128
37728
  });
37129
37729
  this.topbar = new Topbar(this.root, {
37130
37730
  symbol: "",
37731
+ // RAW option, not the resolved lists — the bar distinguishes a host-declared
37732
+ // side (list is law) from a default one (an override's `order` may flow).
37733
+ composition: opts.topbar,
37131
37734
  onSymbolClick: () => this.symbolPicker.open(),
37132
37735
  ...picker ? { onIndicatorsClick: () => picker.open() } : {},
37133
37736
  onUndoClick: () => this.active.history.undo(),
@@ -37141,7 +37744,9 @@ var VelaWorkspace = class {
37141
37744
  onTimeframe: (tf) => this.setActiveTimeframe(tf),
37142
37745
  onTimeframeFavorite: (tf, on) => this.setTimeframeFavorite(tf, on),
37143
37746
  onPriceStyle: (style) => this.active.setPriceStyle(style),
37144
- layout: {
37747
+ // Single-chart mode: no layout block at all — the topbar renders no layout
37748
+ // button and no sync switches (see TopbarOptions.layout).
37749
+ layout: this.monoLayout ? void 0 : {
37145
37750
  current: this.def.id,
37146
37751
  // The picker composes dynamic layouts on its grid canvas; registered
37147
37752
  // presets the canvas cannot express (bespoke plugin areas) list as rows.
@@ -37166,8 +37771,11 @@ var VelaWorkspace = class {
37166
37771
  });
37167
37772
  const main = doc.createElement("div");
37168
37773
  main.className = "vela-ws-main";
37774
+ const toolbar = buildToolbar(opts.drawings);
37775
+ this.drawingsEnabled = opts.drawings !== false;
37776
+ this.toolbarDef = toolbar.definition;
37169
37777
  let toolbarHost = null;
37170
- if (opts.drawingToolbar !== false) {
37778
+ if (this.drawingsEnabled && toolbar.visible && opts.drawingToolbar !== false) {
37171
37779
  toolbarHost = doc.createElement("div");
37172
37780
  toolbarHost.className = "vela-ws-toolbar";
37173
37781
  main.appendChild(toolbarHost);
@@ -37180,13 +37788,13 @@ var VelaWorkspace = class {
37180
37788
  context: () => this.context(),
37181
37789
  changed: () => this.markStateDirty()
37182
37790
  });
37183
- this.objectTree = new ObjectTree(main);
37791
+ this.objectTree = new ObjectTree(main, (sym) => this.feed.symbolIcon(sym));
37184
37792
  this.dataWindow = new DataWindow(main);
37185
37793
  this.dock.addBuiltIn({ id: "dataWindow", title: "Data window", icon: "datawindow", order: 10, panel: this.dataWindow, onChart: (c) => this.dataWindow.onChart(c) });
37186
37794
  this.dock.addBuiltIn({ id: "objects", title: "Object tree", icon: "objects", order: 20, panel: this.objectTree, onChart: (c) => this.objectTree.onChart(c) });
37187
37795
  this.dock.refresh();
37188
37796
  this.root.appendChild(main);
37189
- this.toast = new Toast(this.gridEl);
37797
+ this.toastHost = new Toast(this.gridEl);
37190
37798
  const attribution = rendererDefaults().attribution;
37191
37799
  if (attribution !== false) {
37192
37800
  const background = resolveTheme(opts.theme).background;
@@ -37232,7 +37840,7 @@ var VelaWorkspace = class {
37232
37840
  }
37233
37841
  }
37234
37842
  ) : null;
37235
- this.drawToolbar?.setDefinition(defaultToolbar());
37843
+ this.drawToolbar?.setDefinition(this.toolbarDef);
37236
37844
  this.drawToolbar?.setVisible(true);
37237
37845
  this.drawToolbar?.setDrawingsSyncMode(!!this.syncOpts.drawings);
37238
37846
  this.bottombar = opts.bottombar !== false ? new Bottombar(this.root, {
@@ -37252,13 +37860,15 @@ var VelaWorkspace = class {
37252
37860
  timeframe: "60",
37253
37861
  onSymbolClick: () => this.symbolPicker.open(),
37254
37862
  onTimeframeClick: () => this.openTimeframeDrawer(),
37255
- ...picker ? { onIndicatorsClick: () => picker.open() } : {},
37863
+ // Same visibility truth as the desktop bar: composition-hidden
37864
+ // indicators lose their mobile stop too; an override takes it over.
37865
+ ...topbarHas(this.topbarComp, "indicators") && (picker || this.indicatorsOverride) ? { onIndicatorsClick: this.indicatorsOverride ? () => this.runOverride(this.indicatorsOverride) : () => picker.open() } : {},
37256
37866
  getContext: () => this.context(),
37257
- onDrawingsClick: () => this.openDrawingsDrawer(),
37867
+ ...this.drawingsEnabled ? { onDrawingsClick: () => this.openDrawingsDrawer() } : {},
37258
37868
  onMoreClick: () => this.openMoreDrawer(),
37259
37869
  onSettingsClick: () => this.active.chart.renderer.openSettings()
37260
37870
  }) : null;
37261
- this.drawingPill = new DrawingPill(this.gridEl);
37871
+ this.drawingPill = this.drawingsEnabled ? new DrawingPill(this.gridEl) : null;
37262
37872
  hostEl.appendChild(this.root);
37263
37873
  this.layoutCtl = new LayoutModeController(this.root, opts.layoutMode ?? "auto");
37264
37874
  this.layoutCtl.onChange((mode) => this.onLayoutModeChange(mode));
@@ -37297,6 +37907,7 @@ var VelaWorkspace = class {
37297
37907
  this.manifestSettled = true;
37298
37908
  }
37299
37909
  this.mountAttachments();
37910
+ this.restoreGlobalExt();
37300
37911
  }
37301
37912
  // ── access ──────────────────────────────────────────────────
37302
37913
  /** The cell with identity `id` (its declared name, or `c<N>` when undeclared), or
@@ -37358,7 +37969,8 @@ var VelaWorkspace = class {
37358
37969
  openSymbolSearch: (query) => this.symbolPicker.open(query ?? ""),
37359
37970
  togglePanel: (id, open2) => this.dock.toggle(id, open2),
37360
37971
  root: this.root,
37361
- toast: (message, kind) => this.toast.show(message, kind)
37972
+ toast: (message, kind) => this.toastHost.show(message, kind),
37973
+ stateDirty: () => this.markStateDirty()
37362
37974
  });
37363
37975
  }
37364
37976
  /** Re-project contributed topbar actions + side panels, and mount late-registered attachments. */
@@ -37397,22 +38009,35 @@ var VelaWorkspace = class {
37397
38009
  if (this.trackSizes.size > 0) state.trackSizes = Object.fromEntries([...this.trackSizes].map(([k, v]) => [k, { ...v }]));
37398
38010
  const panels2 = this.dock.getState();
37399
38011
  if (panels2) state.panels = panels2;
38012
+ const ext = { ...this.extState };
38013
+ for (const h of statePersistenceHandlers("global")) {
38014
+ try {
38015
+ const value = h.serialize(this.context());
38016
+ if (value === void 0) delete ext[h.key];
38017
+ else ext[h.key] = value;
38018
+ } catch (err) {
38019
+ console.warn(`[vela] state persistence "${h.key}" serialize failed:`, err);
38020
+ }
38021
+ }
38022
+ this.extState = ext;
38023
+ if (Object.keys(ext).length > 0) state.ext = ext;
37400
38024
  return state;
37401
38025
  }
37402
38026
  /**
37403
38027
  * Restore a state document produced by {@link getState} (untrusted-safe: malformed
37404
- * fields are dropped). Replaces the WHOLE workspace state: prefs, sync links,
37405
- * layout, and every slot — current cells are rebuilt from the document. A layout id
37406
- * that is not registered keeps the current grid (register custom layouts first).
38028
+ * fields are dropped). When the document matches the live grid one-to-one — same
38029
+ * layout, same ordered slot identities it is applied IN PLACE: every chart
38030
+ * instance survives (markets switch via `setMarket`), so chart references,
38031
+ * indicator handles, and event subscriptions stay valid. Any structural difference
38032
+ * (layout, slot count, renamed ids) falls back to the full rebuild: prefs, sync
38033
+ * links, layout, and every slot are replaced, current cells rebuilt from the
38034
+ * document. A layout id that is not registered keeps the current grid (register
38035
+ * custom layouts first).
37407
38036
  */
37408
38037
  applyState(state) {
37409
38038
  if (this.destroyed) return;
37410
38039
  const st = sanitizeState(state);
37411
38040
  if (!st) return;
37412
- if (st.timezone) {
37413
- this.timezone = st.timezone;
37414
- this.bottombar?.setTimezone(st.timezone);
37415
- }
37416
38041
  if (st.favorites) this.favs = [...st.favorites];
37417
38042
  if (st.timeframeFavorites) {
37418
38043
  this.tfFavs = [...st.timeframeFavorites];
@@ -37422,6 +38047,36 @@ var VelaWorkspace = class {
37422
38047
  for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) this.applySyncSetting(kind, st.sync?.[kind]);
37423
38048
  this.trackSizes.clear();
37424
38049
  if (st.trackSizes) for (const [id, ts] of Object.entries(st.trackSizes)) this.trackSizes.set(id, ts);
38050
+ const targetDef = this.monoLayout ? this.def : ensureLayout(st.layout) ?? this.def;
38051
+ const liveCount = this.def.cells.length;
38052
+ 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]));
38053
+ if (inPlace) {
38054
+ if (st.favorites) {
38055
+ for (const cell of this.cellsById.values()) cell.chart.drawings.setFavorites(this.favs);
38056
+ }
38057
+ this.drawingLinks.clear();
38058
+ for (const [i] of this.def.cells.entries()) {
38059
+ const { id, ...cs } = st.charts[i];
38060
+ this.cellsById.get(id)?.rehydrate(cs);
38061
+ }
38062
+ if (st.timezone) this.setTimezone(st.timezone);
38063
+ this.pool.clear();
38064
+ for (const { id, ...cs } of st.charts.slice(liveCount)) this.pool.set(id, cs);
38065
+ this.order = st.charts.map((c) => c.id);
38066
+ this.applyGrid();
38067
+ const nextActive2 = st.activeCellId && this.cellsById.has(st.activeCellId) ? st.activeCellId : this.order[0] ?? null;
38068
+ if (nextActive2 === this.activeId) this.projectActiveCell();
38069
+ else this.setActiveCell(nextActive2);
38070
+ this.refreshRetention();
38071
+ this.extState = { ...st.ext ?? {} };
38072
+ this.restoreGlobalExt();
38073
+ this.markStateDirty();
38074
+ return;
38075
+ }
38076
+ if (st.timezone) {
38077
+ this.timezone = st.timezone;
38078
+ this.bottombar?.setTimezone(st.timezone);
38079
+ }
37425
38080
  for (const [id, cell] of [...this.cellsById]) {
37426
38081
  cell.destroy();
37427
38082
  this.cellsById.delete(id);
@@ -37431,7 +38086,7 @@ var VelaWorkspace = class {
37431
38086
  this.drawingLinks.clear();
37432
38087
  for (const { id, ...cs } of st.charts) this.pool.set(id, cs);
37433
38088
  this.order = st.charts.map((c) => c.id);
37434
- const def = ensureLayout(st.layout);
38089
+ const def = this.monoLayout ? null : ensureLayout(st.layout);
37435
38090
  if (def) this.def = def;
37436
38091
  this.cellBackend = this.backendFor(this.def);
37437
38092
  this.applyGrid();
@@ -37443,8 +38098,24 @@ var VelaWorkspace = class {
37443
38098
  else this.setActiveCell(nextActive);
37444
38099
  this.refreshRetention();
37445
38100
  this.events.emit("layout:changed", { layout: this.def.id });
38101
+ this.extState = { ...st.ext ?? {} };
38102
+ this.restoreGlobalExt();
37446
38103
  this.markStateDirty();
37447
38104
  }
38105
+ /** Run the registered global-scope `restore` handlers against the document-level
38106
+ * `ext` bag — keys present in the document only; a failing handler is contained.
38107
+ * Handlers whose restore touches chart content should be `scope: 'cell'` instead
38108
+ * (those run inside the cell's history-mute). */
38109
+ restoreGlobalExt() {
38110
+ for (const h of statePersistenceHandlers("global")) {
38111
+ if (!(h.key in this.extState)) continue;
38112
+ try {
38113
+ h.restore(this.extState[h.key], this.context());
38114
+ } catch (err) {
38115
+ console.warn(`[vela] state persistence "${h.key}" restore failed:`, err);
38116
+ }
38117
+ }
38118
+ }
37448
38119
  /** Set the workspace-global display timezone — applied to EVERY cell. */
37449
38120
  setTimezone(zone) {
37450
38121
  this.timezone = zone;
@@ -37480,6 +38151,7 @@ var VelaWorkspace = class {
37480
38151
  */
37481
38152
  setLayout(layout) {
37482
38153
  if (this.destroyed) return;
38154
+ if (this.monoLayout) return;
37483
38155
  const next = this.resolveLayout(layout);
37484
38156
  const nextBackend = this.backendFor(next);
37485
38157
  const rebuildAll = nextBackend !== this.cellBackend;
@@ -37509,6 +38181,12 @@ var VelaWorkspace = class {
37509
38181
  resize() {
37510
38182
  this.splitters.layout();
37511
38183
  }
38184
+ /** Show a toast over the grid — the same surface the shell's own notices use
38185
+ * (alerts, script errors) and the one contributions reach via `ctx.toast`. */
38186
+ toast(message, kind = "info", durationMs = 3e3) {
38187
+ if (this.destroyed) return;
38188
+ this.toastHost.show(message, kind, durationMs);
38189
+ }
37512
38190
  destroy() {
37513
38191
  if (this.destroyed) return;
37514
38192
  this.persistNow();
@@ -37534,7 +38212,7 @@ var VelaWorkspace = class {
37534
38212
  this.topbar.destroy();
37535
38213
  this.bottombar?.destroy();
37536
38214
  this.mobileBar?.destroy();
37537
- this.drawingPill.destroy();
38215
+ this.drawingPill?.destroy();
37538
38216
  this.tfDrawer?.destroy();
37539
38217
  this.drawingsDrawer?.destroy();
37540
38218
  this.moreDrawer?.destroy();
@@ -37548,10 +38226,10 @@ var VelaWorkspace = class {
37548
38226
  this.indicatorPicker?.destroy();
37549
38227
  this.tfQuick.destroy();
37550
38228
  this.shortcutsHelp?.destroy();
37551
- this.toast.destroy();
38229
+ this.toastHost.destroy();
37552
38230
  this.alertsMenu?.destroy();
37553
38231
  this.glider.stop();
37554
- sharedBarStore.retain(/* @__PURE__ */ new Set());
38232
+ sharedBarStore.retain(/* @__PURE__ */ new Set(), this);
37555
38233
  this.root.remove();
37556
38234
  this.events.clear();
37557
38235
  }
@@ -37569,7 +38247,7 @@ var VelaWorkspace = class {
37569
38247
  this.mobileBar?.renderActions();
37570
38248
  this.mobileBar?.setSymbol(cell.symbol);
37571
38249
  this.mobileBar?.setTimeframe(cell.timeframe);
37572
- this.drawingPill.onChart(cell.chart);
38250
+ this.drawingPill?.onChart(cell.chart);
37573
38251
  const pushHistory = () => this.topbar.setHistoryState(cell.history.canUndo, cell.history.canRedo);
37574
38252
  this.historyUnsub?.();
37575
38253
  this.historyUnsub = cell.history.onChange(pushHistory);
@@ -37695,7 +38373,8 @@ var VelaWorkspace = class {
37695
38373
  onPriceStyleChanged: (id2) => this.onCellPriceStyleChanged(id2),
37696
38374
  onIndicatorsChanged: (id2) => this.onCellIndicatorsChanged(id2),
37697
38375
  onStateDirty: () => this.markStateDirty(),
37698
- manifestSettled: () => this.manifestSettled
38376
+ manifestSettled: () => this.manifestSettled,
38377
+ toast: (message, kind, durationMs) => this.toastHost.show(message, kind, durationMs)
37699
38378
  });
37700
38379
  cell.host.style.gridArea = perCell[slot.id]?.gridArea ?? "";
37701
38380
  this.cellsById.set(id, cell);
@@ -37704,6 +38383,7 @@ var VelaWorkspace = class {
37704
38383
  cell.chart.renderer.setLayoutMode(this.layoutCtl.current);
37705
38384
  if (this.favs.length > 0) cell.chart.drawings.setFavorites(this.favs);
37706
38385
  cell.setManifest(this.manifest, pooled?.indicators == null);
38386
+ cell.restorePersistedExt();
37707
38387
  this.events.emit("cell:created", { id });
37708
38388
  }
37709
38389
  for (const [i] of this.def.cells.entries()) {
@@ -37715,12 +38395,13 @@ var VelaWorkspace = class {
37715
38395
  * cell's whole life, so these live and die with the cell). */
37716
38396
  wireCell(cell) {
37717
38397
  const chart = cell.chart;
37718
- chart.on("indicator:error", ({ error }) => this.toast.show(`[${cell.id}] ${error.message}`, "error", 5e3));
38398
+ chart.on("indicator:error", ({ error }) => this.toastHost.show(`[${cell.id}] ${error.message}`, "error", 5e3));
37719
38399
  chart.on("script:run", (run) => this.events.emit("script:run", { ...run, cell: cell.id }));
37720
38400
  chart.on("alert", (alert) => {
37721
- this.alerts.unshift({ cellId: cell.id, symbol: cell.symbol, title: alert.title ?? "Alert", message: alert.message, time: alert.time });
37722
- if (this.alerts.length > ALERT_CAP) this.alerts.pop();
37723
- this.toast.show(`[${cell.id} \xB7 ${cell.symbol}] ${alert.title ? alert.title + " \u2014 " : ""}${alert.message}`, "info", 4e3);
38401
+ const source = [parseSymbol(cell.symbol).ticker || cell.symbol, timeframeLabel(cell.timeframe), alert.indicator].filter(Boolean).join(" ");
38402
+ this.alerts.unshift({ cellId: cell.id, source, title: alert.title ?? "Alert", message: alert.message, time: alert.time });
38403
+ if (this.alerts.length > this.alertCap) this.alerts.pop();
38404
+ this.toastHost.show(`${source} \u2014 ${alert.title ? alert.title + " \u2014 " : ""}${alert.message}`, "info", 4e3);
37724
38405
  this.topbar.setAlertCount(this.alerts.length);
37725
38406
  });
37726
38407
  chart.on("drawing:favorites", ({ favorites }) => {
@@ -37981,6 +38662,7 @@ var VelaWorkspace = class {
37981
38662
  this.mobileBar?.setTimeframe(cell.timeframe);
37982
38663
  this.objectTree.setSymbol(cell.symbol);
37983
38664
  this.bottombar?.setSession({ session: cell.session, enabled: cell.sessionAvailable });
38665
+ this.bottombar?.setActiveRange(cell.activeRangeId);
37984
38666
  }
37985
38667
  /** Trigger ② — a cell's price style changed: the topbar button/menu only if active.
37986
38668
  * Reads the cell back (not the requested style) so the button reflects what the
@@ -38065,7 +38747,7 @@ var VelaWorkspace = class {
38065
38747
  openDrawingsDrawer() {
38066
38748
  this.drawingsDrawer ?? (this.drawingsDrawer = new DrawingsDrawer({
38067
38749
  host: this.root,
38068
- toolbar: () => defaultToolbar(),
38750
+ toolbar: () => this.toolbarDef,
38069
38751
  // the shared static toolbar's definition (see constructor)
38070
38752
  currentTool: () => this.active.chart.drawings.getTool(),
38071
38753
  isFavorite: (type) => this.active.chart.drawings.isFavorite(type),
@@ -38076,26 +38758,32 @@ var VelaWorkspace = class {
38076
38758
  this.drawingsDrawer.open();
38077
38759
  }
38078
38760
  openMoreDrawer() {
38761
+ const has = (id) => topbarHas(this.topbarComp, id);
38079
38762
  this.moreDrawer ?? (this.moreDrawer = new MoreDrawer({
38080
38763
  host: this.root,
38081
- onUndo: () => this.active.history.undo(),
38082
- onRedo: () => this.active.history.redo(),
38083
- onScreenshot: () => this.active.downloadScreenshot(),
38764
+ ...has("undo-redo") ? { onUndo: () => this.active.history.undo(), onRedo: () => this.active.history.redo() } : {},
38765
+ ...has("screenshot") ? { onScreenshot: this.screenshotOverride ? () => this.runOverride(this.screenshotOverride) : () => this.active.downloadScreenshot() } : {},
38084
38766
  canUndo: () => this.active.history.canUndo,
38085
38767
  canRedo: () => this.active.history.canRedo,
38086
38768
  priceStyles: () => priceStyleIds().map((id) => ({ id, label: priceStyleLabel(id), icon: priceStyleIcon(id) })),
38087
38769
  priceStyle: () => this.active.priceStyle,
38088
38770
  onPriceStyle: (id) => this.active.setPriceStyle(id),
38089
- panels: () => [...this.dock.list()],
38771
+ panels: () => has("panels") ? [...this.dock.list()] : [],
38090
38772
  onTogglePanel: (id) => this.dock.toggle(id),
38091
- alerts: () => this.alerts.map((a) => ({ title: `[${a.cellId} \xB7 ${a.symbol}] ${a.title}`, message: a.message, time: a.time })),
38773
+ ...has("alerts") ? { alerts: () => this.alerts.map((a) => ({ title: `${a.source} \xB7 ${a.title}`, message: a.message, time: a.time })) } : {},
38092
38774
  // Left-aligned actions have their own bottom-bar stop — only the rest
38093
- // lands in the drawer, or every left action would appear twice.
38094
- actions: () => widgetActions("topbar", this.context()).filter((a) => a.align !== "left").map((a) => ({ label: a.label, icon: a.icon, run: () => a.run(this.context()) })),
38775
+ // lands in the drawer, or every left action would appear twice. Built-in-id
38776
+ // actions are slot OVERRIDES: they reach the drawer through the slot's own
38777
+ // routed button (screenshot) or stop (indicators), never as an extra row.
38778
+ actions: () => {
38779
+ const builtin = new Set(TOPBAR_BUILTIN_IDS);
38780
+ return widgetActions("topbar", this.context()).filter((a) => a.align !== "left" && !builtin.has(a.id)).map((a) => ({ label: a.label, icon: a.icon, run: () => a.run(this.context()) }));
38781
+ },
38095
38782
  // The desktop layout dropdown's whole surface — the grid canvas, the
38096
38783
  // non-canvas presets and the sync switches — relocated into the kebab
38097
- // drawer (the topbar is hidden on mobile). Same reads as the topbar block.
38098
- layout: {
38784
+ // drawer (the topbar is hidden on mobile). Same reads as the topbar block;
38785
+ // single-chart mode and a composition without 'layout' omit it here too.
38786
+ layout: this.monoLayout || !has("layout") ? void 0 : {
38099
38787
  shape: () => layoutShape(this.def),
38100
38788
  presets: () => layouts().filter((l) => layoutShape(l) === null).map((l) => ({ id: l.id, label: l.label, checked: l.id === this.def.id })),
38101
38789
  onSelectGrid: (rows, cols) => this.setLayout(layoutForGrid(rows, cols)),
@@ -38138,7 +38826,7 @@ var VelaWorkspace = class {
38138
38826
  this.alertsMenu?.destroy();
38139
38827
  const items = this.alerts.length ? this.alerts.map((a, i) => ({
38140
38828
  id: String(i),
38141
- label: `[${a.cellId} \xB7 ${a.symbol}] ${new Date(a.time).toLocaleTimeString()} \xB7 ${a.title}: ${a.message}`.slice(0, 80)
38829
+ label: `${a.source} \xB7 ${new Date(a.time).toLocaleTimeString()} \xB7 ${a.title}: ${a.message}`.slice(0, 80)
38142
38830
  })) : [{ id: "none", label: "No alerts yet", disabled: true }];
38143
38831
  this.alertsMenu = new Menu({
38144
38832
  host: this.root,
@@ -38162,9 +38850,23 @@ var VelaWorkspace = class {
38162
38850
  }
38163
38851
  }
38164
38852
  }
38853
+ /** Invoke a slot override the way its button would: fresh context, `when` respected. */
38854
+ runOverride(action) {
38855
+ const ctx = this.context();
38856
+ if (!action.when || action.when(ctx)) action.run(ctx);
38857
+ }
38165
38858
  /** The default shortcut set — every binding acts on the ACTIVE cell. */
38166
38859
  registerDefaultKeys() {
38167
- this.keymap.register({ id: "chart.screenshot", keys: "mod+alt+s", label: "Download a chart screenshot", category: "Chart", run: () => this.active.downloadScreenshot() });
38860
+ if (topbarHas(this.topbarComp, "screenshot")) {
38861
+ const ov = this.screenshotOverride;
38862
+ this.keymap.register({
38863
+ id: "chart.screenshot",
38864
+ keys: "mod+alt+s",
38865
+ label: ov ? ov.label : "Download a chart screenshot",
38866
+ category: "Chart",
38867
+ run: ov ? () => this.runOverride(ov) : () => this.active.downloadScreenshot()
38868
+ });
38869
+ }
38168
38870
  this.keymap.register({ id: "chart.reset-view", keys: "alt+r", label: "Reset view (all history)", category: "Chart", run: () => this.active.chart.setVisibleRangePreset("ALL") });
38169
38871
  this.keymap.register({ id: "chart.toggle-log", keys: "alt+l", label: "Toggle logarithmic scale", category: "Chart", run: () => this.active.chart.renderer.set("logScale", !this.active.chart.renderer.get("logScale")) });
38170
38872
  this.keymap.register({
@@ -38204,7 +38906,10 @@ var VelaWorkspace = class {
38204
38906
  this.keymap.register({ id: "view.zoom-out", keys: "mod+arrowdown", label: "Zoom out", category: "Chart", run: () => this.glider.zoom(ZOOM_OUT) });
38205
38907
  this.keymap.register({ id: "view.pan-left", keys: "mod+arrowleft", label: "Pan toward history", category: "Chart", run: () => this.active.chart.panBy(-PAN_FAST) });
38206
38908
  this.keymap.register({ id: "view.pan-right", keys: "mod+arrowright", label: "Pan toward now", category: "Chart", run: () => this.active.chart.panBy(PAN_FAST) });
38207
- if (this.indicatorPicker) {
38909
+ const indOv = this.indicatorsOverride;
38910
+ if (indOv && topbarHas(this.topbarComp, "indicators")) {
38911
+ this.keymap.register({ id: "indicators.open", keys: "/", label: indOv.label, category: "Indicators", run: () => this.runOverride(indOv) });
38912
+ } else if (this.indicatorPicker) {
38208
38913
  this.keymap.register({ id: "indicators.open", keys: "/", label: "Open the indicator picker", category: "Indicators", run: () => this.indicatorPicker?.open() });
38209
38914
  }
38210
38915
  this.keymap.register({
@@ -38259,7 +38964,7 @@ var VelaWorkspace = class {
38259
38964
  if (!raw) continue;
38260
38965
  symbols.add(this.feed.resolveSymbol(raw)?.ticker ?? raw);
38261
38966
  }
38262
- sharedBarStore.retain(symbols);
38967
+ sharedBarStore.retain(symbols, this);
38263
38968
  }
38264
38969
  };
38265
38970