@luxalgo/vela 0.6.9 → 0.6.11

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 (46) hide show
  1. package/dist/{DataProvider-8Z95Q-RJ.d.cts → DataProvider-BBf-jc6W.d.ts} +49 -1
  2. package/dist/{DataProvider-DExJrfut.d.ts → DataProvider-p0TEyhlX.d.cts} +49 -1
  3. package/dist/{chunk-6WDDVMBJ.js → chunk-73PEA4MU.js} +707 -353
  4. package/dist/{chunk-62SVGONC.js → chunk-G77Y7LK2.js} +2 -2
  5. package/dist/{chunk-STHSKXOR.js → chunk-IO3NYSQV.js} +650 -157
  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-EQCHJZOT.js → chunk-MTLJKZDZ.js} +10 -2
  9. package/dist/{contributions-D7PVZO2i.d.ts → contributions-Bbe2R-mQ.d.ts} +20 -6
  10. package/dist/{contributions-C1U2Krwg.d.cts → contributions-CO01zWve.d.cts} +20 -6
  11. package/dist/index.cjs +719 -350
  12. package/dist/index.d.cts +37 -10
  13. package/dist/index.d.ts +37 -10
  14. package/dist/index.js +5 -5
  15. package/dist/{options-FM0peknS.d.ts → options-yp7sA96q.d.cts} +14 -7
  16. package/dist/{options-FM0peknS.d.cts → options-yp7sA96q.d.ts} +14 -7
  17. package/dist/{plugin-DfVqBz9p.d.cts → plugin-CkkH8QnX.d.cts} +3 -3
  18. package/dist/{plugin-7bkF32Rk.d.ts → plugin-DwxjM3Ni.d.ts} +3 -3
  19. package/dist/plugin.cjs +12 -1
  20. package/dist/plugin.d.cts +4 -4
  21. package/dist/plugin.d.ts +4 -4
  22. package/dist/plugin.js +3 -3
  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-DOPiT6I6.d.cts → statusline-CHPDuKNp.d.ts} +13 -5
  30. package/dist/{statusline-zdF4eZLr.d.ts → statusline-DTHZUFqK.d.cts} +13 -5
  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 +719 -350
  36. package/dist/vela.global.min.js +51 -49
  37. package/dist/widget.cjs +1249 -388
  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 +1249 -388
  42. package/dist/workspace.d.cts +103 -10
  43. package/dist/workspace.d.ts +103 -10
  44. package/dist/workspace.js +6 -6
  45. package/package.json +1 -1
  46. /package/dist/{chunk-AOGZBKUE.js → chunk-H26BEHF4.js} +0 -0
