@luxalgo/vela 0.6.9 → 0.6.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/{DataProvider-8Z95Q-RJ.d.cts → DataProvider-CWmp31dA.d.ts} +49 -1
  2. package/dist/{DataProvider-DExJrfut.d.ts → DataProvider-l_eLMly_.d.cts} +49 -1
  3. package/dist/{chunk-EQCHJZOT.js → chunk-4KZVZ7QQ.js} +1 -1
  4. package/dist/{chunk-6WDDVMBJ.js → chunk-FFOP37FQ.js} +577 -321
  5. package/dist/{chunk-62SVGONC.js → chunk-G77Y7LK2.js} +2 -2
  6. package/dist/{chunk-WFSZBX3R.js → chunk-KG4YT3TI.js} +3 -1
  7. package/dist/{chunk-FCVIG7JG.js → chunk-MHY7MVXH.js} +5 -1
  8. package/dist/{chunk-STHSKXOR.js → chunk-NMTQXNT4.js} +591 -141
  9. package/dist/{contributions-D7PVZO2i.d.ts → contributions-BCz6Dr6a.d.ts} +6 -6
  10. package/dist/{contributions-C1U2Krwg.d.cts → contributions-D9vTzm5p.d.cts} +6 -6
  11. package/dist/index.cjs +580 -318
  12. package/dist/index.d.cts +31 -10
  13. package/dist/index.d.ts +31 -10
  14. package/dist/index.js +4 -4
  15. package/dist/{options-FM0peknS.d.ts → options-DSqHsQyN.d.cts} +6 -6
  16. package/dist/{options-FM0peknS.d.cts → options-DSqHsQyN.d.ts} +6 -6
  17. package/dist/{plugin-DfVqBz9p.d.cts → plugin-Bt8hLR8Z.d.cts} +3 -3
  18. package/dist/{plugin-7bkF32Rk.d.ts → plugin-CH7pfMrE.d.ts} +3 -3
  19. package/dist/plugin.cjs +3 -1
  20. package/dist/plugin.d.cts +4 -4
  21. package/dist/plugin.d.ts +4 -4
  22. package/dist/plugin.js +2 -2
  23. package/dist/providers/binance.d.cts +2 -2
  24. package/dist/providers/binance.d.ts +2 -2
  25. package/dist/providers/coinbase.d.cts +2 -2
  26. package/dist/providers/coinbase.d.ts +2 -2
  27. package/dist/providers/hyperliquid.d.cts +2 -2
  28. package/dist/providers/hyperliquid.d.ts +2 -2
  29. package/dist/{statusline-zdF4eZLr.d.ts → statusline-5K7y4Ya2.d.ts} +3 -3
  30. package/dist/{statusline-DOPiT6I6.d.cts → statusline-TYLQmoeh.d.cts} +3 -3
  31. package/dist/ui.cjs +7 -1
  32. package/dist/ui.d.cts +3 -1
  33. package/dist/ui.d.ts +3 -1
  34. package/dist/ui.js +3 -3
  35. package/dist/vela.global.js +580 -318
  36. package/dist/vela.global.min.js +50 -48
  37. package/dist/widget.cjs +1052 -340
  38. package/dist/widget.d.cts +17 -6
  39. package/dist/widget.d.ts +17 -6
  40. package/dist/widget.js +7 -7
  41. package/dist/workspace.cjs +1052 -340
  42. package/dist/workspace.d.cts +89 -5
  43. package/dist/workspace.d.ts +89 -5
  44. package/dist/workspace.js +6 -6
  45. package/package.json +1 -1