@@ -130,6 +130,12 @@ var Vela = (function (exports) {
130
130
  get visible() {
131
131
  return this.visibleState;
132
132
  }
133
+ inputValues() {
134
+ return this.controller.inputValuesOf(this.id);
135
+ }
136
+ propValues() {
137
+ return this.controller.propValuesOf(this.id);
138
+ }
133
139
  setInput(key, value) {
134
140
  this.controller.applyInputs(this.id, { [key]: value });
135
141
  }
@@ -528,6 +534,8 @@ var Vela = (function (exports) {
528
534
  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
535
  registerIcon("collapse", S('<path d="M3 8h10"/>'));
530
536
  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"/>'));
537
+ registerIcon("plus", S('<path d="M8 2.8v10.4M2.8 8h10.4"/>'));
538
+ registerIcon("minus", S('<path d="M2.8 8h10.4"/>'));
531
539
  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
540
  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
541
  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 +543,7 @@ var Vela = (function (exports) {
535
543
  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
544
  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
545
  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"/>'));
546
+ 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
547
  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
548
  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
549
  registerIcon("cursor", svg24('<path d="M5 3l6 16 2-6 6-2z"/>', 'fill="currentColor" stroke="none"'));
@@ -6906,6 +6914,23 @@ var Vela = (function (exports) {
6906
6914
  return 36e5;
6907
6915
  }
6908
6916
 
6917
+ // src/data/symbol-groups.ts
6918
+ function isGroupRow(d) {
6919
+ return d.group != null && d.ticker === d.group;
6920
+ }
6921
+ function groupKeyOf(d) {
6922
+ return `${(d.prefix ?? d.provider ?? "").toLowerCase()}:${(d.group ?? "").toUpperCase()}`;
6923
+ }
6924
+ function groupMembers(pool, groupRow) {
6925
+ const key = groupKeyOf(groupRow);
6926
+ return pool.filter((s) => s.group != null && !isGroupRow(s) && groupKeyOf(s) === key);
6927
+ }
6928
+ function defaultMemberOf(pool, groupRow) {
6929
+ const members = groupMembers(pool, groupRow);
6930
+ const defaults2 = members.filter((m) => m.default);
6931
+ return (defaults2.length === 1 ? defaults2[0] : members[0]) ?? null;
6932
+ }
6933
+
6909
6934
  // src/data/ProviderRegistry.ts
6910
6935
  var normName = (n) => n.trim().toLowerCase();
6911
6936
  var normTicker = (t) => t.trim().toUpperCase();
@@ -6995,17 +7020,24 @@ var Vela = (function (exports) {
6995
7020
  resolve(raw, opts = {}) {
6996
7021
  const { provider, ticker } = this.parse(raw);
6997
7022
  if (provider) {
6998
- if (this.entries.has(provider)) return { provider, ticker };
7023
+ if (this.entries.has(provider)) {
7024
+ const d = this.entries.get(provider).byTicker?.get(normTicker(ticker));
7025
+ return { provider, ticker: d != null && isGroupRow(d) ? this.loadableTicker(provider, d) : ticker };
7026
+ }
6999
7027
  const key = `${provider}:${normTicker(ticker)}`;
7000
7028
  for (const name of this.candidateOrder(opts.default)) {
7001
7029
  const d = this.entries.get(name).prefixIndex?.get(key);
7002
- if (d) return { provider: name, ticker: d.ticker };
7030
+ if (d) return { provider: name, ticker: this.loadableTicker(name, d) };
7003
7031
  }
7004
7032
  return null;
7005
7033
  }
7006
7034
  const norm = normTicker(ticker);
7007
7035
  for (const name of this.candidateOrder(opts.default)) {
7008
- if (this.entries.get(name).index?.has(norm)) return { provider: name, ticker };
7036
+ const e = this.entries.get(name);
7037
+ if (e.index?.has(norm)) {
7038
+ const d = e.byTicker?.get(norm);
7039
+ return { provider: name, ticker: d != null && isGroupRow(d) ? this.loadableTicker(name, d) : ticker };
7040
+ }
7009
7041
  }
7010
7042
  if (opts.lenient && this.entries.size === 1) {
7011
7043
  const only = this.names()[0];
@@ -7014,6 +7046,13 @@ var Vela = (function (exports) {
7014
7046
  }
7015
7047
  return null;
7016
7048
  }
7049
+ /** What a resolved descriptor LOADS: itself — unless it is a GROUP row (listed, never
7050
+ * served), which loads its default member (single `default`, else first listed). A
7051
+ * memberless group keeps its own ticker and fails downstream like any unknown symbol. */
7052
+ loadableTicker(name, d) {
7053
+ if (!isGroupRow(d)) return d.ticker;
7054
+ return defaultMemberOf(this.entries.get(name)?.descriptors ?? [], d)?.ticker ?? d.ticker;
7055
+ }
7017
7056
  /**
7018
7057
  * Resolve now if possible, else resolve later — re-attempting after each provider
7019
7058
  * settles. The promise stays pending until some registered provider can serve the
@@ -7239,6 +7278,7 @@ var Vela = (function (exports) {
7239
7278
  var PREVIEW_BARS = 300;
7240
7279
  var SINGLE_LOAD_BARS = 5e3;
7241
7280
  var CHUNK_BARS = 1e4;
7281
+ var FIRST_PAINT_BARS = 100;
7242
7282
  var GAP_FACTOR = 1.5;
7243
7283
  var HEAL_COOLDOWN_MS = 5e3;
7244
7284
  var EngineOrchestrator = class _EngineOrchestrator {
@@ -7302,6 +7342,10 @@ var Vela = (function (exports) {
7302
7342
  /** Invalidates detached async work (backfill loops, in-flight loads, gap heals):
7303
7343
  * bumped by init(), setMarket() and destroy(). */
7304
7344
  this.generation = 0;
7345
+ /** Aborts the in-flight PROGRESSIVE load's source polling on supersession — an
7346
+ * abandoned stream left polling to its own budget starves the browser's per-host
7347
+ * connection pool, and the NEXT symbol's very first fetch with it (measured). */
7348
+ this.progressiveAbort = null;
7305
7349
  /** Awaiters racing a superseded load (setMarket callers) — released on every bump so they never hang. */
7306
7350
  this.supersedeWaiters = [];
7307
7351
  /** `history:complete` fired for the CURRENT load. Each market load re-arms the cycle
@@ -7444,6 +7488,8 @@ var Vela = (function (exports) {
7444
7488
  * superseded setMarket awaiters so their promises resolve instead of hanging. */
7445
7489
  bumpGeneration() {
7446
7490
  const gen = ++this.generation;
7491
+ this.progressiveAbort?.abort();
7492
+ this.progressiveAbort = null;
7447
7493
  for (const w of this.supersedeWaiters.splice(0)) w();
7448
7494
  return gen;
7449
7495
  }
@@ -7497,7 +7543,48 @@ var Vela = (function (exports) {
7497
7543
  const requested = market.bars ?? 500;
7498
7544
  const initialRange = market.visibleRange;
7499
7545
  const deep = !market.data?.length && initialRange == null && requested > SINGLE_LOAD_BARS;
7500
- if (deep && this.feed.loadRange) {
7546
+ let progressiveServed = false;
7547
+ if (!market.data?.length && initialRange == null && this.feed.loadProgressive) {
7548
+ let painted = false;
7549
+ const paint = (bars, final) => {
7550
+ if (this.generation !== gen || !final && bars.length === 0) return;
7551
+ if (!painted && !final && bars.length < Math.min(requested, FIRST_PAINT_BARS)) return;
7552
+ this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
7553
+ if (!painted && bars.length > 0) {
7554
+ painted = true;
7555
+ if (opts.firstLoad) this.activateBarLayers();
7556
+ if (!final) this.historyState = "backfill";
7557
+ }
7558
+ };
7559
+ const abort = new AbortController();
7560
+ this.progressiveAbort = abort;
7561
+ progressiveServed = await new Promise((firstPaint) => {
7562
+ let signaled = false;
7563
+ const signal = (served) => {
7564
+ if (!signaled) {
7565
+ signaled = true;
7566
+ firstPaint(served);
7567
+ }
7568
+ };
7569
+ abort.signal.addEventListener("abort", () => signal(true), { once: true });
7570
+ this.feed.loadProgressive(market, (bars) => {
7571
+ paint(bars, false);
7572
+ if (painted) signal(true);
7573
+ }, { signal: abort.signal }).then((full) => {
7574
+ if (this.progressiveAbort === abort) this.progressiveAbort = null;
7575
+ if (full == null) return signal(false);
7576
+ if (this.generation !== gen) return signal(true);
7577
+ paint(full, true);
7578
+ this.completeHistory(full.length >= requested ? "depth" : "genesis");
7579
+ signal(true);
7580
+ }).catch(() => {
7581
+ if (this.progressiveAbort === abort) this.progressiveAbort = null;
7582
+ if (this.generation === gen) this.completeHistory("aborted");
7583
+ signal(true);
7584
+ });
7585
+ });
7586
+ }
7587
+ if (progressiveServed) ; else if (deep && this.feed.loadRange) {
7501
7588
  const head = await this.feed.load({ ...market, bars: Math.min(requested, CHUNK_BARS) });
7502
7589
  if (this.generation !== gen) return;
7503
7590
  this.setBarSeries(head);
@@ -8092,6 +8179,15 @@ var Vela = (function (exports) {
8092
8179
  record.pendingCause = "inputs";
8093
8180
  if (record.session) record.session.update(record.inputValues);
8094
8181
  else if (record.native && !record.hidden) record.native.instance.setInputs(record.inputValues);
8182
+ this.events.emit("indicator:inputs", { id });
8183
+ }
8184
+ /** IndicatorController: the CURRENT stored input values (defaults merged with edits). */
8185
+ inputValuesOf(id) {
8186
+ return { ...this.registry.get(id)?.inputValues };
8187
+ }
8188
+ /** IndicatorController: the CURRENT declaration-prop overrides. */
8189
+ propValuesOf(id) {
8190
+ return { ...this.registry.get(id)?.propValues };
8095
8191
  }
8096
8192
  /** IndicatorController: re-run an indicator with merged declaration-prop overrides.
8097
8193
  * Same lifecycle as {@link applyInputs} — a prop change replays the whole script.
@@ -8105,6 +8201,7 @@ var Vela = (function (exports) {
8105
8201
  if (!record.hidden) this.setLoading(record, true);
8106
8202
  record.pendingCause = "inputs";
8107
8203
  record.session.update(record.inputValues, record.propValues);
8204
+ this.events.emit("indicator:inputs", { id });
8108
8205
  }
8109
8206
  /** IndicatorController: tear down an indicator and (if now empty) its pane. */
8110
8207
  /** Live handles of every indicator on the chart (script + native), insertion order. */
@@ -9169,6 +9266,28 @@ var Vela = (function (exports) {
9169
9266
  this.store.merge(key, dropForming(bars));
9170
9267
  return bars;
9171
9268
  }
9269
+ /**
9270
+ * Progressive twin of {@link load}: a COLD load streams through the inner feed's
9271
+ * progressive path — batches forwarded verbatim, the FINAL answer cached exactly as
9272
+ * `load` caches — while a cache-covered load answers once through `load` itself (a
9273
+ * warm chart has nothing to stream). Falls back to `load` wholesale when the inner
9274
+ * feed lacks the capability, so callers may prefer this method unconditionally.
9275
+ */
9276
+ async loadProgressive(cfg, onBatch, opts) {
9277
+ if (cfg.data && cfg.data.length > 0) return this.inner.load(cfg);
9278
+ if (!this.inner.loadProgressive) return null;
9279
+ const symbol = cfg.symbol ?? "TEST";
9280
+ const key = cacheKey(symbol, cfg.timeframe ?? "60", cfg.session);
9281
+ const cached = this.store.get(key);
9282
+ const n = cfg.bars ?? 500;
9283
+ const lastCached = cached?.[cached.length - 1];
9284
+ if (cached && lastCached && cached.length >= n - 1 && this.inner.loadRange) return this.load(cfg);
9285
+ this.store.retainSymbol(symbol);
9286
+ const bars = await this.inner.loadProgressive(cfg, onBatch, opts);
9287
+ if (bars == null) return null;
9288
+ this.store.merge(key, dropForming(bars));
9289
+ return bars;
9290
+ }
9172
9291
  /**
9173
9292
  * Cache-backed ranged fetch — the gateway engines use for secondary series
9174
9293
  * (`request.security` HTF/LTF/cross-symbol) and the orchestrator's backward
@@ -9414,6 +9533,17 @@ var Vela = (function (exports) {
9414
9533
  this.prefetchSymbolInfo(resolved);
9415
9534
  return this.cache.load(canonical(cfg, resolved));
9416
9535
  }
9536
+ /** Progressive twin of {@link load} — same resolution, the cache streams the batches. */
9537
+ async loadProgressive(cfg, onBatch, opts) {
9538
+ if (cfg.data && cfg.data.length > 0) {
9539
+ this.liveBars = cfg.data.map((b) => ({ ...b }));
9540
+ return cfg.data;
9541
+ }
9542
+ const resolved = await this.registry.whenResolvable(rawSymbol(cfg), { default: this.primaryProvider });
9543
+ this.primaryProvider = resolved.provider;
9544
+ this.prefetchSymbolInfo(resolved);
9545
+ return this.cache.loadProgressive(canonical(cfg, resolved), onBatch, opts);
9546
+ }
9417
9547
  /**
9418
9548
  * Synchronous per-symbol metadata for engines (Pine `syminfo.*`), served from the cache
9419
9549
  * warmed by load(). Undefined until the prefetch lands (the engine then synthesizes a
@@ -9501,6 +9631,18 @@ var Vela = (function (exports) {
9501
9631
  if (!provider) return Promise.resolve([]);
9502
9632
  return safeBars(provider, ticker, cfg.timeframe ?? "60", { limit: cfg.bars ?? 500, session: cfg.session });
9503
9633
  }
9634
+ async loadProgressive(cfg, onBatch, opts) {
9635
+ const { provider: name, ticker } = parseSymbol(cfg.symbol ?? "");
9636
+ const provider = this.registry.get(name ?? "");
9637
+ if (!provider) return [];
9638
+ if (!provider.getBarsProgressive) return null;
9639
+ try {
9640
+ return await provider.getBarsProgressive(ticker, cfg.timeframe ?? "60", { limit: cfg.bars ?? 500, session: cfg.session }, onBatch, opts);
9641
+ } catch (e) {
9642
+ console.warn(`[vela] progressive fetch failed for ${ticker} ${cfg.timeframe ?? "60"} \u2014 ${e instanceof Error ? e.message : String(e)}`);
9643
+ return [];
9644
+ }
9645
+ }
9504
9646
  loadRange(cfg, range) {
9505
9647
  const { provider: name, ticker } = parseSymbol(cfg.symbol ?? "");
9506
9648
  const provider = this.registry.get(name ?? "");
@@ -17014,6 +17156,10 @@ ${STATIC_DECLS}
17014
17156
  get open() {
17015
17157
  return this.pop?.open ?? false;
17016
17158
  }
17159
+ /** Close the deployed panel, if any. The bubble itself stays. */
17160
+ hidePanel() {
17161
+ this.pop?.hide();
17162
+ }
17017
17163
  destroy() {
17018
17164
  this.pop?.destroy();
17019
17165
  this.pop = null;
@@ -17094,6 +17240,14 @@ ${STATIC_DECLS}
17094
17240
  const conds = Array.isArray(when) ? when : [when];
17095
17241
  return conds.every((c) => c.anyOf ? c.anyOf.some((x) => x === values[c.key]) : values[c.key] === c.equals);
17096
17242
  }
17243
+ function inputDeltas(schema, values) {
17244
+ const out = {};
17245
+ for (const s of schema) {
17246
+ const v = values[s.key];
17247
+ if (v !== void 0 && JSON.stringify(v) !== JSON.stringify(s.defval)) out[s.key] = v;
17248
+ }
17249
+ return Object.keys(out).length > 0 ? out : void 0;
17250
+ }
17097
17251
 
17098
17252
  // src/ui/components/select/controller.ts
17099
17253
  function selectController(opts) {
@@ -21110,6 +21264,9 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21110
21264
  var FOLD_SVG = iconAt("chevron-up", LEGEND_ICON_PX2);
21111
21265
  var UNFOLD_SVG = iconAt("chevron-down", LEGEND_ICON_PX2);
21112
21266
  var OVERVIEW_SVG = iconAt("objects", LEGEND_ICON_PX2);
21267
+ function legendCalloutsDisplay(open2, hasCallouts) {
21268
+ return !open2 && hasCallouts ? "inline-flex" : "none";
21269
+ }
21113
21270
  var InputsUI = class {
21114
21271
  constructor(container, theme, paneBoundsOf) {
21115
21272
  this.container = container;
@@ -21313,7 +21470,7 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21313
21470
  row.callouts = [];
21314
21471
  row.calloutsEl.replaceChildren();
21315
21472
  const views = this.legendCallouts?.(row.id) ?? [];
21316
- row.calloutsEl.style.display = views.length > 0 ? "inline-flex" : "none";
21473
+ row.calloutsEl.style.display = legendCalloutsDisplay(row.highlighted, views.length > 0);
21317
21474
  for (const view of views) {
21318
21475
  const bubble = new CalloutBubble({
21319
21476
  icon: view.icon,
@@ -21797,11 +21954,11 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21797
21954
  row.controlsEl.style.display = open2 || row.hidden ? "inline-flex" : "none";
21798
21955
  if (open2) {
21799
21956
  row.el.appendChild(row.statusEl);
21800
- row.el.appendChild(row.calloutsEl);
21957
+ for (const bubble of row.callouts) bubble.hidePanel();
21801
21958
  } else {
21802
21959
  row.el.insertBefore(row.statusEl, row.valuesEl);
21803
- row.el.insertBefore(row.calloutsEl, row.statusEl);
21804
21960
  }
21961
+ row.calloutsEl.style.display = legendCalloutsDisplay(open2, row.callouts.length > 0);
21805
21962
  for (const child of Array.from(row.controlsEl.children)) {
21806
21963
  if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
21807
21964
  if (child === row.extrasEl) {
@@ -21891,7 +22048,7 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21891
22048
  };
21892
22049
  var STYLE_ID = "vela-pane-controls";
21893
22050
  var ICON_PX = 12;
21894
- var CLUSTER_PILL = "rgba(0,0,0,0.28)";
22051
+ var CLUSTER_PILL = "rgba(0,0,0,0.65)";
21895
22052
  function ensureStyles() {
21896
22053
  if (typeof document === "undefined" || document.getElementById(STYLE_ID)) return;
21897
22054
  const st = document.createElement("style");
@@ -21911,8 +22068,12 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21911
22068
  this.deps = deps;
21912
22069
  this.clusters = /* @__PURE__ */ new Map();
21913
22070
  this.hoverPaneId = null;
22071
+ /** Mobile: hover clusters are meaningless without a cursor — suppressed; a
22072
+ * collapsed pane's standalone expand chip stays (the only way back up). */
22073
+ this.suspended = false;
21914
22074
  /** Reveal the cluster for the pane under the cursor, resolved from the pointer's y in the plot. */
21915
22075
  this.onPlotMove = (e) => {
22076
+ if (this.suspended) return;
21916
22077
  const rect = this.plot.getBoundingClientRect();
21917
22078
  const y = e.clientY - rect.top;
21918
22079
  let hit = null;
@@ -21984,7 +22145,12 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21984
22145
  }
21985
22146
  if (p.count > 1) {
21986
22147
  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" })
22148
+ this.button(p.maximized ? ICONS.restore : ICONS.maximize, p.maximized ? "Restore pane" : "Maximize pane", false, () => this.deps.onToggleMaximize(p.id), {
22149
+ role: "maximize",
22150
+ // Same inverse-chip treatment as the collapsed pane's expand toggle: the
22151
+ // maximized state must read as an active state, not just a swapped glyph.
22152
+ selected: p.maximized
22153
+ })
21988
22154
  );
21989
22155
  }
21990
22156
  }
@@ -22021,17 +22187,18 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
22021
22187
  }
22022
22188
  const hovered = id === this.hoverPaneId;
22023
22189
  const hasButtons = cluster.children.length > 0;
22024
- const visible = hasButtons && (hovered || p.collapsed) && p.height > 8;
22190
+ const stateChipRole = p.collapsed ? "collapse" : !this.suspended && p.maximized ? "maximize" : null;
22191
+ const visible = hasButtons && (hovered || stateChipRole != null) && p.height > 8;
22025
22192
  cluster.style.right = `${rightPx}px`;
22026
22193
  cluster.style.top = p.collapsed ? `${p.top + Math.max(1, Math.round((p.height - 24) / 2))}px` : `${p.top + 4}px`;
22027
22194
  cluster.style.display = visible ? "flex" : "none";
22028
22195
  if (!visible) continue;
22029
- const soloExpand = p.collapsed && !hovered;
22030
- cluster.style.background = soloExpand ? "transparent" : CLUSTER_PILL;
22196
+ const soloChip = stateChipRole != null && !hovered;
22197
+ cluster.style.background = soloChip ? "transparent" : CLUSTER_PILL;
22031
22198
  for (const child of cluster.children) {
22032
22199
  const btn2 = child;
22033
22200
  btn2.style.display = "inline-flex";
22034
- btn2.style.visibility = soloExpand && btn2.dataset.role !== "collapse" ? "hidden" : "visible";
22201
+ btn2.style.visibility = soloChip && btn2.dataset.role !== stateChipRole ? "hidden" : "visible";
22035
22202
  }
22036
22203
  }
22037
22204
  }
@@ -22041,6 +22208,14 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
22041
22208
  this.hoverPaneId = paneId;
22042
22209
  this.reposition();
22043
22210
  }
22211
+ /** Mobile suppression: no hover clusters (touch has no cursor; the shell's own
22212
+ * chrome covers maximize), while collapsed panes keep their expand chips. */
22213
+ setSuspended(on) {
22214
+ if (on === this.suspended) return;
22215
+ this.suspended = on;
22216
+ if (on) this.hoverPaneId = null;
22217
+ this.reposition();
22218
+ }
22044
22219
  destroy() {
22045
22220
  this.plot.removeEventListener("pointermove", this.onPlotMove);
22046
22221
  this.plot.removeEventListener("pointerleave", this.onPlotLeave);
@@ -22191,160 +22366,6 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
22191
22366
  }
22192
22367
  };
22193
22368
 
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
22369
  // src/renderers/native/capabilities.ts
22349
22370
  var NATIVE_CAPABILITIES = {
22350
22371
  panes: true,
@@ -22365,7 +22386,7 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
22365
22386
  drawingDepth: true,
22366
22387
  // drawings share the series' z space (backend-composited interleave layers)
22367
22388
  tables: true,
22368
- // reuses the DOM TableOverlay
22389
+ // canvas-painted into the owning indicator's interleave slice
22369
22390
  trades: true,
22370
22391
  // strategy order-fill markers (arrows + labels + fill-price ticks)
22371
22392
  inputsUI: true
@@ -22396,6 +22417,9 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
22396
22417
  if (spacing < CANDLE_BODY_MIN_SPACING) return "wick";
22397
22418
  return "full";
22398
22419
  }
22420
+ function snapY(yCss, dpr) {
22421
+ return Math.round(yCss * dpr) / dpr;
22422
+ }
22399
22423
  function candleGeometry(xCss, spacing, dpr, bodyScale = 1) {
22400
22424
  const wickDev = Math.max(1, Math.round(wickWidth(spacing) * dpr));
22401
22425
  const wickLeftDev = Math.round(xCss * dpr - wickDev / 2);
@@ -23202,11 +23226,9 @@ void main() {
23202
23226
  };
23203
23227
  b.alpha = this.modelAlpha;
23204
23228
  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
23229
  if (isPrice) {
23207
23230
  for (const m of scene.indicators.values()) {
23208
23231
  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
23232
  }
23211
23233
  }
23212
23234
  const drawCandles = isPrice && !scene.candlesHidden;
@@ -23222,6 +23244,7 @@ void main() {
23222
23244
  b.alpha = this.modelAlpha;
23223
23245
  const off = scene.offsetOf(m.id);
23224
23246
  const mp = effPane(m);
23247
+ for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, mp, coords, i0, i1, off);
23225
23248
  for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
23226
23249
  }
23227
23250
  if (drawCandles && !candleDrawn) {
@@ -23229,14 +23252,18 @@ void main() {
23229
23252
  b.alpha = this.candleStructureAlpha;
23230
23253
  this.emitPriceSeries(b, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
23231
23254
  }
23232
- drawSlicesUpTo(Infinity);
23233
23255
  b.alpha = this.modelAlpha;
23234
23256
  if (isPrice) {
23257
+ for (const m of scene.indicators.values()) {
23258
+ const off = scene.offsetOf(m.id);
23259
+ for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, i1, off);
23260
+ }
23235
23261
  for (const m of scene.indicators.values()) {
23236
23262
  const off = scene.offsetOf(m.id);
23237
23263
  for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0, i1, theme, off);
23238
23264
  }
23239
23265
  }
23266
+ drawSlicesUpTo(Infinity);
23240
23267
  for (const m of models) {
23241
23268
  const mp = effPane(m);
23242
23269
  for (const pl of m.priceLines) this.emitHline(b, pl, mp, coords, dataW, theme);
@@ -23631,12 +23658,12 @@ void main() {
23631
23658
  if (drawBody) {
23632
23659
  const oY = coords.priceToY(bar.open, pane.scale, pane.bounds);
23633
23660
  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));
23661
+ bodyTop = snapY(Math.min(oY, cY), coords.dpr);
23662
+ bodyH = Math.max(1 / coords.dpr, snapY(Math.max(oY, cY), coords.dpr) - bodyTop);
23636
23663
  }
23637
23664
  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);
23665
+ const hY = snapY(coords.priceToY(bar.high, pane.scale, pane.bounds), coords.dpr);
23666
+ const lY = snapY(coords.priceToY(bar.low, pane.scale, pane.bounds), coords.dpr);
23640
23667
  b.alpha = this.candleStructureAlpha;
23641
23668
  const wCol = parseColor((isUp ? cs.wickUpColor : cs.wickDownColor) ?? (drawBody ? dir : bodyColorStr));
23642
23669
  if (drawBody) {
@@ -24193,6 +24220,7 @@ void main() {
24193
24220
  var DOUBLE_TAP_SLOP = 30;
24194
24221
  var TIME_SCALE_K = 4e-3;
24195
24222
  var WHEEL_ZOOM_K = 4e-3;
24223
+ var WHEEL_PRICE_DRAG_PX = 0.25;
24196
24224
  function wheelZoomAnchor(coords, cursorX, rightEdge) {
24197
24225
  if (rightEdge) return { logical: coords.rightEdgeLogical, x: coords.width };
24198
24226
  return { logical: coords.xToLogical(cursorX), x: cursorX };
@@ -24323,7 +24351,7 @@ void main() {
24323
24351
  this.capture(e.pointerId);
24324
24352
  return;
24325
24353
  }
24326
- if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y)) {
24354
+ if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y, this.snapMode(e))) {
24327
24355
  this.region = "drawing";
24328
24356
  this.capture(e.pointerId);
24329
24357
  return;
@@ -24437,7 +24465,7 @@ void main() {
24437
24465
  const wasTouch = e.pointerType === "touch";
24438
24466
  const tapRelease = this.dragging && !this.moved && (!wasTouch || Math.hypot(x - this.startX, y - this.startY) <= TOUCH_TAP_SLOP);
24439
24467
  if (this.dragging && this.region === "drawing") {
24440
- this.deps.drawingsPointerUp?.(x, y);
24468
+ this.deps.drawingsPointerUp?.(x, y, this.snapMode(e));
24441
24469
  } else if (tapRelease && this.region === "data") {
24442
24470
  this.deps.onClick(x, y);
24443
24471
  } else if (this.dragging && this.region === "data") {
@@ -24458,7 +24486,7 @@ void main() {
24458
24486
  if (e.pointerType === "touch") this.touches.delete(e.pointerId);
24459
24487
  this.cancelLongPress();
24460
24488
  if (!this.dragging) return;
24461
- if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY);
24489
+ if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY, this.snapMode(e));
24462
24490
  if (this.region === "crosshair" || e.pointerType === "touch") this.deps.onPointerMove(null, null);
24463
24491
  this.endGesture(e);
24464
24492
  };
@@ -24474,6 +24502,12 @@ void main() {
24474
24502
  this.onWheel = (e) => {
24475
24503
  e.preventDefault();
24476
24504
  this.deps.drawingsClearTransient?.();
24505
+ const { x, y } = this.local(e);
24506
+ if (this.regionAt(x, y) === "price" && e.deltaY !== 0) {
24507
+ this.deps.beginPriceScale(x, y);
24508
+ this.deps.priceScaleBy(e.deltaY * WHEEL_PRICE_DRAG_PX);
24509
+ return;
24510
+ }
24477
24511
  const coords = this.deps.getCoords();
24478
24512
  const vp = coords.getViewport();
24479
24513
  const pan = wheelPanDelta(e.deltaX, e.deltaY, e.shiftKey);
@@ -24481,9 +24515,8 @@ void main() {
24481
24515
  this.deps.apply({ barSpacing: vp.barSpacing, rightOffset: wheelPanRightOffset(vp.rightOffset, pan, coords.pxPerBar()) });
24482
24516
  return;
24483
24517
  }
24484
- const cursorX = this.local(e).x;
24485
24518
  const rightEdge = this.rightEdgeZoom && !(e.ctrlKey || e.metaKey);
24486
- const anchor = wheelZoomAnchor(coords, cursorX, rightEdge);
24519
+ const anchor = wheelZoomAnchor(coords, x, rightEdge);
24487
24520
  const target = clampBarSpacing(vp.barSpacing * Math.exp(-e.deltaY * WHEEL_ZOOM_K));
24488
24521
  this.deps.zoomTo(target, anchor.logical, anchor.x);
24489
24522
  };
@@ -24969,9 +25002,10 @@ void main() {
24969
25002
  * so each indicator arrives behind the candles (and behind older indicators);
24970
25003
  * `setIndicatorZ`/`bringToFront`/`sendToBack` change it. */
24971
25004
  this.seriesZ = /* @__PURE__ */ new Map();
24972
- /** Per-pane raster layers of user drawings interleaved into the series stack each is a
25005
+ /** Per-pane raster layers of drawings interleaved into the series stack (each
25006
+ * indicator's Pine drawings at its model's z, plus in-stack user drawings) — each a
24973
25007
  * 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. */
25008
+ * Rebuilt by the renderer per data frame. */
24975
25009
  this.drawingSlices = /* @__PURE__ */ new Map();
24976
25010
  /** Per-model index offset: the chart bar index of the model's `anchorTime` — its
24977
25011
  * index-aligned payloads (dense series arrays, `bar_index` drawings) count from that
@@ -25206,11 +25240,9 @@ void main() {
25206
25240
  };
25207
25241
  ctx.globalAlpha = this.modelAlpha;
25208
25242
  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
25243
  if (isPrice) {
25211
25244
  for (const m of scene.indicators.values()) {
25212
25245
  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
25246
  }
25215
25247
  }
25216
25248
  const slices = scene.drawingSlices.get(pane.id) ?? [];
@@ -25234,6 +25266,7 @@ void main() {
25234
25266
  ctx.globalAlpha = this.modelAlpha;
25235
25267
  const off = scene.offsetOf(m.id);
25236
25268
  const mp = effPane(m);
25269
+ for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, mp, coords, i0, i1, off);
25237
25270
  for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
25238
25271
  }
25239
25272
  if (drawCandles && !candleDrawn) {
@@ -25241,14 +25274,18 @@ void main() {
25241
25274
  ctx.globalAlpha = this.candleStructureAlpha;
25242
25275
  this.drawPriceSeries(ctx, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
25243
25276
  }
25244
- drawSlicesUpTo(Infinity);
25245
25277
  if (isPrice) {
25246
25278
  ctx.globalAlpha = this.modelAlpha;
25279
+ for (const m of scene.indicators.values()) {
25280
+ const off = scene.offsetOf(m.id);
25281
+ for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, off);
25282
+ }
25247
25283
  for (const m of scene.indicators.values()) {
25248
25284
  const off = scene.offsetOf(m.id);
25249
25285
  for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0, i1, theme, off);
25250
25286
  }
25251
25287
  }
25288
+ drawSlicesUpTo(Infinity);
25252
25289
  ctx.globalAlpha = this.modelAlpha;
25253
25290
  for (const m of models) {
25254
25291
  const mp = effPane(m);
@@ -25492,13 +25529,13 @@ void main() {
25492
25529
  if (drawBody) {
25493
25530
  const oY = coords.priceToY(b.open, pane.scale, pane.bounds);
25494
25531
  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));
25532
+ top = snapY(Math.min(oY, cY), coords.dpr);
25533
+ bodyH = Math.max(1 / coords.dpr, snapY(Math.max(oY, cY), coords.dpr) - top);
25497
25534
  }
25498
25535
  if (cs.wickVisible) {
25499
25536
  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);
25537
+ const hY = snapY(coords.priceToY(b.high, pane.scale, pane.bounds), coords.dpr);
25538
+ const lY = snapY(coords.priceToY(b.low, pane.scale, pane.bounds), coords.dpr);
25502
25539
  ctx.globalAlpha = this.candleStructureAlpha;
25503
25540
  ctx.strokeStyle = wick;
25504
25541
  ctx.lineWidth = g.wickW;
@@ -25945,9 +25982,31 @@ void main() {
25945
25982
 
25946
25983
  // src/renderers/shared/DrawingSceneRenderer.ts
25947
25984
  var EMPTY_DRAWING_SET = { lines: [], boxes: [], labels: [], polylines: [], linefills: [] };
25985
+ function modelDrawingSet(m, overlay) {
25986
+ const want = (d) => Boolean(d.overlay) === overlay;
25987
+ return {
25988
+ lines: (m.lines ?? []).filter(want),
25989
+ boxes: (m.boxes ?? []).filter(want),
25990
+ labels: (m.labels ?? []).filter(want),
25991
+ polylines: (m.polylines ?? []).filter(want),
25992
+ linefills: (m.linefills ?? []).filter(want)
25993
+ };
25994
+ }
25995
+ function drawingSetEmpty(s) {
25996
+ return !s.lines.length && !s.boxes.length && !s.labels.length && !s.polylines.length && !s.linefills.length;
25997
+ }
25948
25998
  function fontSizePx(size3) {
25949
25999
  return size3 === "auto" ? 12 : namedFontSize(size3);
25950
26000
  }
26001
+ function lineCoversWindow(a, b, extend, lo, hi) {
26002
+ const minX = Math.min(a, b);
26003
+ const maxX = Math.max(a, b);
26004
+ if (a === b) return a >= lo && a <= hi;
26005
+ if (extend === "both") return true;
26006
+ if (extend === "left") return maxX >= lo;
26007
+ if (extend === "right") return minX <= hi;
26008
+ return maxX >= lo && minX <= hi;
26009
+ }
25951
26010
  var DrawingSceneRenderer = class {
25952
26011
  constructor(deps, set = EMPTY_DRAWING_SET) {
25953
26012
  this.deps = deps;
@@ -26012,7 +26071,7 @@ void main() {
26012
26071
  };
26013
26072
  for (const ln of this.set.lines) {
26014
26073
  if (ln.invisible) continue;
26015
- if (!visible(this.logicalOf(ln.xloc, ln.x1), this.logicalOf(ln.xloc, ln.x2), ln.extend)) continue;
26074
+ if (!lineCoversWindow(this.logicalOf(ln.xloc, ln.x1), this.logicalOf(ln.xloc, ln.x2), ln.extend, lo, hi)) continue;
26016
26075
  fold(ln.y1);
26017
26076
  fold(ln.y2);
26018
26077
  }
@@ -26797,10 +26856,8 @@ void main() {
26797
26856
  this.ctx = null;
26798
26857
  // The color for axis tick labels — the host-passed surface text, set each frame in render().
26799
26858
  this.axisTextColor = DARK_THEME.textColor;
26800
- // Shared Pine-drawing renderer (line/box/label/polyline/linefill); widthCache persists.
26859
+ // Shared Pine-drawing renderer, used here for autoscale geometry only; widthCache persists.
26801
26860
  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
26861
  }
26805
26862
  mount(canvas) {
26806
26863
  this.canvas = canvas;
@@ -26824,8 +26881,8 @@ void main() {
26824
26881
  */
26825
26882
  paneDrawingsRange(ownModels, scene, isPricePane, vr) {
26826
26883
  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)));
26884
+ for (const m of ownModels) dr = unionRange(dr, this.drawingsRange(modelDrawingSet(m, false), vr, scene.offsetOf(m.id)));
26885
+ if (isPricePane) for (const m of scene.indicators.values()) dr = unionRange(dr, this.drawingsRange(modelDrawingSet(m, true), vr, scene.offsetOf(m.id)));
26829
26886
  return dr;
26830
26887
  }
26831
26888
  /** Clear the chrome canvas and draw drawings + axes + current-price line.
@@ -26842,7 +26899,6 @@ void main() {
26842
26899
  const dataW = coords.width;
26843
26900
  const dataH = coords.height;
26844
26901
  this.axisTextColor = surface?.textColor ?? theme.textColor;
26845
- this.labelTips = [];
26846
26902
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
26847
26903
  ctx.clearRect(0, 0, fullW, fullH);
26848
26904
  if (surface && (fullW > dataW || fullH > dataH)) {
@@ -26858,17 +26914,6 @@ void main() {
26858
26914
  return;
26859
26915
  }
26860
26916
  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
26917
  if (pricePane && !pricePane.collapsed && scene.tradeMarkers.visible) {
26873
26918
  for (const m of scene.indicators.values()) {
26874
26919
  if (m.trades?.length) this.renderTrades(ctx, coords, scene, theme, m.trades, pricePane, dataW);
@@ -26884,25 +26929,6 @@ void main() {
26884
26929
  this.canvas = null;
26885
26930
  this.ctx = null;
26886
26931
  }
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
26932
  drawingsRange(set, vr, indexOffset = 0) {
26907
26933
  this.drawScene.setSet(set, indexOffset);
26908
26934
  if (this.drawScene.isEmpty()) return null;
@@ -26936,34 +26962,6 @@ void main() {
26936
26962
  );
26937
26963
  ctx.restore();
26938
26964
  }
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
26965
  // ── axes ──
26968
26966
  drawPriceAxes(ctx, scene, coords, theme, dataW, panes) {
26969
26967
  ctx.strokeStyle = scene.style.borderColor ?? theme.borderColor;
@@ -30013,6 +30011,22 @@ ${overlayScrollbarCss(".vela-sd-pane")}
30013
30011
  this.snapAt = changed ? { point: snapped, paneId } : null;
30014
30012
  return snapped;
30015
30013
  }
30014
+ /**
30015
+ * Resolve a cursor pixel through the magnet and return the snapped pixel — the same
30016
+ * conversion drawing placement uses. Updates the snap-ring marker. The measure
30017
+ * ruler goes through this so its endpoints follow weak/strong/Ctrl magnet too.
30018
+ */
30019
+ snapCursor(x, y, mode) {
30020
+ const proj = this.deps.projector();
30021
+ const paneId = proj.paneIdAtY(y) ?? "price";
30022
+ const point = this.resolve(x, y, paneId, mode);
30023
+ const sy = proj.yOf(point.price, paneId);
30024
+ return { x: proj.xOf(point.time), y: sy ?? y };
30025
+ }
30026
+ /** Drop the snap-ring marker (a transient mode ended without going through `up`). */
30027
+ clearSnapMarker() {
30028
+ this.snapAt = null;
30029
+ }
30016
30030
  /** Resolve a pixel to a data point with the segment angle locked to 45° steps around
30017
30031
  * `pivot` (Shift held on a line tool). Works in PIXEL space — the user reasons about
30018
30032
  * the angle they see, not about time/price units. The magnet is bypassed: snapping
@@ -31372,17 +31386,17 @@ ${overlayScrollbarCss(".vela-sd-pane")}
31372
31386
  return textGlyph(String(glyph), 15);
31373
31387
  }
31374
31388
  function stampSizeIcon(size3) {
31375
- return textGlyph("\u25CF", (SIZE_PX4[String(size3)] ?? 13) + 4);
31389
+ return textGlyph("\u25CF", (SIZE_PX3[String(size3)] ?? 13) + 4);
31376
31390
  }
31377
31391
  function sizeIcon(size3) {
31378
31392
  return textGlyph(String(size3).charAt(0).toUpperCase(), 15);
31379
31393
  }
31380
- var SIZE_PX4 = { small: 10, normal: 13, large: 16, huge: 20 };
31394
+ var SIZE_PX3 = { small: 10, normal: 13, large: 16, huge: 20 };
31381
31395
  function numbersSizeIcon(size3) {
31382
- return textGlyph("12", (SIZE_PX4[String(size3)] ?? 13) - 1, 16.5, 'font-weight="600"');
31396
+ return textGlyph("12", (SIZE_PX3[String(size3)] ?? 13) - 1, 16.5, 'font-weight="600"');
31383
31397
  }
31384
31398
  function labelSizeIcon(size3) {
31385
- return textGlyph("T", (SIZE_PX4[String(size3)] ?? 13) + 2);
31399
+ return textGlyph("T", (SIZE_PX3[String(size3)] ?? 13) + 2);
31386
31400
  }
31387
31401
  function capitalize(s) {
31388
31402
  return s.charAt(0).toUpperCase() + s.slice(1);
@@ -31546,7 +31560,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
31546
31560
  this.root.replaceChildren();
31547
31561
  this.groupCells.clear();
31548
31562
  this.groupIcons.clear();
31549
- this.cursorBtn = this.makeButton(CURSOR_ICON, "Cursor", () => this.onArm(null));
31563
+ this.cursorBtn = this.makeButton(CURSOR_ICON, "Cursor", () => this.onCursorClick());
31550
31564
  this.root.appendChild(this.cursorBtn);
31551
31565
  if (this.def.groups.length > 0) this.root.appendChild(this.divider());
31552
31566
  for (const g of this.def.groups) {
@@ -31650,6 +31664,14 @@ ${overlayScrollbarCss(".vela-sd-pane")}
31650
31664
  this.magnetIcon = icon2;
31651
31665
  return cell;
31652
31666
  }
31667
+ /** Cursor returns to select/idle: an active measure/eraser mode exits through its own
31668
+ * toggle callback (disarming a tool via `onArm(null)` alone can't — the host treats a
31669
+ * null arm as a no-op side effect of entering those modes), then the tool disarms. */
31670
+ onCursorClick() {
31671
+ if (this.measureActive) this.onMeasure();
31672
+ if (this.eraserActive) this.onEraser();
31673
+ this.onArm(null);
31674
+ }
31653
31675
  /** Clicking the icon arms the group's last-used tool (it does NOT open the flyout). */
31654
31676
  onGroupIconClick(group) {
31655
31677
  const type = this.lastUsed.get(group.id) ?? group.tools[0]?.type;
@@ -32042,30 +32064,39 @@ ${overlayScrollbarCss(".vela-sd-pane")}
32042
32064
  isFinished() {
32043
32065
  return this.state === "finished";
32044
32066
  }
32045
- /** A press: begin the measurement, or finish it on the second click. */
32046
- down(x, y) {
32067
+ /** A press: begin the measurement, or finish it on the second click.
32068
+ * `x,y` are the raw cursor (drag-slop vs click-move-click). `gx,gy` are the
32069
+ * graphic endpoints — magnet-snapped when the magnet is on, else the same as `x,y`. */
32070
+ down(x, y, gx = x, gy = y) {
32047
32071
  if (this.state === "measuring") {
32048
- this.end = { x, y };
32072
+ this.end = { x: gx, y: gy };
32049
32073
  this.state = "finished";
32050
32074
  return;
32051
32075
  }
32052
- this.start = { x, y };
32053
- this.end = { x, y };
32076
+ this.start = { x: gx, y: gy };
32077
+ this.end = { x: gx, y: gy };
32054
32078
  this.pressX = x;
32055
32079
  this.pressY = y;
32056
32080
  this.state = "measuring";
32057
32081
  }
32082
+ /** Size the in-progress ruler. `x,y` are the graphic (magnet-snapped) cursor. */
32058
32083
  move(x, y) {
32059
32084
  if (this.state === "measuring") this.end = { x, y };
32060
32085
  }
32061
32086
  /** A release: finish if the press was actually dragged (press-drag-release), else wait
32062
- * for the second click (click-move-click). */
32063
- up(x, y) {
32087
+ * for the second click (click-move-click). `x,y` are the raw cursor (slop); `gx,gy`
32088
+ * are the graphic end (magnet-snapped when the magnet is on). */
32089
+ up(x, y, gx = x, gy = y) {
32064
32090
  if (this.state === "measuring" && Math.hypot(x - this.pressX, y - this.pressY) > DRAG_SLOP3) {
32065
- this.end = { x, y };
32091
+ this.end = { x: gx, y: gy };
32066
32092
  this.state = "finished";
32067
32093
  }
32068
32094
  }
32095
+ /** Current graphic endpoints in media pixels, or null when idle. */
32096
+ points() {
32097
+ if (!this.start || !this.end) return null;
32098
+ return { start: this.start, end: this.end };
32099
+ }
32069
32100
  clear() {
32070
32101
  this.state = "idle";
32071
32102
  this.start = null;
@@ -32442,11 +32473,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
32442
32473
  }
32443
32474
  /** Shift+press on the empty plot: arm the measure ruler AND start it at (x, y) in one
32444
32475
  * gesture — the equivalent of clicking the toolbar's Measure button, then pressing.
32445
- * Returns false when a mode/tool is already active (the normal press path owns it). */
32446
- beginMeasureAt(x, y) {
32476
+ * `snap` is the effective magnet (sticky mode, or Ctrl/Cmd-forced strong). Returns
32477
+ * false when a mode/tool is already active (the normal press path owns it). */
32478
+ beginMeasureAt(x, y, snap = "off") {
32447
32479
  if (this.measureMode || this.eraserMode || this.activeTool != null) return false;
32448
32480
  this.withModeIntent(() => this.toggleMeasure());
32449
- this.measure.down(x, y);
32481
+ const g = this.interaction.snapCursor(x, y, snap);
32482
+ this.measure.down(x, y, g.x, g.y);
32450
32483
  this.render();
32451
32484
  return true;
32452
32485
  }
@@ -32468,7 +32501,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
32468
32501
  return;
32469
32502
  }
32470
32503
  if (this.measureMode) {
32471
- this.measure.down(x, y);
32504
+ const g = this.interaction.snapCursor(x, y, snap);
32505
+ this.measure.down(x, y, g.x, g.y);
32472
32506
  if (this.measure.isFinished()) this.withModeIntent(() => this.exitMeasure(false));
32473
32507
  this.render();
32474
32508
  return;
@@ -32481,7 +32515,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
32481
32515
  return;
32482
32516
  }
32483
32517
  if (this.measureMode) {
32484
- this.measure.move(x, y);
32518
+ const g = this.interaction.snapCursor(x, y, snap);
32519
+ this.measure.move(g.x, g.y);
32485
32520
  this.render();
32486
32521
  return;
32487
32522
  }
@@ -32499,13 +32534,14 @@ ${overlayScrollbarCss(".vela-sd-pane")}
32499
32534
  this.render();
32500
32535
  }
32501
32536
  }
32502
- pointerUp(x, y) {
32537
+ pointerUp(x, y, snap = "off") {
32503
32538
  if (this.eraserMode) {
32504
32539
  this.erasing = false;
32505
32540
  return;
32506
32541
  }
32507
32542
  if (this.measureMode) {
32508
- this.measure.up(x, y);
32543
+ const g = this.interaction.snapCursor(x, y, snap);
32544
+ this.measure.up(x, y, g.x, g.y);
32509
32545
  if (this.measure.isFinished()) this.withModeIntent(() => this.exitMeasure(false));
32510
32546
  this.render();
32511
32547
  return;
@@ -32550,6 +32586,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
32550
32586
  /** Leave ruler mode. `clearGraphic` keeps a just-finished measurement on screen (false). */
32551
32587
  exitMeasure(clearGraphic = true) {
32552
32588
  this.measureMode = false;
32589
+ this.interaction.clearSnapMarker();
32553
32590
  if (clearGraphic) this.measure.clear();
32554
32591
  this.toolbar.setMeasureActive(false);
32555
32592
  this.render();
@@ -32690,15 +32727,28 @@ ${overlayScrollbarCss(".vela-sd-pane")}
32690
32727
  if (this.eraserMode) return "pointer";
32691
32728
  return this.interaction.cursorAt(x, y);
32692
32729
  }
32693
- /** Right-click while placing: cancel the in-progress drawing and revert to the
32694
- * pointer the gesture is an explicit escape, so it disarms even in
32695
- * stay-in-drawing-mode (where Escape would leave the tool armed). Returns whether
32696
- * the press was consumed; false lets the host's context menu open normally. */
32730
+ /** Right-click: an explicit escape back to the pointer. Cancels an in-progress
32731
+ * placement or measurement, and also plain-disarms an armed-but-idle drawing
32732
+ * tool or the eraser — so a right-click ALWAYS reverts to the pointer, even in
32733
+ * stay-in-drawing-mode (where Escape would leave a drawing tool armed).
32734
+ * Persistent toggles (magnet, stay-mode, favorites) are untouched. Returns
32735
+ * whether the press was consumed; false lets the host's context menu open
32736
+ * normally. */
32697
32737
  cancelPlacement() {
32698
- if (!this.interaction.isPlacing()) return false;
32699
- this.interaction.cancel();
32700
- if (this.activeTool != null) this.emit({ kind: "arm", type: null });
32701
- return true;
32738
+ if (this.measureMode || this.eraserMode) {
32739
+ this.withModeIntent(() => this.measureMode ? this.exitMeasure() : this.exitEraser());
32740
+ return true;
32741
+ }
32742
+ if (this.interaction.isPlacing()) {
32743
+ this.interaction.cancel();
32744
+ if (this.activeTool != null) this.emit({ kind: "arm", type: null });
32745
+ return true;
32746
+ }
32747
+ if (this.activeTool != null) {
32748
+ this.emit({ kind: "arm", type: null });
32749
+ return true;
32750
+ }
32751
+ return false;
32702
32752
  }
32703
32753
  /** Double-click over a drawing → suppress the chart's view reset (single-click already
32704
32754
  * opens settings). Returns true only when a drawing is under the cursor. */
@@ -32724,6 +32774,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
32724
32774
  return true;
32725
32775
  }
32726
32776
  if (this.interaction.cancel()) return true;
32777
+ if (this.measureMode) {
32778
+ this.withModeIntent(() => this.exitMeasure());
32779
+ return true;
32780
+ }
32727
32781
  if (this.selectedIds.size) {
32728
32782
  this.clearSelection();
32729
32783
  return true;
@@ -32981,6 +33035,315 @@ ${overlayScrollbarCss(".vela-sd-pane")}
32981
33035
  }
32982
33036
  };
32983
33037
 
33038
+ // src/renderers/shared/TableOverlay.ts
33039
+ var SIZE_PX4 = {
33040
+ auto: 13,
33041
+ tiny: 10,
33042
+ small: 11,
33043
+ normal: 13,
33044
+ large: 16,
33045
+ huge: 20
33046
+ };
33047
+ function fontPxOf(size3) {
33048
+ if (typeof size3 === "number") return size3 > 0 ? size3 : SIZE_PX4.auto;
33049
+ return SIZE_PX4[size3] ?? SIZE_PX4.auto;
33050
+ }
33051
+ function tableHasContent(t) {
33052
+ return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
33053
+ }
33054
+ function mergeRenderPlan(t) {
33055
+ const span = /* @__PURE__ */ new Map();
33056
+ const omit = /* @__PURE__ */ new Set();
33057
+ for (const m of t.merges) {
33058
+ span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
33059
+ for (let r = m.startRow; r <= m.endRow; r += 1) {
33060
+ for (let c = m.startCol; c <= m.endCol; c += 1) {
33061
+ if (r !== m.startRow || c !== m.startCol) omit.add(`${r}:${c}`);
33062
+ }
33063
+ }
33064
+ }
33065
+ for (let r = 0; r < t.rows; r += 1) {
33066
+ for (let c = 0; c < t.columns; c += 1) {
33067
+ if (t.cells[r]?.[c]?.merged && !span.has(`${r}:${c}`)) omit.add(`${r}:${c}`);
33068
+ }
33069
+ }
33070
+ for (const key of span.keys()) omit.delete(key);
33071
+ return { span, omit };
33072
+ }
33073
+
33074
+ // src/renderers/shared/TableCanvasRenderer.ts
33075
+ var PAD_X = 6;
33076
+ var PAD_Y = 2;
33077
+ var MARGIN = 6;
33078
+ var LINE_HEIGHT = 1.2;
33079
+ function paintTable(ctx, t, args, tips) {
33080
+ if (!tableHasContent(t)) return;
33081
+ const layout = layoutTable(ctx, t, args);
33082
+ if (!layout || layout.w <= 0 || layout.h <= 0) return;
33083
+ const fw = t.frameColor && t.frameWidth > 0 ? t.frameWidth : 0;
33084
+ const { x, y } = anchorOrigin(t.position, layout.w + 2 * fw, layout.h + 2 * fw, args);
33085
+ const x0 = x + fw;
33086
+ const y0 = y + fw;
33087
+ if (t.bgColor) {
33088
+ ctx.fillStyle = t.bgColor;
33089
+ ctx.fillRect(x0, y0, layout.w, layout.h);
33090
+ }
33091
+ if (fw > 0 && t.frameColor) {
33092
+ ctx.strokeStyle = t.frameColor;
33093
+ ctx.lineWidth = fw;
33094
+ ctx.strokeRect(x + fw / 2, y + fw / 2, layout.w + fw, layout.h + fw);
33095
+ }
33096
+ const colX = [0];
33097
+ for (const w of layout.colW) colX.push(colX[colX.length - 1] + w);
33098
+ const rowY = [0];
33099
+ for (const h of layout.rowH) rowY.push(rowY[rowY.length - 1] + h);
33100
+ const prevBaseline = ctx.textBaseline;
33101
+ const prevAlign = ctx.textAlign;
33102
+ ctx.textBaseline = "middle";
33103
+ for (const box of layout.boxes) {
33104
+ const rx = x0 + colX[box.c];
33105
+ const ry = y0 + rowY[box.r];
33106
+ const rw = colX[box.c + box.cs] - colX[box.c];
33107
+ const rh = rowY[box.r + box.rs] - rowY[box.r];
33108
+ const cell = box.cell;
33109
+ if (cell.bgColor) {
33110
+ ctx.fillStyle = cell.bgColor;
33111
+ ctx.fillRect(rx, ry, rw, rh);
33112
+ }
33113
+ const text = cell.text ?? "";
33114
+ if (text.length > 0) {
33115
+ const px = fontPxOf(cell.textSize);
33116
+ ctx.font = cellFont(cell, px, args.theme);
33117
+ ctx.fillStyle = cell.textColor ?? args.theme.textColor;
33118
+ const lines = text.split("\n");
33119
+ const blockH = lines.length * px * LINE_HEIGHT;
33120
+ const blockTop = cell.vAlign === "top" ? ry + PAD_Y : cell.vAlign === "bottom" ? ry + rh - PAD_Y - blockH : ry + (rh - blockH) / 2;
33121
+ const tx = cell.hAlign === "left" ? rx + PAD_X : cell.hAlign === "right" ? rx + rw - PAD_X : rx + rw / 2;
33122
+ ctx.textAlign = cell.hAlign;
33123
+ lines.forEach((line, i) => ctx.fillText(line, tx, blockTop + (i + 0.5) * px * LINE_HEIGHT));
33124
+ }
33125
+ if (cell.tooltip) tips.push({ left: rx, top: ry, right: rx + rw, bottom: ry + rh, text: cell.tooltip });
33126
+ }
33127
+ ctx.textBaseline = prevBaseline;
33128
+ ctx.textAlign = prevAlign;
33129
+ if (t.borderColor && t.borderWidth > 0) {
33130
+ ctx.strokeStyle = t.borderColor;
33131
+ ctx.lineWidth = t.borderWidth;
33132
+ const seen = /* @__PURE__ */ new Set();
33133
+ ctx.beginPath();
33134
+ const edge = (ax, ay, bx, by) => {
33135
+ const key = `${ax},${ay},${bx},${by}`;
33136
+ if (seen.has(key)) return;
33137
+ seen.add(key);
33138
+ ctx.moveTo(ax, ay);
33139
+ ctx.lineTo(bx, by);
33140
+ };
33141
+ for (const box of layout.boxes) {
33142
+ const l = Math.round(x0 + colX[box.c]);
33143
+ const r = Math.round(x0 + colX[box.c + box.cs]);
33144
+ const tp = Math.round(y0 + rowY[box.r]);
33145
+ const bt = Math.round(y0 + rowY[box.r + box.rs]);
33146
+ edge(l, tp, r, tp);
33147
+ edge(l, bt, r, bt);
33148
+ edge(l, tp, l, bt);
33149
+ edge(r, tp, r, bt);
33150
+ }
33151
+ ctx.stroke();
33152
+ }
33153
+ }
33154
+ function layoutTable(ctx, t, args) {
33155
+ const { span, omit } = mergeRenderPlan(t);
33156
+ const colW = new Array(t.columns).fill(0);
33157
+ const rowH = new Array(t.rows).fill(0);
33158
+ const boxes = [];
33159
+ for (let r = 0; r < t.rows; r += 1) {
33160
+ for (let c = 0; c < t.columns; c += 1) {
33161
+ if (omit.has(`${r}:${c}`)) continue;
33162
+ const cell = t.cells[r]?.[c];
33163
+ if (cell == null) continue;
33164
+ const sp = span.get(`${r}:${c}`);
33165
+ boxes.push({ cell, r, c, cs: Math.min(sp?.cs ?? 1, t.columns - c), rs: Math.min(sp?.rs ?? 1, t.rows - r) });
33166
+ }
33167
+ }
33168
+ if (boxes.length === 0) return null;
33169
+ const sizeOf = (cell) => {
33170
+ const px = fontPxOf(cell.textSize);
33171
+ ctx.font = cellFont(cell, px, args.theme);
33172
+ const lines = (cell.text ?? "").split("\n");
33173
+ let maxW = 0;
33174
+ for (const line of lines) maxW = Math.max(maxW, ctx.measureText(line).width);
33175
+ let w2 = Math.ceil(maxW) + 2 * PAD_X;
33176
+ let h2 = Math.ceil(lines.length * px * LINE_HEIGHT) + 2 * PAD_Y;
33177
+ if (cell.width) w2 = Math.max(w2, cell.width / 100 * args.plotWidth);
33178
+ if (cell.height) h2 = Math.max(h2, cell.height / 100 * args.paneHeight);
33179
+ return { w: w2, h: h2 };
33180
+ };
33181
+ const spanning = [];
33182
+ for (const box of boxes) {
33183
+ const { w: w2, h: h2 } = sizeOf(box.cell);
33184
+ if (box.cs === 1) colW[box.c] = Math.max(colW[box.c], w2);
33185
+ if (box.rs === 1) rowH[box.r] = Math.max(rowH[box.r], h2);
33186
+ if (box.cs > 1 || box.rs > 1) spanning.push({ box, w: w2, h: h2 });
33187
+ }
33188
+ for (const { box, w: w2, h: h2 } of spanning) {
33189
+ if (box.cs > 1) {
33190
+ let sum = 0;
33191
+ for (let c = box.c; c < box.c + box.cs; c += 1) sum += colW[c];
33192
+ if (w2 > sum) for (let c = box.c; c < box.c + box.cs; c += 1) colW[c] += (w2 - sum) / box.cs;
33193
+ }
33194
+ if (box.rs > 1) {
33195
+ let sum = 0;
33196
+ for (let r = box.r; r < box.r + box.rs; r += 1) sum += rowH[r];
33197
+ if (h2 > sum) for (let r = box.r; r < box.r + box.rs; r += 1) rowH[r] += (h2 - sum) / box.rs;
33198
+ }
33199
+ }
33200
+ let w = 0;
33201
+ for (const cw of colW) w += cw;
33202
+ let h = 0;
33203
+ for (const rh of rowH) h += rh;
33204
+ return { colW, rowH, w, h, boxes };
33205
+ }
33206
+ function anchorOrigin(position, totalW, totalH, args) {
33207
+ let y;
33208
+ if (position.startsWith("top")) y = MARGIN;
33209
+ else if (position.startsWith("bottom")) y = args.paneHeight - MARGIN - totalH;
33210
+ else y = args.paneHeight / 2 - totalH / 2;
33211
+ let x;
33212
+ if (position.endsWith("left")) x = MARGIN;
33213
+ else if (position.endsWith("right")) x = args.plotWidth - MARGIN - totalW;
33214
+ else x = args.plotWidth / 2 - totalW / 2;
33215
+ return { x, y };
33216
+ }
33217
+ function cellFont(cell, px, theme) {
33218
+ const family = cell.fontFamily === "monospace" ? "monospace" : theme.fontFamily || "sans-serif";
33219
+ return `${cell.italic ? "italic " : ""}${cell.bold ? "bold " : ""}${px}px ${family}`;
33220
+ }
33221
+
33222
+ // src/renderers/native/drawings/IndicatorDrawingSlices.ts
33223
+ function indicatorSliceKey(z, boundaries) {
33224
+ return boundaries.find((b) => b > z) ?? Infinity;
33225
+ }
33226
+ var IndicatorDrawingSlices = class {
33227
+ constructor() {
33228
+ this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
33229
+ /** Slice canvas cache, keyed `paneId|beforeZ` — same lifecycle as the user-drawing cache. */
33230
+ this.sliceCache = /* @__PURE__ */ new Map();
33231
+ /** Tooltip hit-rects of every label drawn this frame, in plot coords (rebuilt per prepare). */
33232
+ this.tips = [];
33233
+ }
33234
+ /**
33235
+ * Rebuild the per-indicator drawing slices for this data frame. `ref` is the data
33236
+ * canvas the slices must match pixel-for-pixel (the backend composites them 1:1).
33237
+ * Runs from the renderer's data paint, just before the backend composites the scene.
33238
+ */
33239
+ prepare(scene, coords, theme, ref2) {
33240
+ this.tips = [];
33241
+ const out = /* @__PURE__ */ new Map();
33242
+ if (ref2.width === 0 || ref2.height === 0) {
33243
+ this.sliceCache.clear();
33244
+ return out;
33245
+ }
33246
+ this.drawScene.setDeps({
33247
+ timeToLogical: (ms) => coords.timeToLogical(ms),
33248
+ barAt: (logical) => {
33249
+ const b = scene.bars[Math.round(logical)];
33250
+ return b ? { high: b.high, low: b.low } : null;
33251
+ },
33252
+ theme
33253
+ });
33254
+ const dpr = coords.dpr;
33255
+ const dataW = coords.width;
33256
+ const buckets = /* @__PURE__ */ new Map();
33257
+ const add = (paneId, beforeZ, entry) => {
33258
+ const key = `${paneId}|${beforeZ}`;
33259
+ const bucket = buckets.get(key);
33260
+ if (bucket) bucket.entries.push(entry);
33261
+ else buckets.set(key, { paneId, beforeZ, entries: [entry] });
33262
+ };
33263
+ for (const pane of scene.orderedPanes()) {
33264
+ if (pane.collapsed) continue;
33265
+ const boundaries = scene.seriesBoundaries(pane.id);
33266
+ for (const m of scene.orderedIndicatorsForPane(pane.id)) {
33267
+ const set = modelDrawingSet(m, false);
33268
+ const tables = (m.tables ?? []).filter((t) => !t.overlay);
33269
+ if (drawingSetEmpty(set) && tables.length === 0) continue;
33270
+ const sc = scene.scaleFor(m, pane);
33271
+ const mp = sc === pane.scale ? pane : { ...pane, scale: sc };
33272
+ const beforeZ = indicatorSliceKey(scene.zOf(m.id), boundaries);
33273
+ add(pane.id, beforeZ, { set, tables, pane: mp, indexOffset: scene.offsetOf(m.id) });
33274
+ }
33275
+ if (pane.kind === "price") {
33276
+ for (const m of scene.indicators.values()) {
33277
+ const set = modelDrawingSet(m, true);
33278
+ const tables = (m.tables ?? []).filter((t) => t.overlay === true);
33279
+ if (drawingSetEmpty(set) && tables.length === 0) continue;
33280
+ add(pane.id, Infinity, { set, tables, pane, indexOffset: scene.offsetOf(m.id) });
33281
+ }
33282
+ }
33283
+ }
33284
+ for (const [key, { paneId, beforeZ, entries }] of buckets) {
33285
+ let canvas = this.sliceCache.get(key);
33286
+ if (!canvas) {
33287
+ canvas = document.createElement("canvas");
33288
+ this.sliceCache.set(key, canvas);
33289
+ }
33290
+ if (canvas.width !== ref2.width || canvas.height !== ref2.height) {
33291
+ canvas.width = ref2.width;
33292
+ canvas.height = ref2.height;
33293
+ }
33294
+ const ctx = canvas.getContext("2d");
33295
+ if (!ctx) continue;
33296
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
33297
+ ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
33298
+ for (const e of entries) this.paintEntry(ctx, e, coords, dataW, theme);
33299
+ const slices = out.get(paneId) ?? [];
33300
+ slices.push({ beforeZ, canvas });
33301
+ out.set(paneId, slices);
33302
+ }
33303
+ for (const key of [...this.sliceCache.keys()]) if (!buckets.has(key)) this.sliceCache.delete(key);
33304
+ for (const slices of out.values()) slices.sort((a, b) => a.beforeZ - b.beforeZ);
33305
+ return out;
33306
+ }
33307
+ paintEntry(ctx, e, coords, dataW, theme) {
33308
+ const { pane } = e;
33309
+ const paneTips = [];
33310
+ ctx.save();
33311
+ ctx.translate(0, pane.bounds.top);
33312
+ ctx.beginPath();
33313
+ ctx.rect(0, 0, dataW, pane.bounds.height);
33314
+ ctx.clip();
33315
+ this.drawScene.setSet(e.set, e.indexOffset);
33316
+ this.drawScene.render(
33317
+ ctx,
33318
+ dataW,
33319
+ pane.bounds.height,
33320
+ (l) => coords.logicalToX(l),
33321
+ (price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
33322
+ );
33323
+ paneTips.push(...this.drawScene.labelTipRegions());
33324
+ for (const t of e.tables) paintTable(ctx, t, { paneHeight: pane.bounds.height, plotWidth: dataW, theme }, paneTips);
33325
+ ctx.restore();
33326
+ for (const r of paneTips) {
33327
+ this.tips.push({ ...r, top: r.top + pane.bounds.top, bottom: r.bottom + pane.bounds.top });
33328
+ }
33329
+ }
33330
+ /** Tooltip of the topmost label or table cell under a plot-space point, or null. Fed by the last prepare. */
33331
+ labelTooltipAt(x, y) {
33332
+ for (let i = this.tips.length - 1; i >= 0; i -= 1) {
33333
+ const r = this.tips[i];
33334
+ if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return r.text;
33335
+ }
33336
+ return null;
33337
+ }
33338
+ };
33339
+ function mergeSlices(indicator, user) {
33340
+ const out = /* @__PURE__ */ new Map();
33341
+ for (const [paneId, slices] of indicator) out.set(paneId, [...slices]);
33342
+ for (const [paneId, slices] of user) out.set(paneId, [...out.get(paneId) ?? [], ...slices]);
33343
+ for (const slices of out.values()) slices.sort((a, b) => a.beforeZ - b.beforeZ);
33344
+ return out;
33345
+ }
33346
+
32984
33347
  // src/renderers/native/drawings/Projector.ts
32985
33348
  function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
32986
33349
  return {
@@ -33860,6 +34223,11 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33860
34223
  this.vpvrRenderer = new VpvrRenderer();
33861
34224
  this.resizeObserver = null;
33862
34225
  this.dprMedia = null;
34226
+ /** Plot size in INTEGER device px, as last reported by the resize observer's
34227
+ * device-pixel-content-box — the browser's own statement of how many device pixels
34228
+ * it paints the plot into. `null` until the first report or where the box type is
34229
+ * unsupported (WebKit); syncSize then falls back to rounding the client rect. */
34230
+ this.plotDeviceSize = null;
33863
34231
  this.coords = new CoordinateSystem();
33864
34232
  this.scene = new SceneGraph();
33865
34233
  // chosen at mount (WebGL2 if available, else canvas2d)
@@ -33867,6 +34235,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33867
34235
  this.glowAmount = 0;
33868
34236
  // WebGL2 neon-glow intensity (canvas2d ignores it)
33869
34237
  this.chrome = new ChromeRenderer();
34238
+ /** Prepaints each indicator's Pine drawings into interleave slices at the model's z. */
34239
+ this.indicatorSlices = new IndicatorDrawingSlices();
33870
34240
  /** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
33871
34241
  this.labelTooltip = null;
33872
34242
  this.crosshairLayer = new CrosshairRenderer();
@@ -34015,7 +34385,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34015
34385
  this.toggleVisibleCbs = /* @__PURE__ */ new Set();
34016
34386
  this.moveIndicatorCbs = /* @__PURE__ */ new Set();
34017
34387
  this.priceStyleCbs = /* @__PURE__ */ new Set();
34018
- this.tableOverlays = /* @__PURE__ */ new Map();
34019
34388
  this.name = "native";
34020
34389
  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
34390
  /** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
@@ -34329,7 +34698,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34329
34698
  * hidden) on clear so a re-show picks up the current theme.
34330
34699
  */
34331
34700
  setLoading(loading) {
34332
- for (const overlay of this.tableOverlays.values()) overlay.setVisible(!loading);
34333
34701
  if (!loading || !this.wrapper) {
34334
34702
  this.loadingEl?.remove();
34335
34703
  this.loadingEl = null;
@@ -34968,13 +35336,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34968
35336
  resetView: () => this.resetView(),
34969
35337
  // User drawings claim a gesture before pan when armed / over a drawing.
34970
35338
  drawingsClaim: (x, y) => this.userDrawings?.claim(x, y) ?? false,
34971
- drawingsMeasureStart: (x, y) => this.userDrawings?.beginMeasureAt(x, y) ?? false,
35339
+ drawingsMeasureStart: (x, y, snap) => this.userDrawings?.beginMeasureAt(x, y, snap) ?? false,
34972
35340
  drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y) ?? false,
34973
35341
  drawingsCancelPlacement: () => this.userDrawings?.cancelPlacement() ?? false,
34974
35342
  drawingsSnapMode: () => this.snapMode,
34975
35343
  drawingsPointerDown: (x, y, snap, shift3) => this.userDrawings?.pointerDown(x, y, snap, shift3),
34976
35344
  drawingsPointerMove: (x, y, snap, shift3) => this.userDrawings?.pointerMove(x, y, snap, shift3),
34977
- drawingsPointerUp: (x, y) => this.userDrawings?.pointerUp(x, y),
35345
+ drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
34978
35346
  drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
34979
35347
  drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
34980
35348
  drawingsClearTransient: () => this.userDrawings?.clearTransient()
@@ -34991,7 +35359,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34991
35359
  this.plot.addEventListener("pointerleave", this.onScrollProximityLeave);
34992
35360
  this.labelTooltip = new LabelTooltip(this.plot, {
34993
35361
  theme: () => this.chromeTheme(),
34994
- lookup: (x, y) => this.chrome.labelTooltipAt(x, y)
35362
+ lookup: (x, y) => this.indicatorSlices.labelTooltipAt(x, y)
34995
35363
  });
34996
35364
  this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
34997
35365
  projector: () => this.drawingProjector(),
@@ -35076,6 +35444,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35076
35444
  this.emitPaneAction({ type: "maximize", paneId, maximized });
35077
35445
  }
35078
35446
  });
35447
+ this.paneControls.setSuspended(this.layoutMode === "mobile");
35079
35448
  this.axisScaleButtons = new AxisScaleButtons(this.plot, theme, {
35080
35449
  panes: () => this.axisScaleViews(),
35081
35450
  rightAxis: () => this.rightAxisW,
@@ -35085,8 +35454,19 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35085
35454
  if (pane) this.setPaneLog(paneId, !paneLogScale(this.scene, pane));
35086
35455
  }
35087
35456
  });
35088
- this.resizeObserver = new ResizeObserver(() => this.resize());
35457
+ this.resizeObserver = new ResizeObserver((entries) => {
35458
+ for (const e of entries) {
35459
+ if (e.target !== this.plot) continue;
35460
+ const s = e.devicePixelContentBoxSize?.[0];
35461
+ if (s) this.plotDeviceSize = { width: s.inlineSize, height: s.blockSize };
35462
+ }
35463
+ this.resize();
35464
+ });
35089
35465
  this.resizeObserver.observe(this.wrapper);
35466
+ try {
35467
+ this.resizeObserver.observe(this.plot, { box: "device-pixel-content-box" });
35468
+ } catch {
35469
+ }
35090
35470
  this.watchDpr();
35091
35471
  this.syncSize();
35092
35472
  }
@@ -35211,8 +35591,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35211
35591
  this.inputsUI?.destroy();
35212
35592
  this.paneControls?.destroy();
35213
35593
  this.axisScaleButtons?.destroy();
35214
- for (const overlay of this.tableOverlays.values()) overlay.destroy();
35215
- this.tableOverlays.clear();
35216
35594
  this.resizeObserver?.disconnect();
35217
35595
  this.resizeObserver = null;
35218
35596
  this.dprMedia?.removeEventListener("change", this.onDprChange);
@@ -35241,6 +35619,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35241
35619
  this.attributionEl = null;
35242
35620
  this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
35243
35621
  this.mountContainer?.style.removeProperty("--vela-scale-gutter");
35622
+ this.mountContainer?.style.removeProperty("--vela-bottom-gutter");
35244
35623
  this.mountContainer?.style.removeProperty("--vela-price-pane-top");
35245
35624
  this.mountContainer?.style.removeProperty("--vela-price-pane-bottom");
35246
35625
  this.mountContainer = null;
@@ -35278,7 +35657,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35278
35657
  }
35279
35658
  const skipFit = opts?.preserveView === true && this.didInitialFit;
35280
35659
  if (this.coords.width > 0 && !skipFit) {
35281
- this.fitContent();
35660
+ if (this.didInitialFit) this.reframeKeepZoom();
35661
+ else this.fitContent();
35282
35662
  this.didInitialFit = true;
35283
35663
  }
35284
35664
  if (!this.introPlayed && this.bars.length > 0) {
@@ -35333,7 +35713,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35333
35713
  ensurePane(pane) {
35334
35714
  this.scene.ensurePane(pane.id, pane.kind, pane.order, pane.heightWeight ?? (pane.kind === "price" ? 3 : 1));
35335
35715
  this.layoutPanes();
35336
- this.repositionTables();
35337
35716
  this.paneControls?.refresh();
35338
35717
  this.scheduler.invalidate(4 /* Full */);
35339
35718
  }
@@ -35355,17 +35734,14 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35355
35734
  if (!model.ownScale) this.scene.dropIndicatorScale(handle.id);
35356
35735
  this.inputsUI.setPane(handle.id, paneId);
35357
35736
  this.refreshAnchorOffset(model);
35358
- this.syncTables(model);
35359
35737
  this.refreshAxisWidth();
35360
35738
  this.layoutPanes();
35361
- this.repositionTables();
35362
35739
  this.paneControls?.refresh();
35363
35740
  this.scheduler.invalidate(4 /* Full */);
35364
35741
  }
35365
35742
  orderPanes(orderedIds) {
35366
35743
  this.scene.orderPanes(orderedIds);
35367
35744
  this.layoutPanes();
35368
- this.repositionTables();
35369
35745
  this.paneControls?.refresh();
35370
35746
  this.scheduler.invalidate(4 /* Full */);
35371
35747
  }
@@ -35374,7 +35750,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35374
35750
  if (!pane || pane.collapsed === collapsed) return;
35375
35751
  pane.collapsed = collapsed;
35376
35752
  this.layoutPanes();
35377
- this.repositionTables();
35378
35753
  this.paneControls?.refresh();
35379
35754
  this.scheduler.invalidate(4 /* Full */);
35380
35755
  }
@@ -35382,7 +35757,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35382
35757
  if (paneId !== null && !this.scene.panes.has(paneId)) paneId = null;
35383
35758
  this.maximizedPaneId = paneId;
35384
35759
  this.layoutPanes();
35385
- this.repositionTables();
35386
35760
  this.paneControls?.refresh();
35387
35761
  this.scheduler.invalidate(4 /* Full */);
35388
35762
  }
@@ -35469,7 +35843,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35469
35843
  native: !!model.native,
35470
35844
  ...model.props ? { props: model.props, propValues: model.propValues ?? {} } : {}
35471
35845
  });
35472
- this.syncTables(model);
35473
35846
  if (model.native?.type === "volume") {
35474
35847
  this.volumeActive = true;
35475
35848
  this.volumeHidden = false;
@@ -35493,7 +35866,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35493
35866
  }
35494
35867
  }
35495
35868
  applyPatch(model, patch);
35496
- this.syncTables(model);
35497
35869
  this.scheduler.invalidate(3 /* Light */);
35498
35870
  }
35499
35871
  removeIndicator(handle) {
@@ -35512,8 +35884,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35512
35884
  this.scene.forgetAnchorOffset(handle.id);
35513
35885
  this.scene.dropIndicatorScale(handle.id);
35514
35886
  this.inputsUI.remove(handle.id);
35515
- this.tableOverlays.get(handle.id)?.destroy();
35516
- this.tableOverlays.delete(handle.id);
35517
35887
  this.refreshAxisWidth();
35518
35888
  this.paneControls?.refresh();
35519
35889
  this.scheduler.invalidate(4 /* Full */);
@@ -35557,8 +35927,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35557
35927
  }
35558
35928
  if (!visible) {
35559
35929
  this.scene.indicators.delete(handle.id);
35560
- this.tableOverlays.get(handle.id)?.destroy();
35561
- this.tableOverlays.delete(handle.id);
35562
35930
  }
35563
35931
  this.inputsUI.setVisible(handle.id, visible);
35564
35932
  this.scheduler.invalidate(4 /* Full */);
@@ -35604,6 +35972,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35604
35972
  this.userDrawings?.setLayoutMode(mode);
35605
35973
  this.settingsDialog?.setLayoutMode(mode);
35606
35974
  this.inputsUI?.setLayoutMode(mode);
35975
+ this.paneControls?.setSuspended(mode === "mobile");
35607
35976
  if (this.scrollButton) {
35608
35977
  const px = mode === "mobile" ? SCROLL_BTN_SIZE_TOUCH : SCROLL_BTN_SIZE;
35609
35978
  this.scrollButton.style.width = `${px}px`;
@@ -35937,6 +36306,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35937
36306
  this.scaleDragHeight = res.height;
35938
36307
  this.scaleDragStart = { ...res.holder.scale };
35939
36308
  res.holder.manualScale = { ...res.holder.scale };
36309
+ this.axisScaleButtons?.reposition();
35940
36310
  this.scheduler.invalidate(4 /* Full */);
35941
36311
  }
35942
36312
  /** Rescale the grabbed scale around its center by the total drag (down ⇒ zoom out). */
@@ -35968,6 +36338,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35968
36338
  const res = this.resolveScaleHolder(x, y);
35969
36339
  if (!res) return;
35970
36340
  res.holder.manualScale = null;
36341
+ this.axisScaleButtons?.reposition();
35971
36342
  this.scheduler.invalidate(4 /* Full */);
35972
36343
  }
35973
36344
  /**
@@ -35999,7 +36370,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35999
36370
  /** Relayout + repaint + refresh the hover buttons after a collapse/maximize/order change. */
36000
36371
  afterPaneLayoutChange() {
36001
36372
  this.layoutPanes();
36002
- this.repositionTables();
36003
36373
  this.paneControls?.refresh();
36004
36374
  this.scheduler.invalidate(4 /* Full */);
36005
36375
  }
@@ -36080,7 +36450,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36080
36450
  above.heightWeight = next2.above;
36081
36451
  below.heightWeight = next2.below;
36082
36452
  this.layoutPanes();
36083
- this.repositionTables();
36084
36453
  this.scheduler.invalidate(4 /* Full */);
36085
36454
  }
36086
36455
  /** Double-click a separator → split the two adjacent panes evenly (each gets half of
@@ -36095,7 +36464,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36095
36464
  above.heightWeight = half;
36096
36465
  below.heightWeight = half;
36097
36466
  this.layoutPanes();
36098
- this.repositionTables();
36099
36467
  this.scheduler.invalidate(4 /* Full */);
36100
36468
  }
36101
36469
  // ── keyboard navigation / accessibility (item 11) ──
@@ -36374,7 +36742,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36374
36742
  const liveActual = li >= 0 ? this.bars[li] : void 0;
36375
36743
  const easeLive = !!liveActual && this.liveEaseTime === liveActual.time && (liveActual.high !== this.liveEaseHigh || liveActual.low !== this.liveEaseLow || liveActual.close !== this.liveEaseClose);
36376
36744
  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();
36745
+ this.scene.drawingSlices = mergeSlices(
36746
+ this.indicatorSlices.prepare(this.scene, this.coords, this.theme, this.dataCanvas),
36747
+ this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map()
36748
+ );
36378
36749
  this.backdropRenderer.render(this.scene, this.coords, this.theme, gridAlpha);
36379
36750
  this.backend.render(this.scene, this.coords, this.theme);
36380
36751
  this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
@@ -36605,6 +36976,20 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36605
36976
  this.coords.setViewport(v);
36606
36977
  this.targetBarSpacing = v.barSpacing;
36607
36978
  }
36979
+ /** Re-frame after a series replacement (a symbol/timeframe switch): keep the user's
36980
+ * zoom (bar spacing), re-anchor the newest bars at the default right offset.
36981
+ * `clampViewport`'s fit-all-bars floor deliberately does NOT apply — a progressive
36982
+ * head may still be backfilling toward the previous depth, and raising the spacing
36983
+ * to its temporary bar count would lose the zoom this exists to keep. */
36984
+ reframeKeepZoom() {
36985
+ this.animator?.stop();
36986
+ this.panVelocity = 0;
36987
+ for (const pane of this.scene.panes.values()) pane.manualScale = null;
36988
+ for (const sl of this.scene.indicatorScales.values()) sl.manualScale = null;
36989
+ const v = { barSpacing: clampBarSpacing(this.coords.getViewport().barSpacing), rightOffset: defaultViewport().rightOffset };
36990
+ this.coords.setViewport(v);
36991
+ this.targetBarSpacing = v.barSpacing;
36992
+ }
36608
36993
  paneBoundsFor(paneId) {
36609
36994
  const p = this.scene.panes.get(paneId);
36610
36995
  return { top: p?.bounds.top ?? 0, height: p?.bounds.height ?? 0, rightAxis: this.rightAxisW };
@@ -36812,27 +37197,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36812
37197
  }
36813
37198
  return maxVol;
36814
37199
  }
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
37200
  layoutPanes() {
36837
37201
  const panes = this.scene.orderedPanes();
36838
37202
  const dataHeight = this.coords.height;
@@ -36883,6 +37247,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36883
37247
  const visible = maxPane ? [maxPane] : this.scene.orderedPanes().filter((p) => !p.collapsed);
36884
37248
  const paneBottom = visible.length ? Math.max(...visible.map((p) => p.bounds.top + p.bounds.height)) : dataHeight;
36885
37249
  this.scrollBtnBottomPx = SCROLL_BTN_BOTTOM + Math.max(0, dataHeight - paneBottom);
37250
+ this.mountContainer?.style.setProperty("--vela-bottom-gutter", `${TIME_AXIS_H + Math.max(0, dataHeight - paneBottom)}px`);
36886
37251
  this.scrollBtnRightPx = this.rightAxisW + SCROLL_BTN_RIGHT_INSET;
36887
37252
  if (this.scrollButton) {
36888
37253
  this.scrollButton.style.bottom = `${this.scrollBtnBottomPx}px`;
@@ -36975,30 +37340,33 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36975
37340
  if (w <= 0 || h <= 0) return;
36976
37341
  const dpr = window.devicePixelRatio || 1;
36977
37342
  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;
37343
+ const rect = this.plot.getBoundingClientRect();
37344
+ let bw = Math.max(1, Math.round(rect.width * dpr));
37345
+ let bh = Math.max(1, Math.round(rect.height * dpr));
37346
+ const dev = this.plotDeviceSize;
37347
+ if (dev && Math.abs(dev.width - rect.width * dpr) <= 1 && Math.abs(dev.height - rect.height * dpr) <= 1) {
37348
+ bw = Math.max(1, dev.width);
37349
+ bh = Math.max(1, dev.height);
37350
+ }
37351
+ const pw = bw / dpr;
37352
+ const ph = bh / dpr;
37353
+ const size3 = (canvas) => {
37354
+ canvas.width = bw;
37355
+ canvas.height = bh;
37356
+ canvas.style.width = `${pw}px`;
37357
+ canvas.style.height = `${ph}px`;
37358
+ };
37359
+ size3(this.dataCanvas);
37360
+ size3(this.backdropCanvas);
37361
+ size3(this.volumeCanvas);
37362
+ for (const l of this.extLayers) size3(l.canvas);
37363
+ size3(this.vpvrCanvas);
37364
+ size3(this.chromeCanvas);
37365
+ size3(this.drawingsCanvas);
37366
+ size3(this.cursorCanvas);
36998
37367
  this.coords.setSize(Math.max(1, pw - this.rightAxisW), Math.max(1, ph - TIME_AXIS_H), dpr);
36999
37368
  this.scene.crosshair = null;
37000
37369
  this.layoutPanes();
37001
- this.repositionTables();
37002
37370
  this.userDrawings?.onResize();
37003
37371
  if (!this.didInitialFit && this.coords.barCount > 0) {
37004
37372
  this.fitContent();
@@ -39051,6 +39419,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
39051
39419
  exports.getDrawingType = getDrawingType;
39052
39420
  exports.getNativeIndicator = getNativeIndicator;
39053
39421
  exports.iconMarkup = iconMarkup;
39422
+ exports.inputDeltas = inputDeltas;
39054
39423
  exports.inputVisible = inputVisible;
39055
39424
  exports.legendActions = legendActions;
39056
39425
  exports.legendCallouts = legendCallouts;