@@ -528,6 +528,8 @@ var Vela = (function (exports) {
528
528
  registerIcon("folder-minus", S('<path d="M1.6 4.4a1 1 0 0 1 1-1h2.7l1.3 1.7h6.8a1 1 0 0 1 1 1v6.5a1 1 0 0 1-1 1h-10.8a1 1 0 0 1-1-1z"/><path d="M6.2 9.4h3.6"/>'));
529
529
  registerIcon("collapse", S('<path d="M3 8h10"/>'));
530
530
  registerIcon("expand", S('<rect x="2.2" y="2.2" width="11.6" height="11.6" rx="1.4"/><path d="M8 5.4v5.2M5.4 8h5.2"/>'));
531
+ registerIcon("plus", S('<path d="M8 2.8v10.4M2.8 8h10.4"/>'));
532
+ registerIcon("minus", S('<path d="M2.8 8h10.4"/>'));
531
533
  registerIcon("maximize", S('<path d="M2.5 6V3a.5.5 0 0 1 .5-.5h3M10 2.5h3a.5.5 0 0 1 .5.5v3M13.5 10v3a.5.5 0 0 1-.5.5h-3M6 13.5H3a.5.5 0 0 1-.5-.5v-3"/>'));
532
534
  registerIcon("restore", S('<path d="M6.2 2.5v3.7H2.5M9.8 13.5V9.8h3.7M13.5 6.2H9.8V2.5M2.5 9.8h3.7v3.7"/>'));
533
535
  registerIcon("star", S('<path d="M8 2.2l1.75 3.55 3.9.55-2.8 2.75.65 3.9L8 11.1l-3.5 1.85.65-3.9-2.8-2.75 3.9-.55z"/>'));
@@ -535,7 +537,7 @@ var Vela = (function (exports) {
535
537
  registerIcon("grip", S('<circle cx="6" cy="3.5" r="1"/><circle cx="10" cy="3.5" r="1"/><circle cx="6" cy="8" r="1"/><circle cx="10" cy="8" r="1"/><circle cx="6" cy="12.5" r="1"/><circle cx="10" cy="12.5" r="1"/>', 'fill="currentColor" stroke="none"'));
536
538
  registerIcon("kebab", S('<circle cx="8" cy="3.2" r="1.2"/><circle cx="8" cy="8" r="1.2"/><circle cx="8" cy="12.8" r="1.2"/>', 'fill="currentColor" stroke="none"'));
537
539
  registerIcon("burger", S('<path d="M2.5 4.5h11M2.5 8h11M2.5 11.5h11"/>'));
538
- registerIcon("reset", S('<rect x="6" y="6" width="4" height="4" rx="0.8"/><path d="M2.2 8a5.8 5.8 0 1 0 1.9-4.3L2.2 5.3"/><path d="M2.2 2.2v3.1h3.1"/>'));
540
+ registerIcon("reset", S('<path d="M2 8a6 6 0 1 0 6-6 6.5 6.5 0 0 0-4.5 1.83L2 5.33"/><path d="M2 2v3.33h3.33"/>'));
539
541
  registerIcon("pane-collapse", S('<path d="M2.6 9.4h4v4M13.4 6.6h-4v-4"/><path d="m9.4 6.6 4.2-4.2M2.4 13.6l4.2-4.2"/>'));
540
542
  registerIcon("pane-expand", S('<path d="M9.8 2.4h3.8v3.8M6.2 13.6H2.4V9.8"/><path d="m13.6 2.4-4.4 4.4M2.4 13.6l4.4-4.4"/>'));
541
543
  registerIcon("cursor", svg24('<path d="M5 3l6 16 2-6 6-2z"/>', 'fill="currentColor" stroke="none"'));
@@ -6906,6 +6908,23 @@ var Vela = (function (exports) {
6906
6908
  return 36e5;
6907
6909
  }
6908
6910
 
6911
+ // src/data/symbol-groups.ts
6912
+ function isGroupRow(d) {
6913
+ return d.group != null && d.ticker === d.group;
6914
+ }
6915
+ function groupKeyOf(d) {
6916
+ return `${(d.prefix ?? d.provider ?? "").toLowerCase()}:${(d.group ?? "").toUpperCase()}`;
6917
+ }
6918
+ function groupMembers(pool, groupRow) {
6919
+ const key = groupKeyOf(groupRow);
6920
+ return pool.filter((s) => s.group != null && !isGroupRow(s) && groupKeyOf(s) === key);
6921
+ }
6922
+ function defaultMemberOf(pool, groupRow) {
6923
+ const members = groupMembers(pool, groupRow);
6924
+ const defaults2 = members.filter((m) => m.default);
6925
+ return (defaults2.length === 1 ? defaults2[0] : members[0]) ?? null;
6926
+ }
6927
+
6909
6928
  // src/data/ProviderRegistry.ts
6910
6929
  var normName = (n) => n.trim().toLowerCase();
6911
6930
  var normTicker = (t) => t.trim().toUpperCase();
@@ -6995,17 +7014,24 @@ var Vela = (function (exports) {
6995
7014
  resolve(raw, opts = {}) {
6996
7015
  const { provider, ticker } = this.parse(raw);
6997
7016
  if (provider) {
6998
- if (this.entries.has(provider)) return { provider, ticker };
7017
+ if (this.entries.has(provider)) {
7018
+ const d = this.entries.get(provider).byTicker?.get(normTicker(ticker));
7019
+ return { provider, ticker: d != null && isGroupRow(d) ? this.loadableTicker(provider, d) : ticker };
7020
+ }
6999
7021
  const key = `${provider}:${normTicker(ticker)}`;
7000
7022
  for (const name of this.candidateOrder(opts.default)) {
7001
7023
  const d = this.entries.get(name).prefixIndex?.get(key);
7002
- if (d) return { provider: name, ticker: d.ticker };
7024
+ if (d) return { provider: name, ticker: this.loadableTicker(name, d) };
7003
7025
  }
7004
7026
  return null;
7005
7027
  }
7006
7028
  const norm = normTicker(ticker);
7007
7029
  for (const name of this.candidateOrder(opts.default)) {
7008
- if (this.entries.get(name).index?.has(norm)) return { provider: name, ticker };
7030
+ const e = this.entries.get(name);
7031
+ if (e.index?.has(norm)) {
7032
+ const d = e.byTicker?.get(norm);
7033
+ return { provider: name, ticker: d != null && isGroupRow(d) ? this.loadableTicker(name, d) : ticker };
7034
+ }
7009
7035
  }
7010
7036
  if (opts.lenient && this.entries.size === 1) {
7011
7037
  const only = this.names()[0];
@@ -7014,6 +7040,13 @@ var Vela = (function (exports) {
7014
7040
  }
7015
7041
  return null;
7016
7042
  }
7043
+ /** What a resolved descriptor LOADS: itself — unless it is a GROUP row (listed, never
7044
+ * served), which loads its default member (single `default`, else first listed). A
7045
+ * memberless group keeps its own ticker and fails downstream like any unknown symbol. */
7046
+ loadableTicker(name, d) {
7047
+ if (!isGroupRow(d)) return d.ticker;
7048
+ return defaultMemberOf(this.entries.get(name)?.descriptors ?? [], d)?.ticker ?? d.ticker;
7049
+ }
7017
7050
  /**
7018
7051
  * Resolve now if possible, else resolve later — re-attempting after each provider
7019
7052
  * settles. The promise stays pending until some registered provider can serve the
@@ -7302,6 +7335,10 @@ var Vela = (function (exports) {
7302
7335
  /** Invalidates detached async work (backfill loops, in-flight loads, gap heals):
7303
7336
  * bumped by init(), setMarket() and destroy(). */
7304
7337
  this.generation = 0;
7338
+ /** Aborts the in-flight PROGRESSIVE load's source polling on supersession — an
7339
+ * abandoned stream left polling to its own budget starves the browser's per-host
7340
+ * connection pool, and the NEXT symbol's very first fetch with it (measured). */
7341
+ this.progressiveAbort = null;
7305
7342
  /** Awaiters racing a superseded load (setMarket callers) — released on every bump so they never hang. */
7306
7343
  this.supersedeWaiters = [];
7307
7344
  /** `history:complete` fired for the CURRENT load. Each market load re-arms the cycle
@@ -7444,6 +7481,8 @@ var Vela = (function (exports) {
7444
7481
  * superseded setMarket awaiters so their promises resolve instead of hanging. */
7445
7482
  bumpGeneration() {
7446
7483
  const gen = ++this.generation;
7484
+ this.progressiveAbort?.abort();
7485
+ this.progressiveAbort = null;
7447
7486
  for (const w of this.supersedeWaiters.splice(0)) w();
7448
7487
  return gen;
7449
7488
  }
@@ -7497,7 +7536,47 @@ var Vela = (function (exports) {
7497
7536
  const requested = market.bars ?? 500;
7498
7537
  const initialRange = market.visibleRange;
7499
7538
  const deep = !market.data?.length && initialRange == null && requested > SINGLE_LOAD_BARS;
7500
- if (deep && this.feed.loadRange) {
7539
+ let progressiveServed = false;
7540
+ if (!market.data?.length && initialRange == null && this.feed.loadProgressive) {
7541
+ let painted = false;
7542
+ const paint = (bars, final) => {
7543
+ if (this.generation !== gen || !final && bars.length === 0) return;
7544
+ this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
7545
+ if (!painted && bars.length > 0) {
7546
+ painted = true;
7547
+ if (opts.firstLoad) this.activateBarLayers();
7548
+ if (!final) this.historyState = "backfill";
7549
+ }
7550
+ };
7551
+ const abort = new AbortController();
7552
+ this.progressiveAbort = abort;
7553
+ progressiveServed = await new Promise((firstPaint) => {
7554
+ let signaled = false;
7555
+ const signal = (served) => {
7556
+ if (!signaled) {
7557
+ signaled = true;
7558
+ firstPaint(served);
7559
+ }
7560
+ };
7561
+ abort.signal.addEventListener("abort", () => signal(true), { once: true });
7562
+ this.feed.loadProgressive(market, (bars) => {
7563
+ paint(bars, false);
7564
+ if (painted) signal(true);
7565
+ }, { signal: abort.signal }).then((full) => {
7566
+ if (this.progressiveAbort === abort) this.progressiveAbort = null;
7567
+ if (full == null) return signal(false);
7568
+ if (this.generation !== gen) return signal(true);
7569
+ paint(full, true);
7570
+ this.completeHistory(full.length >= requested ? "depth" : "genesis");
7571
+ signal(true);
7572
+ }).catch(() => {
7573
+ if (this.progressiveAbort === abort) this.progressiveAbort = null;
7574
+ if (this.generation === gen) this.completeHistory("aborted");
7575
+ signal(true);
7576
+ });
7577
+ });
7578
+ }
7579
+ if (progressiveServed) ; else if (deep && this.feed.loadRange) {
7501
7580
  const head = await this.feed.load({ ...market, bars: Math.min(requested, CHUNK_BARS) });
7502
7581
  if (this.generation !== gen) return;
7503
7582
  this.setBarSeries(head);
@@ -9169,6 +9248,28 @@ var Vela = (function (exports) {
9169
9248
  this.store.merge(key, dropForming(bars));
9170
9249
  return bars;
9171
9250
  }
9251
+ /**
9252
+ * Progressive twin of {@link load}: a COLD load streams through the inner feed's
9253
+ * progressive path — batches forwarded verbatim, the FINAL answer cached exactly as
9254
+ * `load` caches — while a cache-covered load answers once through `load` itself (a
9255
+ * warm chart has nothing to stream). Falls back to `load` wholesale when the inner
9256
+ * feed lacks the capability, so callers may prefer this method unconditionally.
9257
+ */
9258
+ async loadProgressive(cfg, onBatch, opts) {
9259
+ if (cfg.data && cfg.data.length > 0) return this.inner.load(cfg);
9260
+ if (!this.inner.loadProgressive) return null;
9261
+ const symbol = cfg.symbol ?? "TEST";
9262
+ const key = cacheKey(symbol, cfg.timeframe ?? "60", cfg.session);
9263
+ const cached = this.store.get(key);
9264
+ const n = cfg.bars ?? 500;
9265
+ const lastCached = cached?.[cached.length - 1];
9266
+ if (cached && lastCached && cached.length >= n - 1 && this.inner.loadRange) return this.load(cfg);
9267
+ this.store.retainSymbol(symbol);
9268
+ const bars = await this.inner.loadProgressive(cfg, onBatch, opts);
9269
+ if (bars == null) return null;
9270
+ this.store.merge(key, dropForming(bars));
9271
+ return bars;
9272
+ }
9172
9273
  /**
9173
9274
  * Cache-backed ranged fetch — the gateway engines use for secondary series
9174
9275
  * (`request.security` HTF/LTF/cross-symbol) and the orchestrator's backward
@@ -9414,6 +9515,17 @@ var Vela = (function (exports) {
9414
9515
  this.prefetchSymbolInfo(resolved);
9415
9516
  return this.cache.load(canonical(cfg, resolved));
9416
9517
  }
9518
+ /** Progressive twin of {@link load} — same resolution, the cache streams the batches. */
9519
+ async loadProgressive(cfg, onBatch, opts) {
9520
+ if (cfg.data && cfg.data.length > 0) {
9521
+ this.liveBars = cfg.data.map((b) => ({ ...b }));
9522
+ return cfg.data;
9523
+ }
9524
+ const resolved = await this.registry.whenResolvable(rawSymbol(cfg), { default: this.primaryProvider });
9525
+ this.primaryProvider = resolved.provider;
9526
+ this.prefetchSymbolInfo(resolved);
9527
+ return this.cache.loadProgressive(canonical(cfg, resolved), onBatch, opts);
9528
+ }
9417
9529
  /**
9418
9530
  * Synchronous per-symbol metadata for engines (Pine `syminfo.*`), served from the cache
9419
9531
  * warmed by load(). Undefined until the prefetch lands (the engine then synthesizes a
@@ -9501,6 +9613,18 @@ var Vela = (function (exports) {
9501
9613
  if (!provider) return Promise.resolve([]);
9502
9614
  return safeBars(provider, ticker, cfg.timeframe ?? "60", { limit: cfg.bars ?? 500, session: cfg.session });
9503
9615
  }
9616
+ async loadProgressive(cfg, onBatch, opts) {
9617
+ const { provider: name, ticker } = parseSymbol(cfg.symbol ?? "");
9618
+ const provider = this.registry.get(name ?? "");
9619
+ if (!provider) return [];
9620
+ if (!provider.getBarsProgressive) return null;
9621
+ try {
9622
+ return await provider.getBarsProgressive(ticker, cfg.timeframe ?? "60", { limit: cfg.bars ?? 500, session: cfg.session }, onBatch, opts);
9623
+ } catch (e) {
9624
+ console.warn(`[vela] progressive fetch failed for ${ticker} ${cfg.timeframe ?? "60"} \u2014 ${e instanceof Error ? e.message : String(e)}`);
9625
+ return [];
9626
+ }
9627
+ }
9504
9628
  loadRange(cfg, range) {
9505
9629
  const { provider: name, ticker } = parseSymbol(cfg.symbol ?? "");
9506
9630
  const provider = this.registry.get(name ?? "");
@@ -17014,6 +17138,10 @@ ${STATIC_DECLS}
17014
17138
  get open() {
17015
17139
  return this.pop?.open ?? false;
17016
17140
  }
17141
+ /** Close the deployed panel, if any. The bubble itself stays. */
17142
+ hidePanel() {
17143
+ this.pop?.hide();
17144
+ }
17017
17145
  destroy() {
17018
17146
  this.pop?.destroy();
17019
17147
  this.pop = null;
@@ -21110,6 +21238,9 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21110
21238
  var FOLD_SVG = iconAt("chevron-up", LEGEND_ICON_PX2);
21111
21239
  var UNFOLD_SVG = iconAt("chevron-down", LEGEND_ICON_PX2);
21112
21240
  var OVERVIEW_SVG = iconAt("objects", LEGEND_ICON_PX2);
21241
+ function legendCalloutsDisplay(open2, hasCallouts) {
21242
+ return !open2 && hasCallouts ? "inline-flex" : "none";
21243
+ }
21113
21244
  var InputsUI = class {
21114
21245
  constructor(container, theme, paneBoundsOf) {
21115
21246
  this.container = container;
@@ -21313,7 +21444,7 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21313
21444
  row.callouts = [];
21314
21445
  row.calloutsEl.replaceChildren();
21315
21446
  const views = this.legendCallouts?.(row.id) ?? [];
21316
- row.calloutsEl.style.display = views.length > 0 ? "inline-flex" : "none";
21447
+ row.calloutsEl.style.display = legendCalloutsDisplay(row.highlighted, views.length > 0);
21317
21448
  for (const view of views) {
21318
21449
  const bubble = new CalloutBubble({
21319
21450
  icon: view.icon,
@@ -21797,11 +21928,11 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21797
21928
  row.controlsEl.style.display = open2 || row.hidden ? "inline-flex" : "none";
21798
21929
  if (open2) {
21799
21930
  row.el.appendChild(row.statusEl);
21800
- row.el.appendChild(row.calloutsEl);
21931
+ for (const bubble of row.callouts) bubble.hidePanel();
21801
21932
  } else {
21802
21933
  row.el.insertBefore(row.statusEl, row.valuesEl);
21803
- row.el.insertBefore(row.calloutsEl, row.statusEl);
21804
21934
  }
21935
+ row.calloutsEl.style.display = legendCalloutsDisplay(open2, row.callouts.length > 0);
21805
21936
  for (const child of Array.from(row.controlsEl.children)) {
21806
21937
  if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
21807
21938
  if (child === row.extrasEl) {
@@ -21891,7 +22022,7 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21891
22022
  };
21892
22023
  var STYLE_ID = "vela-pane-controls";
21893
22024
  var ICON_PX = 12;
21894
- var CLUSTER_PILL = "rgba(0,0,0,0.28)";
22025
+ var CLUSTER_PILL = "rgba(0,0,0,0.65)";
21895
22026
  function ensureStyles() {
21896
22027
  if (typeof document === "undefined" || document.getElementById(STYLE_ID)) return;
21897
22028
  const st = document.createElement("style");
@@ -21911,8 +22042,12 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21911
22042
  this.deps = deps;
21912
22043
  this.clusters = /* @__PURE__ */ new Map();
21913
22044
  this.hoverPaneId = null;
22045
+ /** Mobile: hover clusters are meaningless without a cursor — suppressed; a
22046
+ * collapsed pane's standalone expand chip stays (the only way back up). */
22047
+ this.suspended = false;
21914
22048
  /** Reveal the cluster for the pane under the cursor, resolved from the pointer's y in the plot. */
21915
22049
  this.onPlotMove = (e) => {
22050
+ if (this.suspended) return;
21916
22051
  const rect = this.plot.getBoundingClientRect();
21917
22052
  const y = e.clientY - rect.top;
21918
22053
  let hit = null;
@@ -21984,7 +22119,12 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21984
22119
  }
21985
22120
  if (p.count > 1) {
21986
22121
  cluster.appendChild(
21987
- this.button(p.maximized ? ICONS.restore : ICONS.maximize, p.maximized ? "Restore pane" : "Maximize pane", false, () => this.deps.onToggleMaximize(p.id), { role: "maximize" })
22122
+ this.button(p.maximized ? ICONS.restore : ICONS.maximize, p.maximized ? "Restore pane" : "Maximize pane", false, () => this.deps.onToggleMaximize(p.id), {
22123
+ role: "maximize",
22124
+ // Same inverse-chip treatment as the collapsed pane's expand toggle: the
22125
+ // maximized state must read as an active state, not just a swapped glyph.
22126
+ selected: p.maximized
22127
+ })
21988
22128
  );
21989
22129
  }
21990
22130
  }
@@ -22021,17 +22161,18 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
22021
22161
  }
22022
22162
  const hovered = id === this.hoverPaneId;
22023
22163
  const hasButtons = cluster.children.length > 0;
22024
- const visible = hasButtons && (hovered || p.collapsed) && p.height > 8;
22164
+ const stateChipRole = p.collapsed ? "collapse" : !this.suspended && p.maximized ? "maximize" : null;
22165
+ const visible = hasButtons && (hovered || stateChipRole != null) && p.height > 8;
22025
22166
  cluster.style.right = `${rightPx}px`;
22026
22167
  cluster.style.top = p.collapsed ? `${p.top + Math.max(1, Math.round((p.height - 24) / 2))}px` : `${p.top + 4}px`;
22027
22168
  cluster.style.display = visible ? "flex" : "none";
22028
22169
  if (!visible) continue;
22029
- const soloExpand = p.collapsed && !hovered;
22030
- cluster.style.background = soloExpand ? "transparent" : CLUSTER_PILL;
22170
+ const soloChip = stateChipRole != null && !hovered;
22171
+ cluster.style.background = soloChip ? "transparent" : CLUSTER_PILL;
22031
22172
  for (const child of cluster.children) {
22032
22173
  const btn2 = child;
22033
22174
  btn2.style.display = "inline-flex";
22034
- btn2.style.visibility = soloExpand && btn2.dataset.role !== "collapse" ? "hidden" : "visible";
22175
+ btn2.style.visibility = soloChip && btn2.dataset.role !== stateChipRole ? "hidden" : "visible";
22035
22176
  }
22036
22177
  }
22037
22178
  }
@@ -22041,6 +22182,14 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
22041
22182
  this.hoverPaneId = paneId;
22042
22183
  this.reposition();
22043
22184
  }
22185
+ /** Mobile suppression: no hover clusters (touch has no cursor; the shell's own
22186
+ * chrome covers maximize), while collapsed panes keep their expand chips. */
22187
+ setSuspended(on) {
22188
+ if (on === this.suspended) return;
22189
+ this.suspended = on;
22190
+ if (on) this.hoverPaneId = null;
22191
+ this.reposition();
22192
+ }
22044
22193
  destroy() {
22045
22194
  this.plot.removeEventListener("pointermove", this.onPlotMove);
22046
22195
  this.plot.removeEventListener("pointerleave", this.onPlotLeave);
@@ -22191,160 +22340,6 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
22191
22340
  }
22192
22341
  };
22193
22342
 
22194
- // src/renderers/shared/TableOverlay.ts
22195
- var SIZE_PX3 = {
22196
- auto: 13,
22197
- tiny: 10,
22198
- small: 11,
22199
- normal: 13,
22200
- large: 16,
22201
- huge: 20
22202
- };
22203
- function fontPxOf(size3) {
22204
- if (typeof size3 === "number") return size3 > 0 ? size3 : SIZE_PX3.auto;
22205
- return SIZE_PX3[size3] ?? SIZE_PX3.auto;
22206
- }
22207
- function tableHasContent(t) {
22208
- return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
22209
- }
22210
- function mergeRenderPlan(t) {
22211
- const span = /* @__PURE__ */ new Map();
22212
- const omit = /* @__PURE__ */ new Set();
22213
- for (const m of t.merges) {
22214
- span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
22215
- for (let r = m.startRow; r <= m.endRow; r += 1) {
22216
- for (let c = m.startCol; c <= m.endCol; c += 1) {
22217
- if (r !== m.startRow || c !== m.startCol) omit.add(`${r}:${c}`);
22218
- }
22219
- }
22220
- }
22221
- for (let r = 0; r < t.rows; r += 1) {
22222
- for (let c = 0; c < t.columns; c += 1) {
22223
- if (t.cells[r]?.[c]?.merged && !span.has(`${r}:${c}`)) omit.add(`${r}:${c}`);
22224
- }
22225
- }
22226
- for (const key of span.keys()) omit.delete(key);
22227
- return { span, omit };
22228
- }
22229
- var TableOverlay = class {
22230
- constructor(container, theme, paneBounds) {
22231
- this.container = container;
22232
- this.theme = theme;
22233
- this.paneBounds = paneBounds;
22234
- this.lastTables = [];
22235
- if (getComputedStyle(container).position === "static") container.style.position = "relative";
22236
- this.root = document.createElement("div");
22237
- Object.assign(this.root.style, {
22238
- position: "absolute",
22239
- inset: "0",
22240
- pointerEvents: "none",
22241
- overflow: "hidden",
22242
- zIndex: "3"
22243
- });
22244
- container.appendChild(this.root);
22245
- }
22246
- update(tables) {
22247
- this.lastTables = tables;
22248
- this.root.replaceChildren();
22249
- for (const t of tables) {
22250
- if (tableHasContent(t)) this.root.appendChild(this.renderTable(t));
22251
- }
22252
- }
22253
- /** Re-render at the current pane geometry — after layout settles or on resize. */
22254
- reposition() {
22255
- if (this.root.isConnected) this.update(this.lastTables);
22256
- }
22257
- /** Show/hide the whole overlay. Tables anchor to pane corners, not to bars, so unlike the
22258
- * series content they DON'T vanish with an emptied chart — the loading state hides them. */
22259
- setVisible(visible) {
22260
- this.root.style.display = visible ? "" : "none";
22261
- }
22262
- destroy() {
22263
- this.root.remove();
22264
- }
22265
- renderTable(t) {
22266
- const b = this.paneBounds(t.paneId);
22267
- const wrap2 = document.createElement("div");
22268
- wrap2.style.position = "absolute";
22269
- if (t.frameColor && t.frameWidth > 0) wrap2.style.border = `${t.frameWidth}px solid ${t.frameColor}`;
22270
- this.anchor(wrap2, t.position, b);
22271
- const table = document.createElement("table");
22272
- Object.assign(table.style, {
22273
- borderCollapse: "collapse",
22274
- background: t.bgColor ?? "transparent",
22275
- fontFamily: this.theme.fontFamily || "sans-serif",
22276
- border: "none",
22277
- tableLayout: "auto",
22278
- // Re-enable pointer events on the table only (root is none) so cell tooltips work.
22279
- pointerEvents: "auto"
22280
- });
22281
- const { span, omit } = mergeRenderPlan(t);
22282
- const plotW = Math.max(0, (this.root.clientWidth || this.container.clientWidth) - b.rightAxis);
22283
- const paneH = b.height;
22284
- const cellBorder = t.borderColor && t.borderWidth > 0 ? `${t.borderWidth}px solid ${t.borderColor}` : "none";
22285
- for (let r = 0; r < t.rows; r += 1) {
22286
- const tr = document.createElement("tr");
22287
- for (let c = 0; c < t.columns; c += 1) {
22288
- if (omit.has(`${r}:${c}`)) continue;
22289
- const cell = t.cells[r]?.[c] ?? null;
22290
- const td = document.createElement("td");
22291
- if (cell === null) {
22292
- Object.assign(td.style, { padding: "0", border: "none" });
22293
- tr.appendChild(td);
22294
- continue;
22295
- }
22296
- const sp = span.get(`${r}:${c}`);
22297
- if (sp) {
22298
- if (sp.cs > 1) td.colSpan = sp.cs;
22299
- if (sp.rs > 1) td.rowSpan = sp.rs;
22300
- }
22301
- Object.assign(td.style, {
22302
- border: cellBorder,
22303
- padding: "2px 6px",
22304
- background: cell.bgColor ?? "transparent",
22305
- color: cell.textColor ?? this.theme.textColor,
22306
- textAlign: cell.hAlign,
22307
- verticalAlign: cell.vAlign === "top" ? "top" : cell.vAlign === "bottom" ? "bottom" : "middle",
22308
- fontSize: `${fontPxOf(cell.textSize)}px`,
22309
- fontFamily: cell.fontFamily === "monospace" ? "monospace" : "inherit",
22310
- fontWeight: cell.bold ? "bold" : "normal",
22311
- fontStyle: cell.italic ? "italic" : "normal",
22312
- // Pine cell text never wraps; `\n` still breaks lines. Wrapping
22313
- // used to collapse unicode sparklines and ━━━ dividers.
22314
- whiteSpace: "pre"
22315
- });
22316
- if (cell.width) td.style.width = `${cell.width / 100 * plotW}px`;
22317
- if (cell.height) td.style.height = `${cell.height / 100 * paneH}px`;
22318
- if (cell.tooltip) td.title = cell.tooltip;
22319
- td.textContent = cell.text ?? "";
22320
- tr.appendChild(td);
22321
- }
22322
- table.appendChild(tr);
22323
- }
22324
- wrap2.appendChild(table);
22325
- return wrap2;
22326
- }
22327
- /**
22328
- * Position the wrapper at a Pine `position.*` corner/edge of the table's PANE
22329
- * (not the whole chart), inset past the right price axis so it never overlaps
22330
- * the Y-axis labels.
22331
- */
22332
- anchor(el, position, b) {
22333
- const m = 6;
22334
- const containerH = this.root.clientHeight || this.container.clientHeight;
22335
- const paneBottom = b.top + b.height;
22336
- if (position.startsWith("top")) el.style.top = `${b.top + m}px`;
22337
- else if (position.startsWith("bottom")) el.style.bottom = `${Math.max(0, containerH - paneBottom) + m}px`;
22338
- else el.style.top = `${b.top + b.height / 2}px`;
22339
- if (position.endsWith("left")) el.style.left = `${m}px`;
22340
- else if (position.endsWith("right")) el.style.right = `${b.rightAxis + m}px`;
22341
- else el.style.left = `calc(50% - ${b.rightAxis / 2}px)`;
22342
- const tx = position.endsWith("center") ? "-50%" : "0";
22343
- const ty = position.startsWith("middle") ? "-50%" : "0";
22344
- if (tx !== "0" || ty !== "0") el.style.transform = `translate(${tx}, ${ty})`;
22345
- }
22346
- };
22347
-
22348
22343
  // src/renderers/native/capabilities.ts
22349
22344
  var NATIVE_CAPABILITIES = {
22350
22345
  panes: true,
@@ -22365,7 +22360,7 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
22365
22360
  drawingDepth: true,
22366
22361
  // drawings share the series' z space (backend-composited interleave layers)
22367
22362
  tables: true,
22368
- // reuses the DOM TableOverlay
22363
+ // canvas-painted into the owning indicator's interleave slice
22369
22364
  trades: true,
22370
22365
  // strategy order-fill markers (arrows + labels + fill-price ticks)
22371
22366
  inputsUI: true
@@ -22396,6 +22391,9 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
22396
22391
  if (spacing < CANDLE_BODY_MIN_SPACING) return "wick";
22397
22392
  return "full";
22398
22393
  }
22394
+ function snapY(yCss, dpr) {
22395
+ return Math.round(yCss * dpr) / dpr;
22396
+ }
22399
22397
  function candleGeometry(xCss, spacing, dpr, bodyScale = 1) {
22400
22398
  const wickDev = Math.max(1, Math.round(wickWidth(spacing) * dpr));
22401
22399
  const wickLeftDev = Math.round(xCss * dpr - wickDev / 2);
@@ -23202,11 +23200,9 @@ void main() {
23202
23200
  };
23203
23201
  b.alpha = this.modelAlpha;
23204
23202
  for (const m of models) for (const bgSpan of m.backgrounds) if (bgSpan.overlay !== true) this.emitBackground(b, bgSpan, pane, coords);
23205
- for (const m of models) for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, effPane(m), coords, i0, i1, scene.offsetOf(m.id));
23206
23203
  if (isPrice) {
23207
23204
  for (const m of scene.indicators.values()) {
23208
23205
  for (const bgSpan of m.backgrounds) if (bgSpan.overlay === true) this.emitBackground(b, bgSpan, pane, coords);
23209
- for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, i1, scene.offsetOf(m.id));
23210
23206
  }
23211
23207
  }
23212
23208
  const drawCandles = isPrice && !scene.candlesHidden;
@@ -23222,6 +23218,7 @@ void main() {
23222
23218
  b.alpha = this.modelAlpha;
23223
23219
  const off = scene.offsetOf(m.id);
23224
23220
  const mp = effPane(m);
23221
+ for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, mp, coords, i0, i1, off);
23225
23222
  for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
23226
23223
  }
23227
23224
  if (drawCandles && !candleDrawn) {
@@ -23229,14 +23226,18 @@ void main() {
23229
23226
  b.alpha = this.candleStructureAlpha;
23230
23227
  this.emitPriceSeries(b, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
23231
23228
  }
23232
- drawSlicesUpTo(Infinity);
23233
23229
  b.alpha = this.modelAlpha;
23234
23230
  if (isPrice) {
23231
+ for (const m of scene.indicators.values()) {
23232
+ const off = scene.offsetOf(m.id);
23233
+ for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, i1, off);
23234
+ }
23235
23235
  for (const m of scene.indicators.values()) {
23236
23236
  const off = scene.offsetOf(m.id);
23237
23237
  for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0, i1, theme, off);
23238
23238
  }
23239
23239
  }
23240
+ drawSlicesUpTo(Infinity);
23240
23241
  for (const m of models) {
23241
23242
  const mp = effPane(m);
23242
23243
  for (const pl of m.priceLines) this.emitHline(b, pl, mp, coords, dataW, theme);
@@ -23631,12 +23632,12 @@ void main() {
23631
23632
  if (drawBody) {
23632
23633
  const oY = coords.priceToY(bar.open, pane.scale, pane.bounds);
23633
23634
  const cY = coords.priceToY(bar.close, pane.scale, pane.bounds);
23634
- bodyTop = Math.min(oY, cY);
23635
- bodyH = Math.max(1, Math.abs(cY - oY));
23635
+ bodyTop = snapY(Math.min(oY, cY), coords.dpr);
23636
+ bodyH = Math.max(1 / coords.dpr, snapY(Math.max(oY, cY), coords.dpr) - bodyTop);
23636
23637
  }
23637
23638
  if (cs.wickVisible) {
23638
- const hY = coords.priceToY(bar.high, pane.scale, pane.bounds);
23639
- const lY = coords.priceToY(bar.low, pane.scale, pane.bounds);
23639
+ const hY = snapY(coords.priceToY(bar.high, pane.scale, pane.bounds), coords.dpr);
23640
+ const lY = snapY(coords.priceToY(bar.low, pane.scale, pane.bounds), coords.dpr);
23640
23641
  b.alpha = this.candleStructureAlpha;
23641
23642
  const wCol = parseColor((isUp ? cs.wickUpColor : cs.wickDownColor) ?? (drawBody ? dir : bodyColorStr));
23642
23643
  if (drawBody) {
@@ -24969,9 +24970,10 @@ void main() {
24969
24970
  * so each indicator arrives behind the candles (and behind older indicators);
24970
24971
  * `setIndicatorZ`/`bringToFront`/`sendToBack` change it. */
24971
24972
  this.seriesZ = /* @__PURE__ */ new Map();
24972
- /** Per-pane raster layers of user drawings interleaved into the series stack each is a
24973
+ /** Per-pane raster layers of drawings interleaved into the series stack (each
24974
+ * indicator's Pine drawings at its model's z, plus in-stack user drawings) — each a
24973
24975
  * prepainted canvas the backend composites just before the series carrying `beforeZ`.
24974
- * Rebuilt by the renderer per data frame; empty when every drawing sits over the stack. */
24976
+ * Rebuilt by the renderer per data frame. */
24975
24977
  this.drawingSlices = /* @__PURE__ */ new Map();
24976
24978
  /** Per-model index offset: the chart bar index of the model's `anchorTime` — its
24977
24979
  * index-aligned payloads (dense series arrays, `bar_index` drawings) count from that
@@ -25206,11 +25208,9 @@ void main() {
25206
25208
  };
25207
25209
  ctx.globalAlpha = this.modelAlpha;
25208
25210
  for (const m of models) for (const bg of m.backgrounds) if (bg.overlay !== true) this.drawBackground(ctx, bg, pane, coords);
25209
- for (const m of models) for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, effPane(m), coords, i0, i1, scene.offsetOf(m.id));
25210
25211
  if (isPrice) {
25211
25212
  for (const m of scene.indicators.values()) {
25212
25213
  for (const bg of m.backgrounds) if (bg.overlay === true) this.drawBackground(ctx, bg, pane, coords);
25213
- for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, scene.offsetOf(m.id));
25214
25214
  }
25215
25215
  }
25216
25216
  const slices = scene.drawingSlices.get(pane.id) ?? [];
@@ -25234,6 +25234,7 @@ void main() {
25234
25234
  ctx.globalAlpha = this.modelAlpha;
25235
25235
  const off = scene.offsetOf(m.id);
25236
25236
  const mp = effPane(m);
25237
+ for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, mp, coords, i0, i1, off);
25237
25238
  for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
25238
25239
  }
25239
25240
  if (drawCandles && !candleDrawn) {
@@ -25241,14 +25242,18 @@ void main() {
25241
25242
  ctx.globalAlpha = this.candleStructureAlpha;
25242
25243
  this.drawPriceSeries(ctx, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
25243
25244
  }
25244
- drawSlicesUpTo(Infinity);
25245
25245
  if (isPrice) {
25246
25246
  ctx.globalAlpha = this.modelAlpha;
25247
+ for (const m of scene.indicators.values()) {
25248
+ const off = scene.offsetOf(m.id);
25249
+ for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, off);
25250
+ }
25247
25251
  for (const m of scene.indicators.values()) {
25248
25252
  const off = scene.offsetOf(m.id);
25249
25253
  for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0, i1, theme, off);
25250
25254
  }
25251
25255
  }
25256
+ drawSlicesUpTo(Infinity);
25252
25257
  ctx.globalAlpha = this.modelAlpha;
25253
25258
  for (const m of models) {
25254
25259
  const mp = effPane(m);
@@ -25492,13 +25497,13 @@ void main() {
25492
25497
  if (drawBody) {
25493
25498
  const oY = coords.priceToY(b.open, pane.scale, pane.bounds);
25494
25499
  const cY = coords.priceToY(b.close, pane.scale, pane.bounds);
25495
- top = Math.min(oY, cY);
25496
- bodyH = Math.max(1, Math.abs(cY - oY));
25500
+ top = snapY(Math.min(oY, cY), coords.dpr);
25501
+ bodyH = Math.max(1 / coords.dpr, snapY(Math.max(oY, cY), coords.dpr) - top);
25497
25502
  }
25498
25503
  if (cs.wickVisible) {
25499
25504
  const wick = (up ? cs.wickUpColor : cs.wickDownColor) ?? (drawBody ? dir : color);
25500
- const hY = coords.priceToY(b.high, pane.scale, pane.bounds);
25501
- const lY = coords.priceToY(b.low, pane.scale, pane.bounds);
25505
+ const hY = snapY(coords.priceToY(b.high, pane.scale, pane.bounds), coords.dpr);
25506
+ const lY = snapY(coords.priceToY(b.low, pane.scale, pane.bounds), coords.dpr);
25502
25507
  ctx.globalAlpha = this.candleStructureAlpha;
25503
25508
  ctx.strokeStyle = wick;
25504
25509
  ctx.lineWidth = g.wickW;
@@ -25945,6 +25950,19 @@ void main() {
25945
25950
 
25946
25951
  // src/renderers/shared/DrawingSceneRenderer.ts
25947
25952
  var EMPTY_DRAWING_SET = { lines: [], boxes: [], labels: [], polylines: [], linefills: [] };
25953
+ function modelDrawingSet(m, overlay) {
25954
+ const want = (d) => Boolean(d.overlay) === overlay;
25955
+ return {
25956
+ lines: (m.lines ?? []).filter(want),
25957
+ boxes: (m.boxes ?? []).filter(want),
25958
+ labels: (m.labels ?? []).filter(want),
25959
+ polylines: (m.polylines ?? []).filter(want),
25960
+ linefills: (m.linefills ?? []).filter(want)
25961
+ };
25962
+ }
25963
+ function drawingSetEmpty(s) {
25964
+ return !s.lines.length && !s.boxes.length && !s.labels.length && !s.polylines.length && !s.linefills.length;
25965
+ }
25948
25966
  function fontSizePx(size3) {
25949
25967
  return size3 === "auto" ? 12 : namedFontSize(size3);
25950
25968
  }
@@ -26797,10 +26815,8 @@ void main() {
26797
26815
  this.ctx = null;
26798
26816
  // The color for axis tick labels — the host-passed surface text, set each frame in render().
26799
26817
  this.axisTextColor = DARK_THEME.textColor;
26800
- // Shared Pine-drawing renderer (line/box/label/polyline/linefill); widthCache persists.
26818
+ // Shared Pine-drawing renderer, used here for autoscale geometry only; widthCache persists.
26801
26819
  this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
26802
- // Tooltip hit-rects of every label drawn this frame, in plot coords (rebuilt per render).
26803
- this.labelTips = [];
26804
26820
  }
26805
26821
  mount(canvas) {
26806
26822
  this.canvas = canvas;
@@ -26824,8 +26840,8 @@ void main() {
26824
26840
  */
26825
26841
  paneDrawingsRange(ownModels, scene, isPricePane, vr) {
26826
26842
  let dr = null;
26827
- for (const m of ownModels) dr = unionRange(dr, this.drawingsRange(this.ownDrawings(m), vr, scene.offsetOf(m.id)));
26828
- if (isPricePane) for (const m of scene.indicators.values()) dr = unionRange(dr, this.drawingsRange(this.overlayDrawings(m), vr, scene.offsetOf(m.id)));
26843
+ for (const m of ownModels) dr = unionRange(dr, this.drawingsRange(modelDrawingSet(m, false), vr, scene.offsetOf(m.id)));
26844
+ if (isPricePane) for (const m of scene.indicators.values()) dr = unionRange(dr, this.drawingsRange(modelDrawingSet(m, true), vr, scene.offsetOf(m.id)));
26829
26845
  return dr;
26830
26846
  }
26831
26847
  /** Clear the chrome canvas and draw drawings + axes + current-price line.
@@ -26842,7 +26858,6 @@ void main() {
26842
26858
  const dataW = coords.width;
26843
26859
  const dataH = coords.height;
26844
26860
  this.axisTextColor = surface?.textColor ?? theme.textColor;
26845
- this.labelTips = [];
26846
26861
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
26847
26862
  ctx.clearRect(0, 0, fullW, fullH);
26848
26863
  if (surface && (fullW > dataW || fullH > dataH)) {
@@ -26858,17 +26873,6 @@ void main() {
26858
26873
  return;
26859
26874
  }
26860
26875
  const pricePane = panes.find((p) => p.kind === "price") ?? null;
26861
- for (const pane of panes) {
26862
- if (pane.collapsed) continue;
26863
- for (const m of scene.indicatorsForPane(pane.id)) {
26864
- const sc = scene.scaleFor(m, pane);
26865
- const mp = sc === pane.scale ? pane : { ...pane, scale: sc };
26866
- this.renderDrawings(ctx, coords, this.ownDrawings(m), mp, dataW, scene.offsetOf(m.id));
26867
- }
26868
- }
26869
- if (pricePane) {
26870
- for (const m of scene.indicators.values()) this.renderDrawings(ctx, coords, this.overlayDrawings(m), pricePane, dataW, scene.offsetOf(m.id));
26871
- }
26872
26876
  if (pricePane && !pricePane.collapsed && scene.tradeMarkers.visible) {
26873
26877
  for (const m of scene.indicators.values()) {
26874
26878
  if (m.trades?.length) this.renderTrades(ctx, coords, scene, theme, m.trades, pricePane, dataW);
@@ -26884,25 +26888,6 @@ void main() {
26884
26888
  this.canvas = null;
26885
26889
  this.ctx = null;
26886
26890
  }
26887
- // ── Pine-drawing helpers (own vs force_overlay routing) ──
26888
- ownDrawings(m) {
26889
- return {
26890
- lines: (m.lines ?? []).filter((d) => !d.overlay),
26891
- boxes: (m.boxes ?? []).filter((d) => !d.overlay),
26892
- labels: (m.labels ?? []).filter((d) => !d.overlay),
26893
- polylines: (m.polylines ?? []).filter((d) => !d.overlay),
26894
- linefills: (m.linefills ?? []).filter((d) => !d.overlay)
26895
- };
26896
- }
26897
- overlayDrawings(m) {
26898
- return {
26899
- lines: (m.lines ?? []).filter((d) => d.overlay),
26900
- boxes: (m.boxes ?? []).filter((d) => d.overlay),
26901
- labels: (m.labels ?? []).filter((d) => d.overlay),
26902
- polylines: (m.polylines ?? []).filter((d) => d.overlay),
26903
- linefills: (m.linefills ?? []).filter((d) => d.overlay)
26904
- };
26905
- }
26906
26891
  drawingsRange(set, vr, indexOffset = 0) {
26907
26892
  this.drawScene.setSet(set, indexOffset);
26908
26893
  if (this.drawScene.isEmpty()) return null;
@@ -26936,34 +26921,6 @@ void main() {
26936
26921
  );
26937
26922
  ctx.restore();
26938
26923
  }
26939
- renderDrawings(ctx, coords, set, pane, dataW, indexOffset = 0) {
26940
- this.drawScene.setSet(set, indexOffset);
26941
- if (this.drawScene.isEmpty()) return;
26942
- ctx.save();
26943
- ctx.translate(0, pane.bounds.top);
26944
- ctx.beginPath();
26945
- ctx.rect(0, 0, dataW, pane.bounds.height);
26946
- ctx.clip();
26947
- this.drawScene.render(
26948
- ctx,
26949
- dataW,
26950
- pane.bounds.height,
26951
- (l) => coords.logicalToX(l),
26952
- (price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
26953
- );
26954
- ctx.restore();
26955
- for (const r of this.drawScene.labelTipRegions()) {
26956
- this.labelTips.push({ ...r, top: r.top + pane.bounds.top, bottom: r.bottom + pane.bounds.top });
26957
- }
26958
- }
26959
- /** Tooltip of the topmost label under a plot-space point, or null. Fed by the last render. */
26960
- labelTooltipAt(x, y) {
26961
- for (let i = this.labelTips.length - 1; i >= 0; i -= 1) {
26962
- const r = this.labelTips[i];
26963
- if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return r.text;
26964
- }
26965
- return null;
26966
- }
26967
26924
  // ── axes ──
26968
26925
  drawPriceAxes(ctx, scene, coords, theme, dataW, panes) {
26969
26926
  ctx.strokeStyle = scene.style.borderColor ?? theme.borderColor;
@@ -31372,17 +31329,17 @@ ${overlayScrollbarCss(".vela-sd-pane")}
31372
31329
  return textGlyph(String(glyph), 15);
31373
31330
  }
31374
31331
  function stampSizeIcon(size3) {
31375
- return textGlyph("\u25CF", (SIZE_PX4[String(size3)] ?? 13) + 4);
31332
+ return textGlyph("\u25CF", (SIZE_PX3[String(size3)] ?? 13) + 4);
31376
31333
  }
31377
31334
  function sizeIcon(size3) {
31378
31335
  return textGlyph(String(size3).charAt(0).toUpperCase(), 15);
31379
31336
  }
31380
- var SIZE_PX4 = { small: 10, normal: 13, large: 16, huge: 20 };
31337
+ var SIZE_PX3 = { small: 10, normal: 13, large: 16, huge: 20 };
31381
31338
  function numbersSizeIcon(size3) {
31382
- return textGlyph("12", (SIZE_PX4[String(size3)] ?? 13) - 1, 16.5, 'font-weight="600"');
31339
+ return textGlyph("12", (SIZE_PX3[String(size3)] ?? 13) - 1, 16.5, 'font-weight="600"');
31383
31340
  }
31384
31341
  function labelSizeIcon(size3) {
31385
- return textGlyph("T", (SIZE_PX4[String(size3)] ?? 13) + 2);
31342
+ return textGlyph("T", (SIZE_PX3[String(size3)] ?? 13) + 2);
31386
31343
  }
31387
31344
  function capitalize(s) {
31388
31345
  return s.charAt(0).toUpperCase() + s.slice(1);
@@ -31546,7 +31503,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
31546
31503
  this.root.replaceChildren();
31547
31504
  this.groupCells.clear();
31548
31505
  this.groupIcons.clear();
31549
- this.cursorBtn = this.makeButton(CURSOR_ICON, "Cursor", () => this.onArm(null));
31506
+ this.cursorBtn = this.makeButton(CURSOR_ICON, "Cursor", () => this.onCursorClick());
31550
31507
  this.root.appendChild(this.cursorBtn);
31551
31508
  if (this.def.groups.length > 0) this.root.appendChild(this.divider());
31552
31509
  for (const g of this.def.groups) {
@@ -31650,6 +31607,14 @@ ${overlayScrollbarCss(".vela-sd-pane")}
31650
31607
  this.magnetIcon = icon2;
31651
31608
  return cell;
31652
31609
  }
31610
+ /** Cursor returns to select/idle: an active measure/eraser mode exits through its own
31611
+ * toggle callback (disarming a tool via `onArm(null)` alone can't — the host treats a
31612
+ * null arm as a no-op side effect of entering those modes), then the tool disarms. */
31613
+ onCursorClick() {
31614
+ if (this.measureActive) this.onMeasure();
31615
+ if (this.eraserActive) this.onEraser();
31616
+ this.onArm(null);
31617
+ }
31653
31618
  /** Clicking the icon arms the group's last-used tool (it does NOT open the flyout). */
31654
31619
  onGroupIconClick(group) {
31655
31620
  const type = this.lastUsed.get(group.id) ?? group.tools[0]?.type;
@@ -32981,6 +32946,315 @@ ${overlayScrollbarCss(".vela-sd-pane")}
32981
32946
  }
32982
32947
  };
32983
32948
 
32949
+ // src/renderers/shared/TableOverlay.ts
32950
+ var SIZE_PX4 = {
32951
+ auto: 13,
32952
+ tiny: 10,
32953
+ small: 11,
32954
+ normal: 13,
32955
+ large: 16,
32956
+ huge: 20
32957
+ };
32958
+ function fontPxOf(size3) {
32959
+ if (typeof size3 === "number") return size3 > 0 ? size3 : SIZE_PX4.auto;
32960
+ return SIZE_PX4[size3] ?? SIZE_PX4.auto;
32961
+ }
32962
+ function tableHasContent(t) {
32963
+ return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
32964
+ }
32965
+ function mergeRenderPlan(t) {
32966
+ const span = /* @__PURE__ */ new Map();
32967
+ const omit = /* @__PURE__ */ new Set();
32968
+ for (const m of t.merges) {
32969
+ span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
32970
+ for (let r = m.startRow; r <= m.endRow; r += 1) {
32971
+ for (let c = m.startCol; c <= m.endCol; c += 1) {
32972
+ if (r !== m.startRow || c !== m.startCol) omit.add(`${r}:${c}`);
32973
+ }
32974
+ }
32975
+ }
32976
+ for (let r = 0; r < t.rows; r += 1) {
32977
+ for (let c = 0; c < t.columns; c += 1) {
32978
+ if (t.cells[r]?.[c]?.merged && !span.has(`${r}:${c}`)) omit.add(`${r}:${c}`);
32979
+ }
32980
+ }
32981
+ for (const key of span.keys()) omit.delete(key);
32982
+ return { span, omit };
32983
+ }
32984
+
32985
+ // src/renderers/shared/TableCanvasRenderer.ts
32986
+ var PAD_X = 6;
32987
+ var PAD_Y = 2;
32988
+ var MARGIN = 6;
32989
+ var LINE_HEIGHT = 1.2;
32990
+ function paintTable(ctx, t, args, tips) {
32991
+ if (!tableHasContent(t)) return;
32992
+ const layout = layoutTable(ctx, t, args);
32993
+ if (!layout || layout.w <= 0 || layout.h <= 0) return;
32994
+ const fw = t.frameColor && t.frameWidth > 0 ? t.frameWidth : 0;
32995
+ const { x, y } = anchorOrigin(t.position, layout.w + 2 * fw, layout.h + 2 * fw, args);
32996
+ const x0 = x + fw;
32997
+ const y0 = y + fw;
32998
+ if (t.bgColor) {
32999
+ ctx.fillStyle = t.bgColor;
33000
+ ctx.fillRect(x0, y0, layout.w, layout.h);
33001
+ }
33002
+ if (fw > 0 && t.frameColor) {
33003
+ ctx.strokeStyle = t.frameColor;
33004
+ ctx.lineWidth = fw;
33005
+ ctx.strokeRect(x + fw / 2, y + fw / 2, layout.w + fw, layout.h + fw);
33006
+ }
33007
+ const colX = [0];
33008
+ for (const w of layout.colW) colX.push(colX[colX.length - 1] + w);
33009
+ const rowY = [0];
33010
+ for (const h of layout.rowH) rowY.push(rowY[rowY.length - 1] + h);
33011
+ const prevBaseline = ctx.textBaseline;
33012
+ const prevAlign = ctx.textAlign;
33013
+ ctx.textBaseline = "middle";
33014
+ for (const box of layout.boxes) {
33015
+ const rx = x0 + colX[box.c];
33016
+ const ry = y0 + rowY[box.r];
33017
+ const rw = colX[box.c + box.cs] - colX[box.c];
33018
+ const rh = rowY[box.r + box.rs] - rowY[box.r];
33019
+ const cell = box.cell;
33020
+ if (cell.bgColor) {
33021
+ ctx.fillStyle = cell.bgColor;
33022
+ ctx.fillRect(rx, ry, rw, rh);
33023
+ }
33024
+ const text = cell.text ?? "";
33025
+ if (text.length > 0) {
33026
+ const px = fontPxOf(cell.textSize);
33027
+ ctx.font = cellFont(cell, px, args.theme);
33028
+ ctx.fillStyle = cell.textColor ?? args.theme.textColor;
33029
+ const lines = text.split("\n");
33030
+ const blockH = lines.length * px * LINE_HEIGHT;
33031
+ const blockTop = cell.vAlign === "top" ? ry + PAD_Y : cell.vAlign === "bottom" ? ry + rh - PAD_Y - blockH : ry + (rh - blockH) / 2;
33032
+ const tx = cell.hAlign === "left" ? rx + PAD_X : cell.hAlign === "right" ? rx + rw - PAD_X : rx + rw / 2;
33033
+ ctx.textAlign = cell.hAlign;
33034
+ lines.forEach((line, i) => ctx.fillText(line, tx, blockTop + (i + 0.5) * px * LINE_HEIGHT));
33035
+ }
33036
+ if (cell.tooltip) tips.push({ left: rx, top: ry, right: rx + rw, bottom: ry + rh, text: cell.tooltip });
33037
+ }
33038
+ ctx.textBaseline = prevBaseline;
33039
+ ctx.textAlign = prevAlign;
33040
+ if (t.borderColor && t.borderWidth > 0) {
33041
+ ctx.strokeStyle = t.borderColor;
33042
+ ctx.lineWidth = t.borderWidth;
33043
+ const seen = /* @__PURE__ */ new Set();
33044
+ ctx.beginPath();
33045
+ const edge = (ax, ay, bx, by) => {
33046
+ const key = `${ax},${ay},${bx},${by}`;
33047
+ if (seen.has(key)) return;
33048
+ seen.add(key);
33049
+ ctx.moveTo(ax, ay);
33050
+ ctx.lineTo(bx, by);
33051
+ };
33052
+ for (const box of layout.boxes) {
33053
+ const l = Math.round(x0 + colX[box.c]);
33054
+ const r = Math.round(x0 + colX[box.c + box.cs]);
33055
+ const tp = Math.round(y0 + rowY[box.r]);
33056
+ const bt = Math.round(y0 + rowY[box.r + box.rs]);
33057
+ edge(l, tp, r, tp);
33058
+ edge(l, bt, r, bt);
33059
+ edge(l, tp, l, bt);
33060
+ edge(r, tp, r, bt);
33061
+ }
33062
+ ctx.stroke();
33063
+ }
33064
+ }
33065
+ function layoutTable(ctx, t, args) {
33066
+ const { span, omit } = mergeRenderPlan(t);
33067
+ const colW = new Array(t.columns).fill(0);
33068
+ const rowH = new Array(t.rows).fill(0);
33069
+ const boxes = [];
33070
+ for (let r = 0; r < t.rows; r += 1) {
33071
+ for (let c = 0; c < t.columns; c += 1) {
33072
+ if (omit.has(`${r}:${c}`)) continue;
33073
+ const cell = t.cells[r]?.[c];
33074
+ if (cell == null) continue;
33075
+ const sp = span.get(`${r}:${c}`);
33076
+ boxes.push({ cell, r, c, cs: Math.min(sp?.cs ?? 1, t.columns - c), rs: Math.min(sp?.rs ?? 1, t.rows - r) });
33077
+ }
33078
+ }
33079
+ if (boxes.length === 0) return null;
33080
+ const sizeOf = (cell) => {
33081
+ const px = fontPxOf(cell.textSize);
33082
+ ctx.font = cellFont(cell, px, args.theme);
33083
+ const lines = (cell.text ?? "").split("\n");
33084
+ let maxW = 0;
33085
+ for (const line of lines) maxW = Math.max(maxW, ctx.measureText(line).width);
33086
+ let w2 = Math.ceil(maxW) + 2 * PAD_X;
33087
+ let h2 = Math.ceil(lines.length * px * LINE_HEIGHT) + 2 * PAD_Y;
33088
+ if (cell.width) w2 = Math.max(w2, cell.width / 100 * args.plotWidth);
33089
+ if (cell.height) h2 = Math.max(h2, cell.height / 100 * args.paneHeight);
33090
+ return { w: w2, h: h2 };
33091
+ };
33092
+ const spanning = [];
33093
+ for (const box of boxes) {
33094
+ const { w: w2, h: h2 } = sizeOf(box.cell);
33095
+ if (box.cs === 1) colW[box.c] = Math.max(colW[box.c], w2);
33096
+ if (box.rs === 1) rowH[box.r] = Math.max(rowH[box.r], h2);
33097
+ if (box.cs > 1 || box.rs > 1) spanning.push({ box, w: w2, h: h2 });
33098
+ }
33099
+ for (const { box, w: w2, h: h2 } of spanning) {
33100
+ if (box.cs > 1) {
33101
+ let sum = 0;
33102
+ for (let c = box.c; c < box.c + box.cs; c += 1) sum += colW[c];
33103
+ if (w2 > sum) for (let c = box.c; c < box.c + box.cs; c += 1) colW[c] += (w2 - sum) / box.cs;
33104
+ }
33105
+ if (box.rs > 1) {
33106
+ let sum = 0;
33107
+ for (let r = box.r; r < box.r + box.rs; r += 1) sum += rowH[r];
33108
+ if (h2 > sum) for (let r = box.r; r < box.r + box.rs; r += 1) rowH[r] += (h2 - sum) / box.rs;
33109
+ }
33110
+ }
33111
+ let w = 0;
33112
+ for (const cw of colW) w += cw;
33113
+ let h = 0;
33114
+ for (const rh of rowH) h += rh;
33115
+ return { colW, rowH, w, h, boxes };
33116
+ }
33117
+ function anchorOrigin(position, totalW, totalH, args) {
33118
+ let y;
33119
+ if (position.startsWith("top")) y = MARGIN;
33120
+ else if (position.startsWith("bottom")) y = args.paneHeight - MARGIN - totalH;
33121
+ else y = args.paneHeight / 2 - totalH / 2;
33122
+ let x;
33123
+ if (position.endsWith("left")) x = MARGIN;
33124
+ else if (position.endsWith("right")) x = args.plotWidth - MARGIN - totalW;
33125
+ else x = args.plotWidth / 2 - totalW / 2;
33126
+ return { x, y };
33127
+ }
33128
+ function cellFont(cell, px, theme) {
33129
+ const family = cell.fontFamily === "monospace" ? "monospace" : theme.fontFamily || "sans-serif";
33130
+ return `${cell.italic ? "italic " : ""}${cell.bold ? "bold " : ""}${px}px ${family}`;
33131
+ }
33132
+
33133
+ // src/renderers/native/drawings/IndicatorDrawingSlices.ts
33134
+ function indicatorSliceKey(z, boundaries) {
33135
+ return boundaries.find((b) => b > z) ?? Infinity;
33136
+ }
33137
+ var IndicatorDrawingSlices = class {
33138
+ constructor() {
33139
+ this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
33140
+ /** Slice canvas cache, keyed `paneId|beforeZ` — same lifecycle as the user-drawing cache. */
33141
+ this.sliceCache = /* @__PURE__ */ new Map();
33142
+ /** Tooltip hit-rects of every label drawn this frame, in plot coords (rebuilt per prepare). */
33143
+ this.tips = [];
33144
+ }
33145
+ /**
33146
+ * Rebuild the per-indicator drawing slices for this data frame. `ref` is the data
33147
+ * canvas the slices must match pixel-for-pixel (the backend composites them 1:1).
33148
+ * Runs from the renderer's data paint, just before the backend composites the scene.
33149
+ */
33150
+ prepare(scene, coords, theme, ref2) {
33151
+ this.tips = [];
33152
+ const out = /* @__PURE__ */ new Map();
33153
+ if (ref2.width === 0 || ref2.height === 0) {
33154
+ this.sliceCache.clear();
33155
+ return out;
33156
+ }
33157
+ this.drawScene.setDeps({
33158
+ timeToLogical: (ms) => coords.timeToLogical(ms),
33159
+ barAt: (logical) => {
33160
+ const b = scene.bars[Math.round(logical)];
33161
+ return b ? { high: b.high, low: b.low } : null;
33162
+ },
33163
+ theme
33164
+ });
33165
+ const dpr = coords.dpr;
33166
+ const dataW = coords.width;
33167
+ const buckets = /* @__PURE__ */ new Map();
33168
+ const add = (paneId, beforeZ, entry) => {
33169
+ const key = `${paneId}|${beforeZ}`;
33170
+ const bucket = buckets.get(key);
33171
+ if (bucket) bucket.entries.push(entry);
33172
+ else buckets.set(key, { paneId, beforeZ, entries: [entry] });
33173
+ };
33174
+ for (const pane of scene.orderedPanes()) {
33175
+ if (pane.collapsed) continue;
33176
+ const boundaries = scene.seriesBoundaries(pane.id);
33177
+ for (const m of scene.orderedIndicatorsForPane(pane.id)) {
33178
+ const set = modelDrawingSet(m, false);
33179
+ const tables = (m.tables ?? []).filter((t) => !t.overlay);
33180
+ if (drawingSetEmpty(set) && tables.length === 0) continue;
33181
+ const sc = scene.scaleFor(m, pane);
33182
+ const mp = sc === pane.scale ? pane : { ...pane, scale: sc };
33183
+ const beforeZ = indicatorSliceKey(scene.zOf(m.id), boundaries);
33184
+ add(pane.id, beforeZ, { set, tables, pane: mp, indexOffset: scene.offsetOf(m.id) });
33185
+ }
33186
+ if (pane.kind === "price") {
33187
+ for (const m of scene.indicators.values()) {
33188
+ const set = modelDrawingSet(m, true);
33189
+ const tables = (m.tables ?? []).filter((t) => t.overlay === true);
33190
+ if (drawingSetEmpty(set) && tables.length === 0) continue;
33191
+ add(pane.id, Infinity, { set, tables, pane, indexOffset: scene.offsetOf(m.id) });
33192
+ }
33193
+ }
33194
+ }
33195
+ for (const [key, { paneId, beforeZ, entries }] of buckets) {
33196
+ let canvas = this.sliceCache.get(key);
33197
+ if (!canvas) {
33198
+ canvas = document.createElement("canvas");
33199
+ this.sliceCache.set(key, canvas);
33200
+ }
33201
+ if (canvas.width !== ref2.width || canvas.height !== ref2.height) {
33202
+ canvas.width = ref2.width;
33203
+ canvas.height = ref2.height;
33204
+ }
33205
+ const ctx = canvas.getContext("2d");
33206
+ if (!ctx) continue;
33207
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
33208
+ ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
33209
+ for (const e of entries) this.paintEntry(ctx, e, coords, dataW, theme);
33210
+ const slices = out.get(paneId) ?? [];
33211
+ slices.push({ beforeZ, canvas });
33212
+ out.set(paneId, slices);
33213
+ }
33214
+ for (const key of [...this.sliceCache.keys()]) if (!buckets.has(key)) this.sliceCache.delete(key);
33215
+ for (const slices of out.values()) slices.sort((a, b) => a.beforeZ - b.beforeZ);
33216
+ return out;
33217
+ }
33218
+ paintEntry(ctx, e, coords, dataW, theme) {
33219
+ const { pane } = e;
33220
+ const paneTips = [];
33221
+ ctx.save();
33222
+ ctx.translate(0, pane.bounds.top);
33223
+ ctx.beginPath();
33224
+ ctx.rect(0, 0, dataW, pane.bounds.height);
33225
+ ctx.clip();
33226
+ this.drawScene.setSet(e.set, e.indexOffset);
33227
+ this.drawScene.render(
33228
+ ctx,
33229
+ dataW,
33230
+ pane.bounds.height,
33231
+ (l) => coords.logicalToX(l),
33232
+ (price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
33233
+ );
33234
+ paneTips.push(...this.drawScene.labelTipRegions());
33235
+ for (const t of e.tables) paintTable(ctx, t, { paneHeight: pane.bounds.height, plotWidth: dataW, theme }, paneTips);
33236
+ ctx.restore();
33237
+ for (const r of paneTips) {
33238
+ this.tips.push({ ...r, top: r.top + pane.bounds.top, bottom: r.bottom + pane.bounds.top });
33239
+ }
33240
+ }
33241
+ /** Tooltip of the topmost label or table cell under a plot-space point, or null. Fed by the last prepare. */
33242
+ labelTooltipAt(x, y) {
33243
+ for (let i = this.tips.length - 1; i >= 0; i -= 1) {
33244
+ const r = this.tips[i];
33245
+ if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return r.text;
33246
+ }
33247
+ return null;
33248
+ }
33249
+ };
33250
+ function mergeSlices(indicator, user) {
33251
+ const out = /* @__PURE__ */ new Map();
33252
+ for (const [paneId, slices] of indicator) out.set(paneId, [...slices]);
33253
+ for (const [paneId, slices] of user) out.set(paneId, [...out.get(paneId) ?? [], ...slices]);
33254
+ for (const slices of out.values()) slices.sort((a, b) => a.beforeZ - b.beforeZ);
33255
+ return out;
33256
+ }
33257
+
32984
33258
  // src/renderers/native/drawings/Projector.ts
32985
33259
  function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
32986
33260
  return {
@@ -33860,6 +34134,11 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33860
34134
  this.vpvrRenderer = new VpvrRenderer();
33861
34135
  this.resizeObserver = null;
33862
34136
  this.dprMedia = null;
34137
+ /** Plot size in INTEGER device px, as last reported by the resize observer's
34138
+ * device-pixel-content-box — the browser's own statement of how many device pixels
34139
+ * it paints the plot into. `null` until the first report or where the box type is
34140
+ * unsupported (WebKit); syncSize then falls back to rounding the client rect. */
34141
+ this.plotDeviceSize = null;
33863
34142
  this.coords = new CoordinateSystem();
33864
34143
  this.scene = new SceneGraph();
33865
34144
  // chosen at mount (WebGL2 if available, else canvas2d)
@@ -33867,6 +34146,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33867
34146
  this.glowAmount = 0;
33868
34147
  // WebGL2 neon-glow intensity (canvas2d ignores it)
33869
34148
  this.chrome = new ChromeRenderer();
34149
+ /** Prepaints each indicator's Pine drawings into interleave slices at the model's z. */
34150
+ this.indicatorSlices = new IndicatorDrawingSlices();
33870
34151
  /** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
33871
34152
  this.labelTooltip = null;
33872
34153
  this.crosshairLayer = new CrosshairRenderer();
@@ -34015,7 +34296,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34015
34296
  this.toggleVisibleCbs = /* @__PURE__ */ new Set();
34016
34297
  this.moveIndicatorCbs = /* @__PURE__ */ new Set();
34017
34298
  this.priceStyleCbs = /* @__PURE__ */ new Set();
34018
- this.tableOverlays = /* @__PURE__ */ new Map();
34019
34299
  this.name = "native";
34020
34300
  this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "intro", "zoomAnchor", "axisDrag", "paneResize", "candleZOrder", "candleVisible", "seriesOrder", "highlights", "sessionZones", "gridlines", "axisLabels", "scaleMode", "invertScale", "paneScales", "autoScale", "timezone", "keyboard", "historyChords", "priceStyle", "priceBaseline", "baselinePrice", "settings", "attribution", "dialogHost", "tradeMarkers", "indicatorTitles", "indicatorValues"];
34021
34301
  /** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
@@ -34329,7 +34609,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34329
34609
  * hidden) on clear so a re-show picks up the current theme.
34330
34610
  */
34331
34611
  setLoading(loading) {
34332
- for (const overlay of this.tableOverlays.values()) overlay.setVisible(!loading);
34333
34612
  if (!loading || !this.wrapper) {
34334
34613
  this.loadingEl?.remove();
34335
34614
  this.loadingEl = null;
@@ -34991,7 +35270,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34991
35270
  this.plot.addEventListener("pointerleave", this.onScrollProximityLeave);
34992
35271
  this.labelTooltip = new LabelTooltip(this.plot, {
34993
35272
  theme: () => this.chromeTheme(),
34994
- lookup: (x, y) => this.chrome.labelTooltipAt(x, y)
35273
+ lookup: (x, y) => this.indicatorSlices.labelTooltipAt(x, y)
34995
35274
  });
34996
35275
  this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
34997
35276
  projector: () => this.drawingProjector(),
@@ -35076,6 +35355,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35076
35355
  this.emitPaneAction({ type: "maximize", paneId, maximized });
35077
35356
  }
35078
35357
  });
35358
+ this.paneControls.setSuspended(this.layoutMode === "mobile");
35079
35359
  this.axisScaleButtons = new AxisScaleButtons(this.plot, theme, {
35080
35360
  panes: () => this.axisScaleViews(),
35081
35361
  rightAxis: () => this.rightAxisW,
@@ -35085,8 +35365,19 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35085
35365
  if (pane) this.setPaneLog(paneId, !paneLogScale(this.scene, pane));
35086
35366
  }
35087
35367
  });
35088
- this.resizeObserver = new ResizeObserver(() => this.resize());
35368
+ this.resizeObserver = new ResizeObserver((entries) => {
35369
+ for (const e of entries) {
35370
+ if (e.target !== this.plot) continue;
35371
+ const s = e.devicePixelContentBoxSize?.[0];
35372
+ if (s) this.plotDeviceSize = { width: s.inlineSize, height: s.blockSize };
35373
+ }
35374
+ this.resize();
35375
+ });
35089
35376
  this.resizeObserver.observe(this.wrapper);
35377
+ try {
35378
+ this.resizeObserver.observe(this.plot, { box: "device-pixel-content-box" });
35379
+ } catch {
35380
+ }
35090
35381
  this.watchDpr();
35091
35382
  this.syncSize();
35092
35383
  }
@@ -35211,8 +35502,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35211
35502
  this.inputsUI?.destroy();
35212
35503
  this.paneControls?.destroy();
35213
35504
  this.axisScaleButtons?.destroy();
35214
- for (const overlay of this.tableOverlays.values()) overlay.destroy();
35215
- this.tableOverlays.clear();
35216
35505
  this.resizeObserver?.disconnect();
35217
35506
  this.resizeObserver = null;
35218
35507
  this.dprMedia?.removeEventListener("change", this.onDprChange);
@@ -35241,6 +35530,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35241
35530
  this.attributionEl = null;
35242
35531
  this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
35243
35532
  this.mountContainer?.style.removeProperty("--vela-scale-gutter");
35533
+ this.mountContainer?.style.removeProperty("--vela-bottom-gutter");
35244
35534
  this.mountContainer?.style.removeProperty("--vela-price-pane-top");
35245
35535
  this.mountContainer?.style.removeProperty("--vela-price-pane-bottom");
35246
35536
  this.mountContainer = null;
@@ -35333,7 +35623,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35333
35623
  ensurePane(pane) {
35334
35624
  this.scene.ensurePane(pane.id, pane.kind, pane.order, pane.heightWeight ?? (pane.kind === "price" ? 3 : 1));
35335
35625
  this.layoutPanes();
35336
- this.repositionTables();
35337
35626
  this.paneControls?.refresh();
35338
35627
  this.scheduler.invalidate(4 /* Full */);
35339
35628
  }
@@ -35355,17 +35644,14 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35355
35644
  if (!model.ownScale) this.scene.dropIndicatorScale(handle.id);
35356
35645
  this.inputsUI.setPane(handle.id, paneId);
35357
35646
  this.refreshAnchorOffset(model);
35358
- this.syncTables(model);
35359
35647
  this.refreshAxisWidth();
35360
35648
  this.layoutPanes();
35361
- this.repositionTables();
35362
35649
  this.paneControls?.refresh();
35363
35650
  this.scheduler.invalidate(4 /* Full */);
35364
35651
  }
35365
35652
  orderPanes(orderedIds) {
35366
35653
  this.scene.orderPanes(orderedIds);
35367
35654
  this.layoutPanes();
35368
- this.repositionTables();
35369
35655
  this.paneControls?.refresh();
35370
35656
  this.scheduler.invalidate(4 /* Full */);
35371
35657
  }
@@ -35374,7 +35660,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35374
35660
  if (!pane || pane.collapsed === collapsed) return;
35375
35661
  pane.collapsed = collapsed;
35376
35662
  this.layoutPanes();
35377
- this.repositionTables();
35378
35663
  this.paneControls?.refresh();
35379
35664
  this.scheduler.invalidate(4 /* Full */);
35380
35665
  }
@@ -35382,7 +35667,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35382
35667
  if (paneId !== null && !this.scene.panes.has(paneId)) paneId = null;
35383
35668
  this.maximizedPaneId = paneId;
35384
35669
  this.layoutPanes();
35385
- this.repositionTables();
35386
35670
  this.paneControls?.refresh();
35387
35671
  this.scheduler.invalidate(4 /* Full */);
35388
35672
  }
@@ -35469,7 +35753,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35469
35753
  native: !!model.native,
35470
35754
  ...model.props ? { props: model.props, propValues: model.propValues ?? {} } : {}
35471
35755
  });
35472
- this.syncTables(model);
35473
35756
  if (model.native?.type === "volume") {
35474
35757
  this.volumeActive = true;
35475
35758
  this.volumeHidden = false;
@@ -35493,7 +35776,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35493
35776
  }
35494
35777
  }
35495
35778
  applyPatch(model, patch);
35496
- this.syncTables(model);
35497
35779
  this.scheduler.invalidate(3 /* Light */);
35498
35780
  }
35499
35781
  removeIndicator(handle) {
@@ -35512,8 +35794,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35512
35794
  this.scene.forgetAnchorOffset(handle.id);
35513
35795
  this.scene.dropIndicatorScale(handle.id);
35514
35796
  this.inputsUI.remove(handle.id);
35515
- this.tableOverlays.get(handle.id)?.destroy();
35516
- this.tableOverlays.delete(handle.id);
35517
35797
  this.refreshAxisWidth();
35518
35798
  this.paneControls?.refresh();
35519
35799
  this.scheduler.invalidate(4 /* Full */);
@@ -35557,8 +35837,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35557
35837
  }
35558
35838
  if (!visible) {
35559
35839
  this.scene.indicators.delete(handle.id);
35560
- this.tableOverlays.get(handle.id)?.destroy();
35561
- this.tableOverlays.delete(handle.id);
35562
35840
  }
35563
35841
  this.inputsUI.setVisible(handle.id, visible);
35564
35842
  this.scheduler.invalidate(4 /* Full */);
@@ -35604,6 +35882,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35604
35882
  this.userDrawings?.setLayoutMode(mode);
35605
35883
  this.settingsDialog?.setLayoutMode(mode);
35606
35884
  this.inputsUI?.setLayoutMode(mode);
35885
+ this.paneControls?.setSuspended(mode === "mobile");
35607
35886
  if (this.scrollButton) {
35608
35887
  const px = mode === "mobile" ? SCROLL_BTN_SIZE_TOUCH : SCROLL_BTN_SIZE;
35609
35888
  this.scrollButton.style.width = `${px}px`;
@@ -35999,7 +36278,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35999
36278
  /** Relayout + repaint + refresh the hover buttons after a collapse/maximize/order change. */
36000
36279
  afterPaneLayoutChange() {
36001
36280
  this.layoutPanes();
36002
- this.repositionTables();
36003
36281
  this.paneControls?.refresh();
36004
36282
  this.scheduler.invalidate(4 /* Full */);
36005
36283
  }
@@ -36080,7 +36358,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36080
36358
  above.heightWeight = next2.above;
36081
36359
  below.heightWeight = next2.below;
36082
36360
  this.layoutPanes();
36083
- this.repositionTables();
36084
36361
  this.scheduler.invalidate(4 /* Full */);
36085
36362
  }
36086
36363
  /** Double-click a separator → split the two adjacent panes evenly (each gets half of
@@ -36095,7 +36372,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36095
36372
  above.heightWeight = half;
36096
36373
  below.heightWeight = half;
36097
36374
  this.layoutPanes();
36098
- this.repositionTables();
36099
36375
  this.scheduler.invalidate(4 /* Full */);
36100
36376
  }
36101
36377
  // ── keyboard navigation / accessibility (item 11) ──
@@ -36374,7 +36650,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36374
36650
  const liveActual = li >= 0 ? this.bars[li] : void 0;
36375
36651
  const easeLive = !!liveActual && this.liveEaseTime === liveActual.time && (liveActual.high !== this.liveEaseHigh || liveActual.low !== this.liveEaseLow || liveActual.close !== this.liveEaseClose);
36376
36652
  if (easeLive && liveActual) this.bars[li] = { ...liveActual, high: this.liveEaseHigh, low: this.liveEaseLow, close: this.liveEaseClose };
36377
- this.scene.drawingSlices = this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map();
36653
+ this.scene.drawingSlices = mergeSlices(
36654
+ this.indicatorSlices.prepare(this.scene, this.coords, this.theme, this.dataCanvas),
36655
+ this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map()
36656
+ );
36378
36657
  this.backdropRenderer.render(this.scene, this.coords, this.theme, gridAlpha);
36379
36658
  this.backend.render(this.scene, this.coords, this.theme);
36380
36659
  this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
@@ -36812,27 +37091,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36812
37091
  }
36813
37092
  return maxVol;
36814
37093
  }
36815
- /** Create/update/destroy an indicator's DOM table overlay (anchored off real pane geometry). */
36816
- syncTables(model) {
36817
- const tables = model.tables ?? [];
36818
- let overlay = this.tableOverlays.get(model.id);
36819
- if (tables.length === 0) {
36820
- if (overlay) {
36821
- overlay.destroy();
36822
- this.tableOverlays.delete(model.id);
36823
- }
36824
- return;
36825
- }
36826
- if (!overlay) {
36827
- overlay = new TableOverlay(this.plot, this.theme, (id) => this.paneBoundsFor(id));
36828
- overlay.setVisible(this.loadingEl === null);
36829
- this.tableOverlays.set(model.id, overlay);
36830
- }
36831
- overlay.update(tables);
36832
- }
36833
- repositionTables() {
36834
- for (const overlay of this.tableOverlays.values()) overlay.reposition();
36835
- }
36836
37094
  layoutPanes() {
36837
37095
  const panes = this.scene.orderedPanes();
36838
37096
  const dataHeight = this.coords.height;
@@ -36883,6 +37141,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36883
37141
  const visible = maxPane ? [maxPane] : this.scene.orderedPanes().filter((p) => !p.collapsed);
36884
37142
  const paneBottom = visible.length ? Math.max(...visible.map((p) => p.bounds.top + p.bounds.height)) : dataHeight;
36885
37143
  this.scrollBtnBottomPx = SCROLL_BTN_BOTTOM + Math.max(0, dataHeight - paneBottom);
37144
+ this.mountContainer?.style.setProperty("--vela-bottom-gutter", `${TIME_AXIS_H + Math.max(0, dataHeight - paneBottom)}px`);
36886
37145
  this.scrollBtnRightPx = this.rightAxisW + SCROLL_BTN_RIGHT_INSET;
36887
37146
  if (this.scrollButton) {
36888
37147
  this.scrollButton.style.bottom = `${this.scrollBtnBottomPx}px`;
@@ -36975,30 +37234,33 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36975
37234
  if (w <= 0 || h <= 0) return;
36976
37235
  const dpr = window.devicePixelRatio || 1;
36977
37236
  this.plot.style.left = `${this.toolbarGutter}px`;
36978
- const pw = Math.max(1, w - this.toolbarGutter);
36979
- const ph = h;
36980
- this.dataCanvas.width = Math.round(pw * dpr);
36981
- this.dataCanvas.height = Math.round(ph * dpr);
36982
- this.backdropCanvas.width = this.dataCanvas.width;
36983
- this.backdropCanvas.height = this.dataCanvas.height;
36984
- this.volumeCanvas.width = this.dataCanvas.width;
36985
- this.volumeCanvas.height = this.dataCanvas.height;
36986
- for (const l of this.extLayers) {
36987
- l.canvas.width = this.dataCanvas.width;
36988
- l.canvas.height = this.dataCanvas.height;
36989
- }
36990
- this.vpvrCanvas.width = this.dataCanvas.width;
36991
- this.vpvrCanvas.height = this.dataCanvas.height;
36992
- this.chromeCanvas.width = this.dataCanvas.width;
36993
- this.chromeCanvas.height = this.dataCanvas.height;
36994
- this.drawingsCanvas.width = this.dataCanvas.width;
36995
- this.drawingsCanvas.height = this.dataCanvas.height;
36996
- this.cursorCanvas.width = this.dataCanvas.width;
36997
- this.cursorCanvas.height = this.dataCanvas.height;
37237
+ const rect = this.plot.getBoundingClientRect();
37238
+ let bw = Math.max(1, Math.round(rect.width * dpr));
37239
+ let bh = Math.max(1, Math.round(rect.height * dpr));
37240
+ const dev = this.plotDeviceSize;
37241
+ if (dev && Math.abs(dev.width - rect.width * dpr) <= 1 && Math.abs(dev.height - rect.height * dpr) <= 1) {
37242
+ bw = Math.max(1, dev.width);
37243
+ bh = Math.max(1, dev.height);
37244
+ }
37245
+ const pw = bw / dpr;
37246
+ const ph = bh / dpr;
37247
+ const size3 = (canvas) => {
37248
+ canvas.width = bw;
37249
+ canvas.height = bh;
37250
+ canvas.style.width = `${pw}px`;
37251
+ canvas.style.height = `${ph}px`;
37252
+ };
37253
+ size3(this.dataCanvas);
37254
+ size3(this.backdropCanvas);
37255
+ size3(this.volumeCanvas);
37256
+ for (const l of this.extLayers) size3(l.canvas);
37257
+ size3(this.vpvrCanvas);
37258
+ size3(this.chromeCanvas);
37259
+ size3(this.drawingsCanvas);
37260
+ size3(this.cursorCanvas);
36998
37261
  this.coords.setSize(Math.max(1, pw - this.rightAxisW), Math.max(1, ph - TIME_AXIS_H), dpr);
36999
37262
  this.scene.crosshair = null;
37000
37263
  this.layoutPanes();
37001
- this.repositionTables();
37002
37264
  this.userDrawings?.onResize();
37003
37265
  if (!this.didInitialFit && this.coords.barCount > 0) {
37004
37266
  this.fitContent();