@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
package/dist/index.cjs CHANGED
@@ -154,6 +154,12 @@ var IndicatorHandleImpl = class {
154
154
  get visible() {
155
155
  return this.visibleState;
156
156
  }
157
+ inputValues() {
158
+ return this.controller.inputValuesOf(this.id);
159
+ }
160
+ propValues() {
161
+ return this.controller.propValuesOf(this.id);
162
+ }
157
163
  setInput(key, value) {
158
164
  this.controller.applyInputs(this.id, { [key]: value });
159
165
  }
@@ -552,6 +558,8 @@ registerIcon("folder-plus", S('<path d="M1.6 4.4a1 1 0 0 1 1-1h2.7l1.3 1.7h6.8a1
552
558
  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"/>'));
553
559
  registerIcon("collapse", S('<path d="M3 8h10"/>'));
554
560
  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"/>'));
561
+ registerIcon("plus", S('<path d="M8 2.8v10.4M2.8 8h10.4"/>'));
562
+ registerIcon("minus", S('<path d="M2.8 8h10.4"/>'));
555
563
  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"/>'));
556
564
  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"/>'));
557
565
  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"/>'));
@@ -559,7 +567,7 @@ registerIcon("star-filled", S('<path d="M8 2.2l1.75 3.55 3.9.55-2.8 2.75.65 3.9L
559
567
  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"'));
560
568
  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"'));
561
569
  registerIcon("burger", S('<path d="M2.5 4.5h11M2.5 8h11M2.5 11.5h11"/>'));
562
- 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"/>'));
570
+ 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"/>'));
563
571
  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"/>'));
564
572
  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"/>'));
565
573
  registerIcon("cursor", svg24('<path d="M5 3l6 16 2-6 6-2z"/>', 'fill="currentColor" stroke="none"'));
@@ -6930,6 +6938,23 @@ function timeframeToMs(timeframe) {
6930
6938
  return 36e5;
6931
6939
  }
6932
6940
 
6941
+ // src/data/symbol-groups.ts
6942
+ function isGroupRow(d) {
6943
+ return d.group != null && d.ticker === d.group;
6944
+ }
6945
+ function groupKeyOf(d) {
6946
+ return `${(d.prefix ?? d.provider ?? "").toLowerCase()}:${(d.group ?? "").toUpperCase()}`;
6947
+ }
6948
+ function groupMembers(pool, groupRow) {
6949
+ const key = groupKeyOf(groupRow);
6950
+ return pool.filter((s) => s.group != null && !isGroupRow(s) && groupKeyOf(s) === key);
6951
+ }
6952
+ function defaultMemberOf(pool, groupRow) {
6953
+ const members = groupMembers(pool, groupRow);
6954
+ const defaults2 = members.filter((m) => m.default);
6955
+ return (defaults2.length === 1 ? defaults2[0] : members[0]) ?? null;
6956
+ }
6957
+
6933
6958
  // src/data/ProviderRegistry.ts
6934
6959
  var normName = (n) => n.trim().toLowerCase();
6935
6960
  var normTicker = (t) => t.trim().toUpperCase();
@@ -7019,17 +7044,24 @@ var ProviderRegistry = class {
7019
7044
  resolve(raw, opts = {}) {
7020
7045
  const { provider, ticker } = this.parse(raw);
7021
7046
  if (provider) {
7022
- if (this.entries.has(provider)) return { provider, ticker };
7047
+ if (this.entries.has(provider)) {
7048
+ const d = this.entries.get(provider).byTicker?.get(normTicker(ticker));
7049
+ return { provider, ticker: d != null && isGroupRow(d) ? this.loadableTicker(provider, d) : ticker };
7050
+ }
7023
7051
  const key = `${provider}:${normTicker(ticker)}`;
7024
7052
  for (const name of this.candidateOrder(opts.default)) {
7025
7053
  const d = this.entries.get(name).prefixIndex?.get(key);
7026
- if (d) return { provider: name, ticker: d.ticker };
7054
+ if (d) return { provider: name, ticker: this.loadableTicker(name, d) };
7027
7055
  }
7028
7056
  return null;
7029
7057
  }
7030
7058
  const norm = normTicker(ticker);
7031
7059
  for (const name of this.candidateOrder(opts.default)) {
7032
- if (this.entries.get(name).index?.has(norm)) return { provider: name, ticker };
7060
+ const e = this.entries.get(name);
7061
+ if (e.index?.has(norm)) {
7062
+ const d = e.byTicker?.get(norm);
7063
+ return { provider: name, ticker: d != null && isGroupRow(d) ? this.loadableTicker(name, d) : ticker };
7064
+ }
7033
7065
  }
7034
7066
  if (opts.lenient && this.entries.size === 1) {
7035
7067
  const only = this.names()[0];
@@ -7038,6 +7070,13 @@ var ProviderRegistry = class {
7038
7070
  }
7039
7071
  return null;
7040
7072
  }
7073
+ /** What a resolved descriptor LOADS: itself — unless it is a GROUP row (listed, never
7074
+ * served), which loads its default member (single `default`, else first listed). A
7075
+ * memberless group keeps its own ticker and fails downstream like any unknown symbol. */
7076
+ loadableTicker(name, d) {
7077
+ if (!isGroupRow(d)) return d.ticker;
7078
+ return defaultMemberOf(this.entries.get(name)?.descriptors ?? [], d)?.ticker ?? d.ticker;
7079
+ }
7041
7080
  /**
7042
7081
  * Resolve now if possible, else resolve later — re-attempting after each provider
7043
7082
  * settles. The promise stays pending until some registered provider can serve the
@@ -7263,6 +7302,7 @@ var RUN_EMIT_THROTTLE_MS = 1e3;
7263
7302
  var PREVIEW_BARS = 300;
7264
7303
  var SINGLE_LOAD_BARS = 5e3;
7265
7304
  var CHUNK_BARS = 1e4;
7305
+ var FIRST_PAINT_BARS = 100;
7266
7306
  var GAP_FACTOR = 1.5;
7267
7307
  var HEAL_COOLDOWN_MS = 5e3;
7268
7308
  var EngineOrchestrator = class _EngineOrchestrator {
@@ -7326,6 +7366,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
7326
7366
  /** Invalidates detached async work (backfill loops, in-flight loads, gap heals):
7327
7367
  * bumped by init(), setMarket() and destroy(). */
7328
7368
  this.generation = 0;
7369
+ /** Aborts the in-flight PROGRESSIVE load's source polling on supersession — an
7370
+ * abandoned stream left polling to its own budget starves the browser's per-host
7371
+ * connection pool, and the NEXT symbol's very first fetch with it (measured). */
7372
+ this.progressiveAbort = null;
7329
7373
  /** Awaiters racing a superseded load (setMarket callers) — released on every bump so they never hang. */
7330
7374
  this.supersedeWaiters = [];
7331
7375
  /** `history:complete` fired for the CURRENT load. Each market load re-arms the cycle
@@ -7468,6 +7512,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
7468
7512
  * superseded setMarket awaiters so their promises resolve instead of hanging. */
7469
7513
  bumpGeneration() {
7470
7514
  const gen = ++this.generation;
7515
+ this.progressiveAbort?.abort();
7516
+ this.progressiveAbort = null;
7471
7517
  for (const w of this.supersedeWaiters.splice(0)) w();
7472
7518
  return gen;
7473
7519
  }
@@ -7521,7 +7567,48 @@ var EngineOrchestrator = class _EngineOrchestrator {
7521
7567
  const requested = market.bars ?? 500;
7522
7568
  const initialRange = market.visibleRange;
7523
7569
  const deep = !market.data?.length && initialRange == null && requested > SINGLE_LOAD_BARS;
7524
- if (deep && this.feed.loadRange) {
7570
+ let progressiveServed = false;
7571
+ if (!market.data?.length && initialRange == null && this.feed.loadProgressive) {
7572
+ let painted = false;
7573
+ const paint = (bars, final) => {
7574
+ if (this.generation !== gen || !final && bars.length === 0) return;
7575
+ if (!painted && !final && bars.length < Math.min(requested, FIRST_PAINT_BARS)) return;
7576
+ this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
7577
+ if (!painted && bars.length > 0) {
7578
+ painted = true;
7579
+ if (opts.firstLoad) this.activateBarLayers();
7580
+ if (!final) this.historyState = "backfill";
7581
+ }
7582
+ };
7583
+ const abort = new AbortController();
7584
+ this.progressiveAbort = abort;
7585
+ progressiveServed = await new Promise((firstPaint) => {
7586
+ let signaled = false;
7587
+ const signal = (served) => {
7588
+ if (!signaled) {
7589
+ signaled = true;
7590
+ firstPaint(served);
7591
+ }
7592
+ };
7593
+ abort.signal.addEventListener("abort", () => signal(true), { once: true });
7594
+ this.feed.loadProgressive(market, (bars) => {
7595
+ paint(bars, false);
7596
+ if (painted) signal(true);
7597
+ }, { signal: abort.signal }).then((full) => {
7598
+ if (this.progressiveAbort === abort) this.progressiveAbort = null;
7599
+ if (full == null) return signal(false);
7600
+ if (this.generation !== gen) return signal(true);
7601
+ paint(full, true);
7602
+ this.completeHistory(full.length >= requested ? "depth" : "genesis");
7603
+ signal(true);
7604
+ }).catch(() => {
7605
+ if (this.progressiveAbort === abort) this.progressiveAbort = null;
7606
+ if (this.generation === gen) this.completeHistory("aborted");
7607
+ signal(true);
7608
+ });
7609
+ });
7610
+ }
7611
+ if (progressiveServed) ; else if (deep && this.feed.loadRange) {
7525
7612
  const head = await this.feed.load({ ...market, bars: Math.min(requested, CHUNK_BARS) });
7526
7613
  if (this.generation !== gen) return;
7527
7614
  this.setBarSeries(head);
@@ -8116,6 +8203,15 @@ var EngineOrchestrator = class _EngineOrchestrator {
8116
8203
  record.pendingCause = "inputs";
8117
8204
  if (record.session) record.session.update(record.inputValues);
8118
8205
  else if (record.native && !record.hidden) record.native.instance.setInputs(record.inputValues);
8206
+ this.events.emit("indicator:inputs", { id });
8207
+ }
8208
+ /** IndicatorController: the CURRENT stored input values (defaults merged with edits). */
8209
+ inputValuesOf(id) {
8210
+ return { ...this.registry.get(id)?.inputValues };
8211
+ }
8212
+ /** IndicatorController: the CURRENT declaration-prop overrides. */
8213
+ propValuesOf(id) {
8214
+ return { ...this.registry.get(id)?.propValues };
8119
8215
  }
8120
8216
  /** IndicatorController: re-run an indicator with merged declaration-prop overrides.
8121
8217
  * Same lifecycle as {@link applyInputs} — a prop change replays the whole script.
@@ -8129,6 +8225,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
8129
8225
  if (!record.hidden) this.setLoading(record, true);
8130
8226
  record.pendingCause = "inputs";
8131
8227
  record.session.update(record.inputValues, record.propValues);
8228
+ this.events.emit("indicator:inputs", { id });
8132
8229
  }
8133
8230
  /** IndicatorController: tear down an indicator and (if now empty) its pane. */
8134
8231
  /** Live handles of every indicator on the chart (script + native), insertion order. */
@@ -9193,6 +9290,28 @@ var CachingDataFeed = class {
9193
9290
  this.store.merge(key, dropForming(bars));
9194
9291
  return bars;
9195
9292
  }
9293
+ /**
9294
+ * Progressive twin of {@link load}: a COLD load streams through the inner feed's
9295
+ * progressive path — batches forwarded verbatim, the FINAL answer cached exactly as
9296
+ * `load` caches — while a cache-covered load answers once through `load` itself (a
9297
+ * warm chart has nothing to stream). Falls back to `load` wholesale when the inner
9298
+ * feed lacks the capability, so callers may prefer this method unconditionally.
9299
+ */
9300
+ async loadProgressive(cfg, onBatch, opts) {
9301
+ if (cfg.data && cfg.data.length > 0) return this.inner.load(cfg);
9302
+ if (!this.inner.loadProgressive) return null;
9303
+ const symbol = cfg.symbol ?? "TEST";
9304
+ const key = cacheKey(symbol, cfg.timeframe ?? "60", cfg.session);
9305
+ const cached = this.store.get(key);
9306
+ const n = cfg.bars ?? 500;
9307
+ const lastCached = cached?.[cached.length - 1];
9308
+ if (cached && lastCached && cached.length >= n - 1 && this.inner.loadRange) return this.load(cfg);
9309
+ this.store.retainSymbol(symbol);
9310
+ const bars = await this.inner.loadProgressive(cfg, onBatch, opts);
9311
+ if (bars == null) return null;
9312
+ this.store.merge(key, dropForming(bars));
9313
+ return bars;
9314
+ }
9196
9315
  /**
9197
9316
  * Cache-backed ranged fetch — the gateway engines use for secondary series
9198
9317
  * (`request.security` HTF/LTF/cross-symbol) and the orchestrator's backward
@@ -9438,6 +9557,17 @@ var MultiProviderFeed = class {
9438
9557
  this.prefetchSymbolInfo(resolved);
9439
9558
  return this.cache.load(canonical(cfg, resolved));
9440
9559
  }
9560
+ /** Progressive twin of {@link load} — same resolution, the cache streams the batches. */
9561
+ async loadProgressive(cfg, onBatch, opts) {
9562
+ if (cfg.data && cfg.data.length > 0) {
9563
+ this.liveBars = cfg.data.map((b) => ({ ...b }));
9564
+ return cfg.data;
9565
+ }
9566
+ const resolved = await this.registry.whenResolvable(rawSymbol(cfg), { default: this.primaryProvider });
9567
+ this.primaryProvider = resolved.provider;
9568
+ this.prefetchSymbolInfo(resolved);
9569
+ return this.cache.loadProgressive(canonical(cfg, resolved), onBatch, opts);
9570
+ }
9441
9571
  /**
9442
9572
  * Synchronous per-symbol metadata for engines (Pine `syminfo.*`), served from the cache
9443
9573
  * warmed by load(). Undefined until the prefetch lands (the engine then synthesizes a
@@ -9525,6 +9655,18 @@ var RegistryFetchFeed = class {
9525
9655
  if (!provider) return Promise.resolve([]);
9526
9656
  return safeBars(provider, ticker, cfg.timeframe ?? "60", { limit: cfg.bars ?? 500, session: cfg.session });
9527
9657
  }
9658
+ async loadProgressive(cfg, onBatch, opts) {
9659
+ const { provider: name, ticker } = parseSymbol(cfg.symbol ?? "");
9660
+ const provider = this.registry.get(name ?? "");
9661
+ if (!provider) return [];
9662
+ if (!provider.getBarsProgressive) return null;
9663
+ try {
9664
+ return await provider.getBarsProgressive(ticker, cfg.timeframe ?? "60", { limit: cfg.bars ?? 500, session: cfg.session }, onBatch, opts);
9665
+ } catch (e) {
9666
+ console.warn(`[vela] progressive fetch failed for ${ticker} ${cfg.timeframe ?? "60"} \u2014 ${e instanceof Error ? e.message : String(e)}`);
9667
+ return [];
9668
+ }
9669
+ }
9528
9670
  loadRange(cfg, range) {
9529
9671
  const { provider: name, ticker } = parseSymbol(cfg.symbol ?? "");
9530
9672
  const provider = this.registry.get(name ?? "");
@@ -10787,6 +10929,10 @@ var CalloutBubble = class {
10787
10929
  get open() {
10788
10930
  return this.pop?.open ?? false;
10789
10931
  }
10932
+ /** Close the deployed panel, if any. The bubble itself stays. */
10933
+ hidePanel() {
10934
+ this.pop?.hide();
10935
+ }
10790
10936
  destroy() {
10791
10937
  this.pop?.destroy();
10792
10938
  this.pop = null;
@@ -10867,6 +11013,14 @@ function inputVisible(when, values) {
10867
11013
  const conds = Array.isArray(when) ? when : [when];
10868
11014
  return conds.every((c) => c.anyOf ? c.anyOf.some((x) => x === values[c.key]) : values[c.key] === c.equals);
10869
11015
  }
11016
+ function inputDeltas(schema, values) {
11017
+ const out = {};
11018
+ for (const s of schema) {
11019
+ const v = values[s.key];
11020
+ if (v !== void 0 && JSON.stringify(v) !== JSON.stringify(s.defval)) out[s.key] = v;
11021
+ }
11022
+ return Object.keys(out).length > 0 ? out : void 0;
11023
+ }
10870
11024
 
10871
11025
  // src/ui/components/select/controller.ts
10872
11026
  function selectController(opts) {
@@ -13694,6 +13848,9 @@ var CLOSE_SVG = iconAt("close", LEGEND_ICON_PX2);
13694
13848
  var FOLD_SVG = iconAt("chevron-up", LEGEND_ICON_PX2);
13695
13849
  var UNFOLD_SVG = iconAt("chevron-down", LEGEND_ICON_PX2);
13696
13850
  var OVERVIEW_SVG = iconAt("objects", LEGEND_ICON_PX2);
13851
+ function legendCalloutsDisplay(open2, hasCallouts) {
13852
+ return !open2 && hasCallouts ? "inline-flex" : "none";
13853
+ }
13697
13854
  var InputsUI = class {
13698
13855
  constructor(container, theme, paneBoundsOf) {
13699
13856
  this.container = container;
@@ -13897,7 +14054,7 @@ var InputsUI = class {
13897
14054
  row.callouts = [];
13898
14055
  row.calloutsEl.replaceChildren();
13899
14056
  const views = this.legendCallouts?.(row.id) ?? [];
13900
- row.calloutsEl.style.display = views.length > 0 ? "inline-flex" : "none";
14057
+ row.calloutsEl.style.display = legendCalloutsDisplay(row.highlighted, views.length > 0);
13901
14058
  for (const view of views) {
13902
14059
  const bubble = new CalloutBubble({
13903
14060
  icon: view.icon,
@@ -14381,11 +14538,11 @@ var InputsUI = class {
14381
14538
  row.controlsEl.style.display = open2 || row.hidden ? "inline-flex" : "none";
14382
14539
  if (open2) {
14383
14540
  row.el.appendChild(row.statusEl);
14384
- row.el.appendChild(row.calloutsEl);
14541
+ for (const bubble of row.callouts) bubble.hidePanel();
14385
14542
  } else {
14386
14543
  row.el.insertBefore(row.statusEl, row.valuesEl);
14387
- row.el.insertBefore(row.calloutsEl, row.statusEl);
14388
14544
  }
14545
+ row.calloutsEl.style.display = legendCalloutsDisplay(open2, row.callouts.length > 0);
14389
14546
  for (const child of Array.from(row.controlsEl.children)) {
14390
14547
  if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
14391
14548
  if (child === row.extrasEl) {
@@ -14475,7 +14632,7 @@ var ICONS = {
14475
14632
  };
14476
14633
  var STYLE_ID = "vela-pane-controls";
14477
14634
  var ICON_PX = 12;
14478
- var CLUSTER_PILL = "rgba(0,0,0,0.28)";
14635
+ var CLUSTER_PILL = "rgba(0,0,0,0.65)";
14479
14636
  function ensureStyles() {
14480
14637
  if (typeof document === "undefined" || document.getElementById(STYLE_ID)) return;
14481
14638
  const st = document.createElement("style");
@@ -14495,8 +14652,12 @@ var PaneControls = class {
14495
14652
  this.deps = deps;
14496
14653
  this.clusters = /* @__PURE__ */ new Map();
14497
14654
  this.hoverPaneId = null;
14655
+ /** Mobile: hover clusters are meaningless without a cursor — suppressed; a
14656
+ * collapsed pane's standalone expand chip stays (the only way back up). */
14657
+ this.suspended = false;
14498
14658
  /** Reveal the cluster for the pane under the cursor, resolved from the pointer's y in the plot. */
14499
14659
  this.onPlotMove = (e) => {
14660
+ if (this.suspended) return;
14500
14661
  const rect = this.plot.getBoundingClientRect();
14501
14662
  const y = e.clientY - rect.top;
14502
14663
  let hit = null;
@@ -14568,7 +14729,12 @@ var PaneControls = class {
14568
14729
  }
14569
14730
  if (p.count > 1) {
14570
14731
  cluster.appendChild(
14571
- this.button(p.maximized ? ICONS.restore : ICONS.maximize, p.maximized ? "Restore pane" : "Maximize pane", false, () => this.deps.onToggleMaximize(p.id), { role: "maximize" })
14732
+ this.button(p.maximized ? ICONS.restore : ICONS.maximize, p.maximized ? "Restore pane" : "Maximize pane", false, () => this.deps.onToggleMaximize(p.id), {
14733
+ role: "maximize",
14734
+ // Same inverse-chip treatment as the collapsed pane's expand toggle: the
14735
+ // maximized state must read as an active state, not just a swapped glyph.
14736
+ selected: p.maximized
14737
+ })
14572
14738
  );
14573
14739
  }
14574
14740
  }
@@ -14605,17 +14771,18 @@ var PaneControls = class {
14605
14771
  }
14606
14772
  const hovered = id === this.hoverPaneId;
14607
14773
  const hasButtons = cluster.children.length > 0;
14608
- const visible = hasButtons && (hovered || p.collapsed) && p.height > 8;
14774
+ const stateChipRole = p.collapsed ? "collapse" : !this.suspended && p.maximized ? "maximize" : null;
14775
+ const visible = hasButtons && (hovered || stateChipRole != null) && p.height > 8;
14609
14776
  cluster.style.right = `${rightPx}px`;
14610
14777
  cluster.style.top = p.collapsed ? `${p.top + Math.max(1, Math.round((p.height - 24) / 2))}px` : `${p.top + 4}px`;
14611
14778
  cluster.style.display = visible ? "flex" : "none";
14612
14779
  if (!visible) continue;
14613
- const soloExpand = p.collapsed && !hovered;
14614
- cluster.style.background = soloExpand ? "transparent" : CLUSTER_PILL;
14780
+ const soloChip = stateChipRole != null && !hovered;
14781
+ cluster.style.background = soloChip ? "transparent" : CLUSTER_PILL;
14615
14782
  for (const child of cluster.children) {
14616
14783
  const btn2 = child;
14617
14784
  btn2.style.display = "inline-flex";
14618
- btn2.style.visibility = soloExpand && btn2.dataset.role !== "collapse" ? "hidden" : "visible";
14785
+ btn2.style.visibility = soloChip && btn2.dataset.role !== stateChipRole ? "hidden" : "visible";
14619
14786
  }
14620
14787
  }
14621
14788
  }
@@ -14625,6 +14792,14 @@ var PaneControls = class {
14625
14792
  this.hoverPaneId = paneId;
14626
14793
  this.reposition();
14627
14794
  }
14795
+ /** Mobile suppression: no hover clusters (touch has no cursor; the shell's own
14796
+ * chrome covers maximize), while collapsed panes keep their expand chips. */
14797
+ setSuspended(on) {
14798
+ if (on === this.suspended) return;
14799
+ this.suspended = on;
14800
+ if (on) this.hoverPaneId = null;
14801
+ this.reposition();
14802
+ }
14628
14803
  destroy() {
14629
14804
  this.plot.removeEventListener("pointermove", this.onPlotMove);
14630
14805
  this.plot.removeEventListener("pointerleave", this.onPlotLeave);
@@ -14775,160 +14950,6 @@ var AxisScaleButtons = class {
14775
14950
  }
14776
14951
  };
14777
14952
 
14778
- // src/renderers/shared/TableOverlay.ts
14779
- var SIZE_PX3 = {
14780
- auto: 13,
14781
- tiny: 10,
14782
- small: 11,
14783
- normal: 13,
14784
- large: 16,
14785
- huge: 20
14786
- };
14787
- function fontPxOf(size) {
14788
- if (typeof size === "number") return size > 0 ? size : SIZE_PX3.auto;
14789
- return SIZE_PX3[size] ?? SIZE_PX3.auto;
14790
- }
14791
- function tableHasContent(t) {
14792
- return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
14793
- }
14794
- function mergeRenderPlan(t) {
14795
- const span = /* @__PURE__ */ new Map();
14796
- const omit = /* @__PURE__ */ new Set();
14797
- for (const m of t.merges) {
14798
- span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
14799
- for (let r = m.startRow; r <= m.endRow; r += 1) {
14800
- for (let c = m.startCol; c <= m.endCol; c += 1) {
14801
- if (r !== m.startRow || c !== m.startCol) omit.add(`${r}:${c}`);
14802
- }
14803
- }
14804
- }
14805
- for (let r = 0; r < t.rows; r += 1) {
14806
- for (let c = 0; c < t.columns; c += 1) {
14807
- if (t.cells[r]?.[c]?.merged && !span.has(`${r}:${c}`)) omit.add(`${r}:${c}`);
14808
- }
14809
- }
14810
- for (const key of span.keys()) omit.delete(key);
14811
- return { span, omit };
14812
- }
14813
- var TableOverlay = class {
14814
- constructor(container, theme, paneBounds) {
14815
- this.container = container;
14816
- this.theme = theme;
14817
- this.paneBounds = paneBounds;
14818
- this.lastTables = [];
14819
- if (getComputedStyle(container).position === "static") container.style.position = "relative";
14820
- this.root = document.createElement("div");
14821
- Object.assign(this.root.style, {
14822
- position: "absolute",
14823
- inset: "0",
14824
- pointerEvents: "none",
14825
- overflow: "hidden",
14826
- zIndex: "3"
14827
- });
14828
- container.appendChild(this.root);
14829
- }
14830
- update(tables) {
14831
- this.lastTables = tables;
14832
- this.root.replaceChildren();
14833
- for (const t of tables) {
14834
- if (tableHasContent(t)) this.root.appendChild(this.renderTable(t));
14835
- }
14836
- }
14837
- /** Re-render at the current pane geometry — after layout settles or on resize. */
14838
- reposition() {
14839
- if (this.root.isConnected) this.update(this.lastTables);
14840
- }
14841
- /** Show/hide the whole overlay. Tables anchor to pane corners, not to bars, so unlike the
14842
- * series content they DON'T vanish with an emptied chart — the loading state hides them. */
14843
- setVisible(visible) {
14844
- this.root.style.display = visible ? "" : "none";
14845
- }
14846
- destroy() {
14847
- this.root.remove();
14848
- }
14849
- renderTable(t) {
14850
- const b = this.paneBounds(t.paneId);
14851
- const wrap = document.createElement("div");
14852
- wrap.style.position = "absolute";
14853
- if (t.frameColor && t.frameWidth > 0) wrap.style.border = `${t.frameWidth}px solid ${t.frameColor}`;
14854
- this.anchor(wrap, t.position, b);
14855
- const table = document.createElement("table");
14856
- Object.assign(table.style, {
14857
- borderCollapse: "collapse",
14858
- background: t.bgColor ?? "transparent",
14859
- fontFamily: this.theme.fontFamily || "sans-serif",
14860
- border: "none",
14861
- tableLayout: "auto",
14862
- // Re-enable pointer events on the table only (root is none) so cell tooltips work.
14863
- pointerEvents: "auto"
14864
- });
14865
- const { span, omit } = mergeRenderPlan(t);
14866
- const plotW = Math.max(0, (this.root.clientWidth || this.container.clientWidth) - b.rightAxis);
14867
- const paneH = b.height;
14868
- const cellBorder = t.borderColor && t.borderWidth > 0 ? `${t.borderWidth}px solid ${t.borderColor}` : "none";
14869
- for (let r = 0; r < t.rows; r += 1) {
14870
- const tr = document.createElement("tr");
14871
- for (let c = 0; c < t.columns; c += 1) {
14872
- if (omit.has(`${r}:${c}`)) continue;
14873
- const cell = t.cells[r]?.[c] ?? null;
14874
- const td = document.createElement("td");
14875
- if (cell === null) {
14876
- Object.assign(td.style, { padding: "0", border: "none" });
14877
- tr.appendChild(td);
14878
- continue;
14879
- }
14880
- const sp = span.get(`${r}:${c}`);
14881
- if (sp) {
14882
- if (sp.cs > 1) td.colSpan = sp.cs;
14883
- if (sp.rs > 1) td.rowSpan = sp.rs;
14884
- }
14885
- Object.assign(td.style, {
14886
- border: cellBorder,
14887
- padding: "2px 6px",
14888
- background: cell.bgColor ?? "transparent",
14889
- color: cell.textColor ?? this.theme.textColor,
14890
- textAlign: cell.hAlign,
14891
- verticalAlign: cell.vAlign === "top" ? "top" : cell.vAlign === "bottom" ? "bottom" : "middle",
14892
- fontSize: `${fontPxOf(cell.textSize)}px`,
14893
- fontFamily: cell.fontFamily === "monospace" ? "monospace" : "inherit",
14894
- fontWeight: cell.bold ? "bold" : "normal",
14895
- fontStyle: cell.italic ? "italic" : "normal",
14896
- // Pine cell text never wraps; `\n` still breaks lines. Wrapping
14897
- // used to collapse unicode sparklines and ━━━ dividers.
14898
- whiteSpace: "pre"
14899
- });
14900
- if (cell.width) td.style.width = `${cell.width / 100 * plotW}px`;
14901
- if (cell.height) td.style.height = `${cell.height / 100 * paneH}px`;
14902
- if (cell.tooltip) td.title = cell.tooltip;
14903
- td.textContent = cell.text ?? "";
14904
- tr.appendChild(td);
14905
- }
14906
- table.appendChild(tr);
14907
- }
14908
- wrap.appendChild(table);
14909
- return wrap;
14910
- }
14911
- /**
14912
- * Position the wrapper at a Pine `position.*` corner/edge of the table's PANE
14913
- * (not the whole chart), inset past the right price axis so it never overlaps
14914
- * the Y-axis labels.
14915
- */
14916
- anchor(el, position, b) {
14917
- const m = 6;
14918
- const containerH = this.root.clientHeight || this.container.clientHeight;
14919
- const paneBottom = b.top + b.height;
14920
- if (position.startsWith("top")) el.style.top = `${b.top + m}px`;
14921
- else if (position.startsWith("bottom")) el.style.bottom = `${Math.max(0, containerH - paneBottom) + m}px`;
14922
- else el.style.top = `${b.top + b.height / 2}px`;
14923
- if (position.endsWith("left")) el.style.left = `${m}px`;
14924
- else if (position.endsWith("right")) el.style.right = `${b.rightAxis + m}px`;
14925
- else el.style.left = `calc(50% - ${b.rightAxis / 2}px)`;
14926
- const tx = position.endsWith("center") ? "-50%" : "0";
14927
- const ty = position.startsWith("middle") ? "-50%" : "0";
14928
- if (tx !== "0" || ty !== "0") el.style.transform = `translate(${tx}, ${ty})`;
14929
- }
14930
- };
14931
-
14932
14953
  // src/renderers/native/capabilities.ts
14933
14954
  var NATIVE_CAPABILITIES = {
14934
14955
  panes: true,
@@ -14949,7 +14970,7 @@ var NATIVE_CAPABILITIES = {
14949
14970
  drawingDepth: true,
14950
14971
  // drawings share the series' z space (backend-composited interleave layers)
14951
14972
  tables: true,
14952
- // reuses the DOM TableOverlay
14973
+ // canvas-painted into the owning indicator's interleave slice
14953
14974
  trades: true,
14954
14975
  // strategy order-fill markers (arrows + labels + fill-price ticks)
14955
14976
  inputsUI: true
@@ -14980,6 +15001,9 @@ function candleTier(spacing) {
14980
15001
  if (spacing < CANDLE_BODY_MIN_SPACING) return "wick";
14981
15002
  return "full";
14982
15003
  }
15004
+ function snapY(yCss, dpr) {
15005
+ return Math.round(yCss * dpr) / dpr;
15006
+ }
14983
15007
  function candleGeometry(xCss, spacing, dpr, bodyScale = 1) {
14984
15008
  const wickDev = Math.max(1, Math.round(wickWidth(spacing) * dpr));
14985
15009
  const wickLeftDev = Math.round(xCss * dpr - wickDev / 2);
@@ -15786,11 +15810,9 @@ var WebGL2Backend = class {
15786
15810
  };
15787
15811
  b.alpha = this.modelAlpha;
15788
15812
  for (const m of models) for (const bgSpan of m.backgrounds) if (bgSpan.overlay !== true) this.emitBackground(b, bgSpan, pane, coords);
15789
- 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));
15790
15813
  if (isPrice) {
15791
15814
  for (const m of scene.indicators.values()) {
15792
15815
  for (const bgSpan of m.backgrounds) if (bgSpan.overlay === true) this.emitBackground(b, bgSpan, pane, coords);
15793
- for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, i1, scene.offsetOf(m.id));
15794
15816
  }
15795
15817
  }
15796
15818
  const drawCandles = isPrice && !scene.candlesHidden;
@@ -15806,6 +15828,7 @@ var WebGL2Backend = class {
15806
15828
  b.alpha = this.modelAlpha;
15807
15829
  const off = scene.offsetOf(m.id);
15808
15830
  const mp = effPane(m);
15831
+ for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, mp, coords, i0, i1, off);
15809
15832
  for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
15810
15833
  }
15811
15834
  if (drawCandles && !candleDrawn) {
@@ -15813,14 +15836,18 @@ var WebGL2Backend = class {
15813
15836
  b.alpha = this.candleStructureAlpha;
15814
15837
  this.emitPriceSeries(b, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
15815
15838
  }
15816
- drawSlicesUpTo(Infinity);
15817
15839
  b.alpha = this.modelAlpha;
15818
15840
  if (isPrice) {
15841
+ for (const m of scene.indicators.values()) {
15842
+ const off = scene.offsetOf(m.id);
15843
+ for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, i1, off);
15844
+ }
15819
15845
  for (const m of scene.indicators.values()) {
15820
15846
  const off = scene.offsetOf(m.id);
15821
15847
  for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0, i1, theme, off);
15822
15848
  }
15823
15849
  }
15850
+ drawSlicesUpTo(Infinity);
15824
15851
  for (const m of models) {
15825
15852
  const mp = effPane(m);
15826
15853
  for (const pl of m.priceLines) this.emitHline(b, pl, mp, coords, dataW, theme);
@@ -16215,12 +16242,12 @@ var WebGL2Backend = class {
16215
16242
  if (drawBody) {
16216
16243
  const oY = coords.priceToY(bar.open, pane.scale, pane.bounds);
16217
16244
  const cY = coords.priceToY(bar.close, pane.scale, pane.bounds);
16218
- bodyTop = Math.min(oY, cY);
16219
- bodyH = Math.max(1, Math.abs(cY - oY));
16245
+ bodyTop = snapY(Math.min(oY, cY), coords.dpr);
16246
+ bodyH = Math.max(1 / coords.dpr, snapY(Math.max(oY, cY), coords.dpr) - bodyTop);
16220
16247
  }
16221
16248
  if (cs.wickVisible) {
16222
- const hY = coords.priceToY(bar.high, pane.scale, pane.bounds);
16223
- const lY = coords.priceToY(bar.low, pane.scale, pane.bounds);
16249
+ const hY = snapY(coords.priceToY(bar.high, pane.scale, pane.bounds), coords.dpr);
16250
+ const lY = snapY(coords.priceToY(bar.low, pane.scale, pane.bounds), coords.dpr);
16224
16251
  b.alpha = this.candleStructureAlpha;
16225
16252
  const wCol = parseColor((isUp ? cs.wickUpColor : cs.wickDownColor) ?? (drawBody ? dir : bodyColorStr));
16226
16253
  if (drawBody) {
@@ -16777,6 +16804,7 @@ var DOUBLE_TAP_MS = 350;
16777
16804
  var DOUBLE_TAP_SLOP = 30;
16778
16805
  var TIME_SCALE_K = 4e-3;
16779
16806
  var WHEEL_ZOOM_K = 4e-3;
16807
+ var WHEEL_PRICE_DRAG_PX = 0.25;
16780
16808
  function wheelZoomAnchor(coords, cursorX, rightEdge) {
16781
16809
  if (rightEdge) return { logical: coords.rightEdgeLogical, x: coords.width };
16782
16810
  return { logical: coords.xToLogical(cursorX), x: cursorX };
@@ -16907,7 +16935,7 @@ var InputController = class {
16907
16935
  this.capture(e.pointerId);
16908
16936
  return;
16909
16937
  }
16910
- if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y)) {
16938
+ if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y, this.snapMode(e))) {
16911
16939
  this.region = "drawing";
16912
16940
  this.capture(e.pointerId);
16913
16941
  return;
@@ -17021,7 +17049,7 @@ var InputController = class {
17021
17049
  const wasTouch = e.pointerType === "touch";
17022
17050
  const tapRelease = this.dragging && !this.moved && (!wasTouch || Math.hypot(x - this.startX, y - this.startY) <= TOUCH_TAP_SLOP);
17023
17051
  if (this.dragging && this.region === "drawing") {
17024
- this.deps.drawingsPointerUp?.(x, y);
17052
+ this.deps.drawingsPointerUp?.(x, y, this.snapMode(e));
17025
17053
  } else if (tapRelease && this.region === "data") {
17026
17054
  this.deps.onClick(x, y);
17027
17055
  } else if (this.dragging && this.region === "data") {
@@ -17042,7 +17070,7 @@ var InputController = class {
17042
17070
  if (e.pointerType === "touch") this.touches.delete(e.pointerId);
17043
17071
  this.cancelLongPress();
17044
17072
  if (!this.dragging) return;
17045
- if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY);
17073
+ if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY, this.snapMode(e));
17046
17074
  if (this.region === "crosshair" || e.pointerType === "touch") this.deps.onPointerMove(null, null);
17047
17075
  this.endGesture(e);
17048
17076
  };
@@ -17058,6 +17086,12 @@ var InputController = class {
17058
17086
  this.onWheel = (e) => {
17059
17087
  e.preventDefault();
17060
17088
  this.deps.drawingsClearTransient?.();
17089
+ const { x, y } = this.local(e);
17090
+ if (this.regionAt(x, y) === "price" && e.deltaY !== 0) {
17091
+ this.deps.beginPriceScale(x, y);
17092
+ this.deps.priceScaleBy(e.deltaY * WHEEL_PRICE_DRAG_PX);
17093
+ return;
17094
+ }
17061
17095
  const coords = this.deps.getCoords();
17062
17096
  const vp = coords.getViewport();
17063
17097
  const pan = wheelPanDelta(e.deltaX, e.deltaY, e.shiftKey);
@@ -17065,9 +17099,8 @@ var InputController = class {
17065
17099
  this.deps.apply({ barSpacing: vp.barSpacing, rightOffset: wheelPanRightOffset(vp.rightOffset, pan, coords.pxPerBar()) });
17066
17100
  return;
17067
17101
  }
17068
- const cursorX = this.local(e).x;
17069
17102
  const rightEdge = this.rightEdgeZoom && !(e.ctrlKey || e.metaKey);
17070
- const anchor = wheelZoomAnchor(coords, cursorX, rightEdge);
17103
+ const anchor = wheelZoomAnchor(coords, x, rightEdge);
17071
17104
  const target = clampBarSpacing(vp.barSpacing * Math.exp(-e.deltaY * WHEEL_ZOOM_K));
17072
17105
  this.deps.zoomTo(target, anchor.logical, anchor.x);
17073
17106
  };
@@ -17553,9 +17586,10 @@ var SceneGraph = class {
17553
17586
  * so each indicator arrives behind the candles (and behind older indicators);
17554
17587
  * `setIndicatorZ`/`bringToFront`/`sendToBack` change it. */
17555
17588
  this.seriesZ = /* @__PURE__ */ new Map();
17556
- /** Per-pane raster layers of user drawings interleaved into the series stack each is a
17589
+ /** Per-pane raster layers of drawings interleaved into the series stack (each
17590
+ * indicator's Pine drawings at its model's z, plus in-stack user drawings) — each a
17557
17591
  * prepainted canvas the backend composites just before the series carrying `beforeZ`.
17558
- * Rebuilt by the renderer per data frame; empty when every drawing sits over the stack. */
17592
+ * Rebuilt by the renderer per data frame. */
17559
17593
  this.drawingSlices = /* @__PURE__ */ new Map();
17560
17594
  /** Per-model index offset: the chart bar index of the model's `anchorTime` — its
17561
17595
  * index-aligned payloads (dense series arrays, `bar_index` drawings) count from that
@@ -17790,11 +17824,9 @@ var Canvas2dBackend = class {
17790
17824
  };
17791
17825
  ctx.globalAlpha = this.modelAlpha;
17792
17826
  for (const m of models) for (const bg of m.backgrounds) if (bg.overlay !== true) this.drawBackground(ctx, bg, pane, coords);
17793
- 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));
17794
17827
  if (isPrice) {
17795
17828
  for (const m of scene.indicators.values()) {
17796
17829
  for (const bg of m.backgrounds) if (bg.overlay === true) this.drawBackground(ctx, bg, pane, coords);
17797
- for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, scene.offsetOf(m.id));
17798
17830
  }
17799
17831
  }
17800
17832
  const slices = scene.drawingSlices.get(pane.id) ?? [];
@@ -17818,6 +17850,7 @@ var Canvas2dBackend = class {
17818
17850
  ctx.globalAlpha = this.modelAlpha;
17819
17851
  const off = scene.offsetOf(m.id);
17820
17852
  const mp = effPane(m);
17853
+ for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, mp, coords, i0, i1, off);
17821
17854
  for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
17822
17855
  }
17823
17856
  if (drawCandles && !candleDrawn) {
@@ -17825,14 +17858,18 @@ var Canvas2dBackend = class {
17825
17858
  ctx.globalAlpha = this.candleStructureAlpha;
17826
17859
  this.drawPriceSeries(ctx, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
17827
17860
  }
17828
- drawSlicesUpTo(Infinity);
17829
17861
  if (isPrice) {
17830
17862
  ctx.globalAlpha = this.modelAlpha;
17863
+ for (const m of scene.indicators.values()) {
17864
+ const off = scene.offsetOf(m.id);
17865
+ for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, off);
17866
+ }
17831
17867
  for (const m of scene.indicators.values()) {
17832
17868
  const off = scene.offsetOf(m.id);
17833
17869
  for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0, i1, theme, off);
17834
17870
  }
17835
17871
  }
17872
+ drawSlicesUpTo(Infinity);
17836
17873
  ctx.globalAlpha = this.modelAlpha;
17837
17874
  for (const m of models) {
17838
17875
  const mp = effPane(m);
@@ -18076,13 +18113,13 @@ var Canvas2dBackend = class {
18076
18113
  if (drawBody) {
18077
18114
  const oY = coords.priceToY(b.open, pane.scale, pane.bounds);
18078
18115
  const cY = coords.priceToY(b.close, pane.scale, pane.bounds);
18079
- top = Math.min(oY, cY);
18080
- bodyH = Math.max(1, Math.abs(cY - oY));
18116
+ top = snapY(Math.min(oY, cY), coords.dpr);
18117
+ bodyH = Math.max(1 / coords.dpr, snapY(Math.max(oY, cY), coords.dpr) - top);
18081
18118
  }
18082
18119
  if (cs.wickVisible) {
18083
18120
  const wick = (up ? cs.wickUpColor : cs.wickDownColor) ?? (drawBody ? dir : color);
18084
- const hY = coords.priceToY(b.high, pane.scale, pane.bounds);
18085
- const lY = coords.priceToY(b.low, pane.scale, pane.bounds);
18121
+ const hY = snapY(coords.priceToY(b.high, pane.scale, pane.bounds), coords.dpr);
18122
+ const lY = snapY(coords.priceToY(b.low, pane.scale, pane.bounds), coords.dpr);
18086
18123
  ctx.globalAlpha = this.candleStructureAlpha;
18087
18124
  ctx.strokeStyle = wick;
18088
18125
  ctx.lineWidth = g.wickW;
@@ -18529,9 +18566,31 @@ function autoFontSize(lines, boxW, boxH, bold) {
18529
18566
 
18530
18567
  // src/renderers/shared/DrawingSceneRenderer.ts
18531
18568
  var EMPTY_DRAWING_SET = { lines: [], boxes: [], labels: [], polylines: [], linefills: [] };
18569
+ function modelDrawingSet(m, overlay) {
18570
+ const want = (d) => Boolean(d.overlay) === overlay;
18571
+ return {
18572
+ lines: (m.lines ?? []).filter(want),
18573
+ boxes: (m.boxes ?? []).filter(want),
18574
+ labels: (m.labels ?? []).filter(want),
18575
+ polylines: (m.polylines ?? []).filter(want),
18576
+ linefills: (m.linefills ?? []).filter(want)
18577
+ };
18578
+ }
18579
+ function drawingSetEmpty(s) {
18580
+ return !s.lines.length && !s.boxes.length && !s.labels.length && !s.polylines.length && !s.linefills.length;
18581
+ }
18532
18582
  function fontSizePx(size) {
18533
18583
  return size === "auto" ? 12 : namedFontSize(size);
18534
18584
  }
18585
+ function lineCoversWindow(a, b, extend, lo, hi) {
18586
+ const minX = Math.min(a, b);
18587
+ const maxX = Math.max(a, b);
18588
+ if (a === b) return a >= lo && a <= hi;
18589
+ if (extend === "both") return true;
18590
+ if (extend === "left") return maxX >= lo;
18591
+ if (extend === "right") return minX <= hi;
18592
+ return maxX >= lo && minX <= hi;
18593
+ }
18535
18594
  var DrawingSceneRenderer = class {
18536
18595
  constructor(deps, set = EMPTY_DRAWING_SET) {
18537
18596
  this.deps = deps;
@@ -18596,7 +18655,7 @@ var DrawingSceneRenderer = class {
18596
18655
  };
18597
18656
  for (const ln of this.set.lines) {
18598
18657
  if (ln.invisible) continue;
18599
- if (!visible(this.logicalOf(ln.xloc, ln.x1), this.logicalOf(ln.xloc, ln.x2), ln.extend)) continue;
18658
+ if (!lineCoversWindow(this.logicalOf(ln.xloc, ln.x1), this.logicalOf(ln.xloc, ln.x2), ln.extend, lo, hi)) continue;
18600
18659
  fold(ln.y1);
18601
18660
  fold(ln.y2);
18602
18661
  }
@@ -19381,10 +19440,8 @@ var ChromeRenderer = class {
19381
19440
  this.ctx = null;
19382
19441
  // The color for axis tick labels — the host-passed surface text, set each frame in render().
19383
19442
  this.axisTextColor = DARK_THEME.textColor;
19384
- // Shared Pine-drawing renderer (line/box/label/polyline/linefill); widthCache persists.
19443
+ // Shared Pine-drawing renderer, used here for autoscale geometry only; widthCache persists.
19385
19444
  this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
19386
- // Tooltip hit-rects of every label drawn this frame, in plot coords (rebuilt per render).
19387
- this.labelTips = [];
19388
19445
  }
19389
19446
  mount(canvas) {
19390
19447
  this.canvas = canvas;
@@ -19408,8 +19465,8 @@ var ChromeRenderer = class {
19408
19465
  */
19409
19466
  paneDrawingsRange(ownModels, scene, isPricePane, vr) {
19410
19467
  let dr = null;
19411
- for (const m of ownModels) dr = unionRange(dr, this.drawingsRange(this.ownDrawings(m), vr, scene.offsetOf(m.id)));
19412
- if (isPricePane) for (const m of scene.indicators.values()) dr = unionRange(dr, this.drawingsRange(this.overlayDrawings(m), vr, scene.offsetOf(m.id)));
19468
+ for (const m of ownModels) dr = unionRange(dr, this.drawingsRange(modelDrawingSet(m, false), vr, scene.offsetOf(m.id)));
19469
+ if (isPricePane) for (const m of scene.indicators.values()) dr = unionRange(dr, this.drawingsRange(modelDrawingSet(m, true), vr, scene.offsetOf(m.id)));
19413
19470
  return dr;
19414
19471
  }
19415
19472
  /** Clear the chrome canvas and draw drawings + axes + current-price line.
@@ -19426,7 +19483,6 @@ var ChromeRenderer = class {
19426
19483
  const dataW = coords.width;
19427
19484
  const dataH = coords.height;
19428
19485
  this.axisTextColor = surface?.textColor ?? theme.textColor;
19429
- this.labelTips = [];
19430
19486
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
19431
19487
  ctx.clearRect(0, 0, fullW, fullH);
19432
19488
  if (surface && (fullW > dataW || fullH > dataH)) {
@@ -19442,17 +19498,6 @@ var ChromeRenderer = class {
19442
19498
  return;
19443
19499
  }
19444
19500
  const pricePane = panes.find((p) => p.kind === "price") ?? null;
19445
- for (const pane of panes) {
19446
- if (pane.collapsed) continue;
19447
- for (const m of scene.indicatorsForPane(pane.id)) {
19448
- const sc = scene.scaleFor(m, pane);
19449
- const mp = sc === pane.scale ? pane : { ...pane, scale: sc };
19450
- this.renderDrawings(ctx, coords, this.ownDrawings(m), mp, dataW, scene.offsetOf(m.id));
19451
- }
19452
- }
19453
- if (pricePane) {
19454
- for (const m of scene.indicators.values()) this.renderDrawings(ctx, coords, this.overlayDrawings(m), pricePane, dataW, scene.offsetOf(m.id));
19455
- }
19456
19501
  if (pricePane && !pricePane.collapsed && scene.tradeMarkers.visible) {
19457
19502
  for (const m of scene.indicators.values()) {
19458
19503
  if (m.trades?.length) this.renderTrades(ctx, coords, scene, theme, m.trades, pricePane, dataW);
@@ -19468,25 +19513,6 @@ var ChromeRenderer = class {
19468
19513
  this.canvas = null;
19469
19514
  this.ctx = null;
19470
19515
  }
19471
- // ── Pine-drawing helpers (own vs force_overlay routing) ──
19472
- ownDrawings(m) {
19473
- return {
19474
- lines: (m.lines ?? []).filter((d) => !d.overlay),
19475
- boxes: (m.boxes ?? []).filter((d) => !d.overlay),
19476
- labels: (m.labels ?? []).filter((d) => !d.overlay),
19477
- polylines: (m.polylines ?? []).filter((d) => !d.overlay),
19478
- linefills: (m.linefills ?? []).filter((d) => !d.overlay)
19479
- };
19480
- }
19481
- overlayDrawings(m) {
19482
- return {
19483
- lines: (m.lines ?? []).filter((d) => d.overlay),
19484
- boxes: (m.boxes ?? []).filter((d) => d.overlay),
19485
- labels: (m.labels ?? []).filter((d) => d.overlay),
19486
- polylines: (m.polylines ?? []).filter((d) => d.overlay),
19487
- linefills: (m.linefills ?? []).filter((d) => d.overlay)
19488
- };
19489
- }
19490
19516
  drawingsRange(set, vr, indexOffset = 0) {
19491
19517
  this.drawScene.setSet(set, indexOffset);
19492
19518
  if (this.drawScene.isEmpty()) return null;
@@ -19520,34 +19546,6 @@ var ChromeRenderer = class {
19520
19546
  );
19521
19547
  ctx.restore();
19522
19548
  }
19523
- renderDrawings(ctx, coords, set, pane, dataW, indexOffset = 0) {
19524
- this.drawScene.setSet(set, indexOffset);
19525
- if (this.drawScene.isEmpty()) return;
19526
- ctx.save();
19527
- ctx.translate(0, pane.bounds.top);
19528
- ctx.beginPath();
19529
- ctx.rect(0, 0, dataW, pane.bounds.height);
19530
- ctx.clip();
19531
- this.drawScene.render(
19532
- ctx,
19533
- dataW,
19534
- pane.bounds.height,
19535
- (l) => coords.logicalToX(l),
19536
- (price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
19537
- );
19538
- ctx.restore();
19539
- for (const r of this.drawScene.labelTipRegions()) {
19540
- this.labelTips.push({ ...r, top: r.top + pane.bounds.top, bottom: r.bottom + pane.bounds.top });
19541
- }
19542
- }
19543
- /** Tooltip of the topmost label under a plot-space point, or null. Fed by the last render. */
19544
- labelTooltipAt(x, y) {
19545
- for (let i = this.labelTips.length - 1; i >= 0; i -= 1) {
19546
- const r = this.labelTips[i];
19547
- if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return r.text;
19548
- }
19549
- return null;
19550
- }
19551
19549
  // ── axes ──
19552
19550
  drawPriceAxes(ctx, scene, coords, theme, dataW, panes) {
19553
19551
  ctx.strokeStyle = scene.style.borderColor ?? theme.borderColor;
@@ -22597,6 +22595,22 @@ var DrawingInteraction = class {
22597
22595
  this.snapAt = changed ? { point: snapped, paneId } : null;
22598
22596
  return snapped;
22599
22597
  }
22598
+ /**
22599
+ * Resolve a cursor pixel through the magnet and return the snapped pixel — the same
22600
+ * conversion drawing placement uses. Updates the snap-ring marker. The measure
22601
+ * ruler goes through this so its endpoints follow weak/strong/Ctrl magnet too.
22602
+ */
22603
+ snapCursor(x, y, mode) {
22604
+ const proj = this.deps.projector();
22605
+ const paneId = proj.paneIdAtY(y) ?? "price";
22606
+ const point = this.resolve(x, y, paneId, mode);
22607
+ const sy = proj.yOf(point.price, paneId);
22608
+ return { x: proj.xOf(point.time), y: sy ?? y };
22609
+ }
22610
+ /** Drop the snap-ring marker (a transient mode ended without going through `up`). */
22611
+ clearSnapMarker() {
22612
+ this.snapAt = null;
22613
+ }
22600
22614
  /** Resolve a pixel to a data point with the segment angle locked to 45° steps around
22601
22615
  * `pivot` (Shift held on a line tool). Works in PIXEL space — the user reasons about
22602
22616
  * the angle they see, not about time/price units. The magnet is bypassed: snapping
@@ -23956,17 +23970,17 @@ function glyphIcon(glyph) {
23956
23970
  return textGlyph(String(glyph), 15);
23957
23971
  }
23958
23972
  function stampSizeIcon(size) {
23959
- return textGlyph("\u25CF", (SIZE_PX4[String(size)] ?? 13) + 4);
23973
+ return textGlyph("\u25CF", (SIZE_PX3[String(size)] ?? 13) + 4);
23960
23974
  }
23961
23975
  function sizeIcon(size) {
23962
23976
  return textGlyph(String(size).charAt(0).toUpperCase(), 15);
23963
23977
  }
23964
- var SIZE_PX4 = { small: 10, normal: 13, large: 16, huge: 20 };
23978
+ var SIZE_PX3 = { small: 10, normal: 13, large: 16, huge: 20 };
23965
23979
  function numbersSizeIcon(size) {
23966
- return textGlyph("12", (SIZE_PX4[String(size)] ?? 13) - 1, 16.5, 'font-weight="600"');
23980
+ return textGlyph("12", (SIZE_PX3[String(size)] ?? 13) - 1, 16.5, 'font-weight="600"');
23967
23981
  }
23968
23982
  function labelSizeIcon(size) {
23969
- return textGlyph("T", (SIZE_PX4[String(size)] ?? 13) + 2);
23983
+ return textGlyph("T", (SIZE_PX3[String(size)] ?? 13) + 2);
23970
23984
  }
23971
23985
  function capitalize(s) {
23972
23986
  return s.charAt(0).toUpperCase() + s.slice(1);
@@ -24130,7 +24144,7 @@ var DrawingToolbar = class {
24130
24144
  this.root.replaceChildren();
24131
24145
  this.groupCells.clear();
24132
24146
  this.groupIcons.clear();
24133
- this.cursorBtn = this.makeButton(CURSOR_ICON, "Cursor", () => this.onArm(null));
24147
+ this.cursorBtn = this.makeButton(CURSOR_ICON, "Cursor", () => this.onCursorClick());
24134
24148
  this.root.appendChild(this.cursorBtn);
24135
24149
  if (this.def.groups.length > 0) this.root.appendChild(this.divider());
24136
24150
  for (const g of this.def.groups) {
@@ -24234,6 +24248,14 @@ var DrawingToolbar = class {
24234
24248
  this.magnetIcon = icon2;
24235
24249
  return cell;
24236
24250
  }
24251
+ /** Cursor returns to select/idle: an active measure/eraser mode exits through its own
24252
+ * toggle callback (disarming a tool via `onArm(null)` alone can't — the host treats a
24253
+ * null arm as a no-op side effect of entering those modes), then the tool disarms. */
24254
+ onCursorClick() {
24255
+ if (this.measureActive) this.onMeasure();
24256
+ if (this.eraserActive) this.onEraser();
24257
+ this.onArm(null);
24258
+ }
24237
24259
  /** Clicking the icon arms the group's last-used tool (it does NOT open the flyout). */
24238
24260
  onGroupIconClick(group) {
24239
24261
  const type = this.lastUsed.get(group.id) ?? group.tools[0]?.type;
@@ -24626,30 +24648,39 @@ var MeasureOverlay = class {
24626
24648
  isFinished() {
24627
24649
  return this.state === "finished";
24628
24650
  }
24629
- /** A press: begin the measurement, or finish it on the second click. */
24630
- down(x, y) {
24651
+ /** A press: begin the measurement, or finish it on the second click.
24652
+ * `x,y` are the raw cursor (drag-slop vs click-move-click). `gx,gy` are the
24653
+ * graphic endpoints — magnet-snapped when the magnet is on, else the same as `x,y`. */
24654
+ down(x, y, gx = x, gy = y) {
24631
24655
  if (this.state === "measuring") {
24632
- this.end = { x, y };
24656
+ this.end = { x: gx, y: gy };
24633
24657
  this.state = "finished";
24634
24658
  return;
24635
24659
  }
24636
- this.start = { x, y };
24637
- this.end = { x, y };
24660
+ this.start = { x: gx, y: gy };
24661
+ this.end = { x: gx, y: gy };
24638
24662
  this.pressX = x;
24639
24663
  this.pressY = y;
24640
24664
  this.state = "measuring";
24641
24665
  }
24666
+ /** Size the in-progress ruler. `x,y` are the graphic (magnet-snapped) cursor. */
24642
24667
  move(x, y) {
24643
24668
  if (this.state === "measuring") this.end = { x, y };
24644
24669
  }
24645
24670
  /** A release: finish if the press was actually dragged (press-drag-release), else wait
24646
- * for the second click (click-move-click). */
24647
- up(x, y) {
24671
+ * for the second click (click-move-click). `x,y` are the raw cursor (slop); `gx,gy`
24672
+ * are the graphic end (magnet-snapped when the magnet is on). */
24673
+ up(x, y, gx = x, gy = y) {
24648
24674
  if (this.state === "measuring" && Math.hypot(x - this.pressX, y - this.pressY) > DRAG_SLOP3) {
24649
- this.end = { x, y };
24675
+ this.end = { x: gx, y: gy };
24650
24676
  this.state = "finished";
24651
24677
  }
24652
24678
  }
24679
+ /** Current graphic endpoints in media pixels, or null when idle. */
24680
+ points() {
24681
+ if (!this.start || !this.end) return null;
24682
+ return { start: this.start, end: this.end };
24683
+ }
24653
24684
  clear() {
24654
24685
  this.state = "idle";
24655
24686
  this.start = null;
@@ -25026,11 +25057,13 @@ var UserDrawingController = class {
25026
25057
  }
25027
25058
  /** Shift+press on the empty plot: arm the measure ruler AND start it at (x, y) in one
25028
25059
  * gesture — the equivalent of clicking the toolbar's Measure button, then pressing.
25029
- * Returns false when a mode/tool is already active (the normal press path owns it). */
25030
- beginMeasureAt(x, y) {
25060
+ * `snap` is the effective magnet (sticky mode, or Ctrl/Cmd-forced strong). Returns
25061
+ * false when a mode/tool is already active (the normal press path owns it). */
25062
+ beginMeasureAt(x, y, snap = "off") {
25031
25063
  if (this.measureMode || this.eraserMode || this.activeTool != null) return false;
25032
25064
  this.withModeIntent(() => this.toggleMeasure());
25033
- this.measure.down(x, y);
25065
+ const g = this.interaction.snapCursor(x, y, snap);
25066
+ this.measure.down(x, y, g.x, g.y);
25034
25067
  this.render();
25035
25068
  return true;
25036
25069
  }
@@ -25052,7 +25085,8 @@ var UserDrawingController = class {
25052
25085
  return;
25053
25086
  }
25054
25087
  if (this.measureMode) {
25055
- this.measure.down(x, y);
25088
+ const g = this.interaction.snapCursor(x, y, snap);
25089
+ this.measure.down(x, y, g.x, g.y);
25056
25090
  if (this.measure.isFinished()) this.withModeIntent(() => this.exitMeasure(false));
25057
25091
  this.render();
25058
25092
  return;
@@ -25065,7 +25099,8 @@ var UserDrawingController = class {
25065
25099
  return;
25066
25100
  }
25067
25101
  if (this.measureMode) {
25068
- this.measure.move(x, y);
25102
+ const g = this.interaction.snapCursor(x, y, snap);
25103
+ this.measure.move(g.x, g.y);
25069
25104
  this.render();
25070
25105
  return;
25071
25106
  }
@@ -25083,13 +25118,14 @@ var UserDrawingController = class {
25083
25118
  this.render();
25084
25119
  }
25085
25120
  }
25086
- pointerUp(x, y) {
25121
+ pointerUp(x, y, snap = "off") {
25087
25122
  if (this.eraserMode) {
25088
25123
  this.erasing = false;
25089
25124
  return;
25090
25125
  }
25091
25126
  if (this.measureMode) {
25092
- this.measure.up(x, y);
25127
+ const g = this.interaction.snapCursor(x, y, snap);
25128
+ this.measure.up(x, y, g.x, g.y);
25093
25129
  if (this.measure.isFinished()) this.withModeIntent(() => this.exitMeasure(false));
25094
25130
  this.render();
25095
25131
  return;
@@ -25134,6 +25170,7 @@ var UserDrawingController = class {
25134
25170
  /** Leave ruler mode. `clearGraphic` keeps a just-finished measurement on screen (false). */
25135
25171
  exitMeasure(clearGraphic = true) {
25136
25172
  this.measureMode = false;
25173
+ this.interaction.clearSnapMarker();
25137
25174
  if (clearGraphic) this.measure.clear();
25138
25175
  this.toolbar.setMeasureActive(false);
25139
25176
  this.render();
@@ -25274,15 +25311,28 @@ var UserDrawingController = class {
25274
25311
  if (this.eraserMode) return "pointer";
25275
25312
  return this.interaction.cursorAt(x, y);
25276
25313
  }
25277
- /** Right-click while placing: cancel the in-progress drawing and revert to the
25278
- * pointer the gesture is an explicit escape, so it disarms even in
25279
- * stay-in-drawing-mode (where Escape would leave the tool armed). Returns whether
25280
- * the press was consumed; false lets the host's context menu open normally. */
25314
+ /** Right-click: an explicit escape back to the pointer. Cancels an in-progress
25315
+ * placement or measurement, and also plain-disarms an armed-but-idle drawing
25316
+ * tool or the eraser — so a right-click ALWAYS reverts to the pointer, even in
25317
+ * stay-in-drawing-mode (where Escape would leave a drawing tool armed).
25318
+ * Persistent toggles (magnet, stay-mode, favorites) are untouched. Returns
25319
+ * whether the press was consumed; false lets the host's context menu open
25320
+ * normally. */
25281
25321
  cancelPlacement() {
25282
- if (!this.interaction.isPlacing()) return false;
25283
- this.interaction.cancel();
25284
- if (this.activeTool != null) this.emit({ kind: "arm", type: null });
25285
- return true;
25322
+ if (this.measureMode || this.eraserMode) {
25323
+ this.withModeIntent(() => this.measureMode ? this.exitMeasure() : this.exitEraser());
25324
+ return true;
25325
+ }
25326
+ if (this.interaction.isPlacing()) {
25327
+ this.interaction.cancel();
25328
+ if (this.activeTool != null) this.emit({ kind: "arm", type: null });
25329
+ return true;
25330
+ }
25331
+ if (this.activeTool != null) {
25332
+ this.emit({ kind: "arm", type: null });
25333
+ return true;
25334
+ }
25335
+ return false;
25286
25336
  }
25287
25337
  /** Double-click over a drawing → suppress the chart's view reset (single-click already
25288
25338
  * opens settings). Returns true only when a drawing is under the cursor. */
@@ -25308,6 +25358,10 @@ var UserDrawingController = class {
25308
25358
  return true;
25309
25359
  }
25310
25360
  if (this.interaction.cancel()) return true;
25361
+ if (this.measureMode) {
25362
+ this.withModeIntent(() => this.exitMeasure());
25363
+ return true;
25364
+ }
25311
25365
  if (this.selectedIds.size) {
25312
25366
  this.clearSelection();
25313
25367
  return true;
@@ -25565,6 +25619,315 @@ var UserDrawingController = class {
25565
25619
  }
25566
25620
  };
25567
25621
 
25622
+ // src/renderers/shared/TableOverlay.ts
25623
+ var SIZE_PX4 = {
25624
+ auto: 13,
25625
+ tiny: 10,
25626
+ small: 11,
25627
+ normal: 13,
25628
+ large: 16,
25629
+ huge: 20
25630
+ };
25631
+ function fontPxOf(size) {
25632
+ if (typeof size === "number") return size > 0 ? size : SIZE_PX4.auto;
25633
+ return SIZE_PX4[size] ?? SIZE_PX4.auto;
25634
+ }
25635
+ function tableHasContent(t) {
25636
+ return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
25637
+ }
25638
+ function mergeRenderPlan(t) {
25639
+ const span = /* @__PURE__ */ new Map();
25640
+ const omit = /* @__PURE__ */ new Set();
25641
+ for (const m of t.merges) {
25642
+ span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
25643
+ for (let r = m.startRow; r <= m.endRow; r += 1) {
25644
+ for (let c = m.startCol; c <= m.endCol; c += 1) {
25645
+ if (r !== m.startRow || c !== m.startCol) omit.add(`${r}:${c}`);
25646
+ }
25647
+ }
25648
+ }
25649
+ for (let r = 0; r < t.rows; r += 1) {
25650
+ for (let c = 0; c < t.columns; c += 1) {
25651
+ if (t.cells[r]?.[c]?.merged && !span.has(`${r}:${c}`)) omit.add(`${r}:${c}`);
25652
+ }
25653
+ }
25654
+ for (const key of span.keys()) omit.delete(key);
25655
+ return { span, omit };
25656
+ }
25657
+
25658
+ // src/renderers/shared/TableCanvasRenderer.ts
25659
+ var PAD_X = 6;
25660
+ var PAD_Y = 2;
25661
+ var MARGIN = 6;
25662
+ var LINE_HEIGHT = 1.2;
25663
+ function paintTable(ctx, t, args, tips) {
25664
+ if (!tableHasContent(t)) return;
25665
+ const layout = layoutTable(ctx, t, args);
25666
+ if (!layout || layout.w <= 0 || layout.h <= 0) return;
25667
+ const fw = t.frameColor && t.frameWidth > 0 ? t.frameWidth : 0;
25668
+ const { x, y } = anchorOrigin(t.position, layout.w + 2 * fw, layout.h + 2 * fw, args);
25669
+ const x0 = x + fw;
25670
+ const y0 = y + fw;
25671
+ if (t.bgColor) {
25672
+ ctx.fillStyle = t.bgColor;
25673
+ ctx.fillRect(x0, y0, layout.w, layout.h);
25674
+ }
25675
+ if (fw > 0 && t.frameColor) {
25676
+ ctx.strokeStyle = t.frameColor;
25677
+ ctx.lineWidth = fw;
25678
+ ctx.strokeRect(x + fw / 2, y + fw / 2, layout.w + fw, layout.h + fw);
25679
+ }
25680
+ const colX = [0];
25681
+ for (const w of layout.colW) colX.push(colX[colX.length - 1] + w);
25682
+ const rowY = [0];
25683
+ for (const h of layout.rowH) rowY.push(rowY[rowY.length - 1] + h);
25684
+ const prevBaseline = ctx.textBaseline;
25685
+ const prevAlign = ctx.textAlign;
25686
+ ctx.textBaseline = "middle";
25687
+ for (const box of layout.boxes) {
25688
+ const rx = x0 + colX[box.c];
25689
+ const ry = y0 + rowY[box.r];
25690
+ const rw = colX[box.c + box.cs] - colX[box.c];
25691
+ const rh = rowY[box.r + box.rs] - rowY[box.r];
25692
+ const cell = box.cell;
25693
+ if (cell.bgColor) {
25694
+ ctx.fillStyle = cell.bgColor;
25695
+ ctx.fillRect(rx, ry, rw, rh);
25696
+ }
25697
+ const text = cell.text ?? "";
25698
+ if (text.length > 0) {
25699
+ const px = fontPxOf(cell.textSize);
25700
+ ctx.font = cellFont(cell, px, args.theme);
25701
+ ctx.fillStyle = cell.textColor ?? args.theme.textColor;
25702
+ const lines = text.split("\n");
25703
+ const blockH = lines.length * px * LINE_HEIGHT;
25704
+ const blockTop = cell.vAlign === "top" ? ry + PAD_Y : cell.vAlign === "bottom" ? ry + rh - PAD_Y - blockH : ry + (rh - blockH) / 2;
25705
+ const tx = cell.hAlign === "left" ? rx + PAD_X : cell.hAlign === "right" ? rx + rw - PAD_X : rx + rw / 2;
25706
+ ctx.textAlign = cell.hAlign;
25707
+ lines.forEach((line, i) => ctx.fillText(line, tx, blockTop + (i + 0.5) * px * LINE_HEIGHT));
25708
+ }
25709
+ if (cell.tooltip) tips.push({ left: rx, top: ry, right: rx + rw, bottom: ry + rh, text: cell.tooltip });
25710
+ }
25711
+ ctx.textBaseline = prevBaseline;
25712
+ ctx.textAlign = prevAlign;
25713
+ if (t.borderColor && t.borderWidth > 0) {
25714
+ ctx.strokeStyle = t.borderColor;
25715
+ ctx.lineWidth = t.borderWidth;
25716
+ const seen = /* @__PURE__ */ new Set();
25717
+ ctx.beginPath();
25718
+ const edge = (ax, ay, bx, by) => {
25719
+ const key = `${ax},${ay},${bx},${by}`;
25720
+ if (seen.has(key)) return;
25721
+ seen.add(key);
25722
+ ctx.moveTo(ax, ay);
25723
+ ctx.lineTo(bx, by);
25724
+ };
25725
+ for (const box of layout.boxes) {
25726
+ const l = Math.round(x0 + colX[box.c]);
25727
+ const r = Math.round(x0 + colX[box.c + box.cs]);
25728
+ const tp = Math.round(y0 + rowY[box.r]);
25729
+ const bt = Math.round(y0 + rowY[box.r + box.rs]);
25730
+ edge(l, tp, r, tp);
25731
+ edge(l, bt, r, bt);
25732
+ edge(l, tp, l, bt);
25733
+ edge(r, tp, r, bt);
25734
+ }
25735
+ ctx.stroke();
25736
+ }
25737
+ }
25738
+ function layoutTable(ctx, t, args) {
25739
+ const { span, omit } = mergeRenderPlan(t);
25740
+ const colW = new Array(t.columns).fill(0);
25741
+ const rowH = new Array(t.rows).fill(0);
25742
+ const boxes = [];
25743
+ for (let r = 0; r < t.rows; r += 1) {
25744
+ for (let c = 0; c < t.columns; c += 1) {
25745
+ if (omit.has(`${r}:${c}`)) continue;
25746
+ const cell = t.cells[r]?.[c];
25747
+ if (cell == null) continue;
25748
+ const sp = span.get(`${r}:${c}`);
25749
+ boxes.push({ cell, r, c, cs: Math.min(sp?.cs ?? 1, t.columns - c), rs: Math.min(sp?.rs ?? 1, t.rows - r) });
25750
+ }
25751
+ }
25752
+ if (boxes.length === 0) return null;
25753
+ const sizeOf = (cell) => {
25754
+ const px = fontPxOf(cell.textSize);
25755
+ ctx.font = cellFont(cell, px, args.theme);
25756
+ const lines = (cell.text ?? "").split("\n");
25757
+ let maxW = 0;
25758
+ for (const line of lines) maxW = Math.max(maxW, ctx.measureText(line).width);
25759
+ let w2 = Math.ceil(maxW) + 2 * PAD_X;
25760
+ let h2 = Math.ceil(lines.length * px * LINE_HEIGHT) + 2 * PAD_Y;
25761
+ if (cell.width) w2 = Math.max(w2, cell.width / 100 * args.plotWidth);
25762
+ if (cell.height) h2 = Math.max(h2, cell.height / 100 * args.paneHeight);
25763
+ return { w: w2, h: h2 };
25764
+ };
25765
+ const spanning = [];
25766
+ for (const box of boxes) {
25767
+ const { w: w2, h: h2 } = sizeOf(box.cell);
25768
+ if (box.cs === 1) colW[box.c] = Math.max(colW[box.c], w2);
25769
+ if (box.rs === 1) rowH[box.r] = Math.max(rowH[box.r], h2);
25770
+ if (box.cs > 1 || box.rs > 1) spanning.push({ box, w: w2, h: h2 });
25771
+ }
25772
+ for (const { box, w: w2, h: h2 } of spanning) {
25773
+ if (box.cs > 1) {
25774
+ let sum = 0;
25775
+ for (let c = box.c; c < box.c + box.cs; c += 1) sum += colW[c];
25776
+ if (w2 > sum) for (let c = box.c; c < box.c + box.cs; c += 1) colW[c] += (w2 - sum) / box.cs;
25777
+ }
25778
+ if (box.rs > 1) {
25779
+ let sum = 0;
25780
+ for (let r = box.r; r < box.r + box.rs; r += 1) sum += rowH[r];
25781
+ if (h2 > sum) for (let r = box.r; r < box.r + box.rs; r += 1) rowH[r] += (h2 - sum) / box.rs;
25782
+ }
25783
+ }
25784
+ let w = 0;
25785
+ for (const cw of colW) w += cw;
25786
+ let h = 0;
25787
+ for (const rh of rowH) h += rh;
25788
+ return { colW, rowH, w, h, boxes };
25789
+ }
25790
+ function anchorOrigin(position, totalW, totalH, args) {
25791
+ let y;
25792
+ if (position.startsWith("top")) y = MARGIN;
25793
+ else if (position.startsWith("bottom")) y = args.paneHeight - MARGIN - totalH;
25794
+ else y = args.paneHeight / 2 - totalH / 2;
25795
+ let x;
25796
+ if (position.endsWith("left")) x = MARGIN;
25797
+ else if (position.endsWith("right")) x = args.plotWidth - MARGIN - totalW;
25798
+ else x = args.plotWidth / 2 - totalW / 2;
25799
+ return { x, y };
25800
+ }
25801
+ function cellFont(cell, px, theme) {
25802
+ const family = cell.fontFamily === "monospace" ? "monospace" : theme.fontFamily || "sans-serif";
25803
+ return `${cell.italic ? "italic " : ""}${cell.bold ? "bold " : ""}${px}px ${family}`;
25804
+ }
25805
+
25806
+ // src/renderers/native/drawings/IndicatorDrawingSlices.ts
25807
+ function indicatorSliceKey(z, boundaries) {
25808
+ return boundaries.find((b) => b > z) ?? Infinity;
25809
+ }
25810
+ var IndicatorDrawingSlices = class {
25811
+ constructor() {
25812
+ this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
25813
+ /** Slice canvas cache, keyed `paneId|beforeZ` — same lifecycle as the user-drawing cache. */
25814
+ this.sliceCache = /* @__PURE__ */ new Map();
25815
+ /** Tooltip hit-rects of every label drawn this frame, in plot coords (rebuilt per prepare). */
25816
+ this.tips = [];
25817
+ }
25818
+ /**
25819
+ * Rebuild the per-indicator drawing slices for this data frame. `ref` is the data
25820
+ * canvas the slices must match pixel-for-pixel (the backend composites them 1:1).
25821
+ * Runs from the renderer's data paint, just before the backend composites the scene.
25822
+ */
25823
+ prepare(scene, coords, theme, ref) {
25824
+ this.tips = [];
25825
+ const out = /* @__PURE__ */ new Map();
25826
+ if (ref.width === 0 || ref.height === 0) {
25827
+ this.sliceCache.clear();
25828
+ return out;
25829
+ }
25830
+ this.drawScene.setDeps({
25831
+ timeToLogical: (ms) => coords.timeToLogical(ms),
25832
+ barAt: (logical) => {
25833
+ const b = scene.bars[Math.round(logical)];
25834
+ return b ? { high: b.high, low: b.low } : null;
25835
+ },
25836
+ theme
25837
+ });
25838
+ const dpr = coords.dpr;
25839
+ const dataW = coords.width;
25840
+ const buckets = /* @__PURE__ */ new Map();
25841
+ const add = (paneId, beforeZ, entry) => {
25842
+ const key = `${paneId}|${beforeZ}`;
25843
+ const bucket = buckets.get(key);
25844
+ if (bucket) bucket.entries.push(entry);
25845
+ else buckets.set(key, { paneId, beforeZ, entries: [entry] });
25846
+ };
25847
+ for (const pane of scene.orderedPanes()) {
25848
+ if (pane.collapsed) continue;
25849
+ const boundaries = scene.seriesBoundaries(pane.id);
25850
+ for (const m of scene.orderedIndicatorsForPane(pane.id)) {
25851
+ const set = modelDrawingSet(m, false);
25852
+ const tables = (m.tables ?? []).filter((t) => !t.overlay);
25853
+ if (drawingSetEmpty(set) && tables.length === 0) continue;
25854
+ const sc = scene.scaleFor(m, pane);
25855
+ const mp = sc === pane.scale ? pane : { ...pane, scale: sc };
25856
+ const beforeZ = indicatorSliceKey(scene.zOf(m.id), boundaries);
25857
+ add(pane.id, beforeZ, { set, tables, pane: mp, indexOffset: scene.offsetOf(m.id) });
25858
+ }
25859
+ if (pane.kind === "price") {
25860
+ for (const m of scene.indicators.values()) {
25861
+ const set = modelDrawingSet(m, true);
25862
+ const tables = (m.tables ?? []).filter((t) => t.overlay === true);
25863
+ if (drawingSetEmpty(set) && tables.length === 0) continue;
25864
+ add(pane.id, Infinity, { set, tables, pane, indexOffset: scene.offsetOf(m.id) });
25865
+ }
25866
+ }
25867
+ }
25868
+ for (const [key, { paneId, beforeZ, entries }] of buckets) {
25869
+ let canvas = this.sliceCache.get(key);
25870
+ if (!canvas) {
25871
+ canvas = document.createElement("canvas");
25872
+ this.sliceCache.set(key, canvas);
25873
+ }
25874
+ if (canvas.width !== ref.width || canvas.height !== ref.height) {
25875
+ canvas.width = ref.width;
25876
+ canvas.height = ref.height;
25877
+ }
25878
+ const ctx = canvas.getContext("2d");
25879
+ if (!ctx) continue;
25880
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
25881
+ ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
25882
+ for (const e of entries) this.paintEntry(ctx, e, coords, dataW, theme);
25883
+ const slices = out.get(paneId) ?? [];
25884
+ slices.push({ beforeZ, canvas });
25885
+ out.set(paneId, slices);
25886
+ }
25887
+ for (const key of [...this.sliceCache.keys()]) if (!buckets.has(key)) this.sliceCache.delete(key);
25888
+ for (const slices of out.values()) slices.sort((a, b) => a.beforeZ - b.beforeZ);
25889
+ return out;
25890
+ }
25891
+ paintEntry(ctx, e, coords, dataW, theme) {
25892
+ const { pane } = e;
25893
+ const paneTips = [];
25894
+ ctx.save();
25895
+ ctx.translate(0, pane.bounds.top);
25896
+ ctx.beginPath();
25897
+ ctx.rect(0, 0, dataW, pane.bounds.height);
25898
+ ctx.clip();
25899
+ this.drawScene.setSet(e.set, e.indexOffset);
25900
+ this.drawScene.render(
25901
+ ctx,
25902
+ dataW,
25903
+ pane.bounds.height,
25904
+ (l) => coords.logicalToX(l),
25905
+ (price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
25906
+ );
25907
+ paneTips.push(...this.drawScene.labelTipRegions());
25908
+ for (const t of e.tables) paintTable(ctx, t, { paneHeight: pane.bounds.height, plotWidth: dataW, theme }, paneTips);
25909
+ ctx.restore();
25910
+ for (const r of paneTips) {
25911
+ this.tips.push({ ...r, top: r.top + pane.bounds.top, bottom: r.bottom + pane.bounds.top });
25912
+ }
25913
+ }
25914
+ /** Tooltip of the topmost label or table cell under a plot-space point, or null. Fed by the last prepare. */
25915
+ labelTooltipAt(x, y) {
25916
+ for (let i = this.tips.length - 1; i >= 0; i -= 1) {
25917
+ const r = this.tips[i];
25918
+ if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return r.text;
25919
+ }
25920
+ return null;
25921
+ }
25922
+ };
25923
+ function mergeSlices(indicator, user) {
25924
+ const out = /* @__PURE__ */ new Map();
25925
+ for (const [paneId, slices] of indicator) out.set(paneId, [...slices]);
25926
+ for (const [paneId, slices] of user) out.set(paneId, [...out.get(paneId) ?? [], ...slices]);
25927
+ for (const slices of out.values()) slices.sort((a, b) => a.beforeZ - b.beforeZ);
25928
+ return out;
25929
+ }
25930
+
25568
25931
  // src/renderers/native/drawings/Projector.ts
25569
25932
  function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
25570
25933
  return {
@@ -26444,6 +26807,11 @@ var NativeRenderer = class {
26444
26807
  this.vpvrRenderer = new VpvrRenderer();
26445
26808
  this.resizeObserver = null;
26446
26809
  this.dprMedia = null;
26810
+ /** Plot size in INTEGER device px, as last reported by the resize observer's
26811
+ * device-pixel-content-box — the browser's own statement of how many device pixels
26812
+ * it paints the plot into. `null` until the first report or where the box type is
26813
+ * unsupported (WebKit); syncSize then falls back to rounding the client rect. */
26814
+ this.plotDeviceSize = null;
26447
26815
  this.coords = new CoordinateSystem();
26448
26816
  this.scene = new SceneGraph();
26449
26817
  // chosen at mount (WebGL2 if available, else canvas2d)
@@ -26451,6 +26819,8 @@ var NativeRenderer = class {
26451
26819
  this.glowAmount = 0;
26452
26820
  // WebGL2 neon-glow intensity (canvas2d ignores it)
26453
26821
  this.chrome = new ChromeRenderer();
26822
+ /** Prepaints each indicator's Pine drawings into interleave slices at the model's z. */
26823
+ this.indicatorSlices = new IndicatorDrawingSlices();
26454
26824
  /** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
26455
26825
  this.labelTooltip = null;
26456
26826
  this.crosshairLayer = new CrosshairRenderer();
@@ -26599,7 +26969,6 @@ var NativeRenderer = class {
26599
26969
  this.toggleVisibleCbs = /* @__PURE__ */ new Set();
26600
26970
  this.moveIndicatorCbs = /* @__PURE__ */ new Set();
26601
26971
  this.priceStyleCbs = /* @__PURE__ */ new Set();
26602
- this.tableOverlays = /* @__PURE__ */ new Map();
26603
26972
  this.name = "native";
26604
26973
  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"];
26605
26974
  /** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
@@ -26913,7 +27282,6 @@ var NativeRenderer = class {
26913
27282
  * hidden) on clear so a re-show picks up the current theme.
26914
27283
  */
26915
27284
  setLoading(loading) {
26916
- for (const overlay of this.tableOverlays.values()) overlay.setVisible(!loading);
26917
27285
  if (!loading || !this.wrapper) {
26918
27286
  this.loadingEl?.remove();
26919
27287
  this.loadingEl = null;
@@ -27552,13 +27920,13 @@ var NativeRenderer = class {
27552
27920
  resetView: () => this.resetView(),
27553
27921
  // User drawings claim a gesture before pan when armed / over a drawing.
27554
27922
  drawingsClaim: (x, y) => this.userDrawings?.claim(x, y) ?? false,
27555
- drawingsMeasureStart: (x, y) => this.userDrawings?.beginMeasureAt(x, y) ?? false,
27923
+ drawingsMeasureStart: (x, y, snap) => this.userDrawings?.beginMeasureAt(x, y, snap) ?? false,
27556
27924
  drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y) ?? false,
27557
27925
  drawingsCancelPlacement: () => this.userDrawings?.cancelPlacement() ?? false,
27558
27926
  drawingsSnapMode: () => this.snapMode,
27559
27927
  drawingsPointerDown: (x, y, snap, shift) => this.userDrawings?.pointerDown(x, y, snap, shift),
27560
27928
  drawingsPointerMove: (x, y, snap, shift) => this.userDrawings?.pointerMove(x, y, snap, shift),
27561
- drawingsPointerUp: (x, y) => this.userDrawings?.pointerUp(x, y),
27929
+ drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
27562
27930
  drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
27563
27931
  drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
27564
27932
  drawingsClearTransient: () => this.userDrawings?.clearTransient()
@@ -27575,7 +27943,7 @@ var NativeRenderer = class {
27575
27943
  this.plot.addEventListener("pointerleave", this.onScrollProximityLeave);
27576
27944
  this.labelTooltip = new LabelTooltip(this.plot, {
27577
27945
  theme: () => this.chromeTheme(),
27578
- lookup: (x, y) => this.chrome.labelTooltipAt(x, y)
27946
+ lookup: (x, y) => this.indicatorSlices.labelTooltipAt(x, y)
27579
27947
  });
27580
27948
  this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
27581
27949
  projector: () => this.drawingProjector(),
@@ -27660,6 +28028,7 @@ var NativeRenderer = class {
27660
28028
  this.emitPaneAction({ type: "maximize", paneId, maximized });
27661
28029
  }
27662
28030
  });
28031
+ this.paneControls.setSuspended(this.layoutMode === "mobile");
27663
28032
  this.axisScaleButtons = new AxisScaleButtons(this.plot, theme, {
27664
28033
  panes: () => this.axisScaleViews(),
27665
28034
  rightAxis: () => this.rightAxisW,
@@ -27669,8 +28038,19 @@ var NativeRenderer = class {
27669
28038
  if (pane) this.setPaneLog(paneId, !paneLogScale(this.scene, pane));
27670
28039
  }
27671
28040
  });
27672
- this.resizeObserver = new ResizeObserver(() => this.resize());
28041
+ this.resizeObserver = new ResizeObserver((entries) => {
28042
+ for (const e of entries) {
28043
+ if (e.target !== this.plot) continue;
28044
+ const s = e.devicePixelContentBoxSize?.[0];
28045
+ if (s) this.plotDeviceSize = { width: s.inlineSize, height: s.blockSize };
28046
+ }
28047
+ this.resize();
28048
+ });
27673
28049
  this.resizeObserver.observe(this.wrapper);
28050
+ try {
28051
+ this.resizeObserver.observe(this.plot, { box: "device-pixel-content-box" });
28052
+ } catch {
28053
+ }
27674
28054
  this.watchDpr();
27675
28055
  this.syncSize();
27676
28056
  }
@@ -27795,8 +28175,6 @@ var NativeRenderer = class {
27795
28175
  this.inputsUI?.destroy();
27796
28176
  this.paneControls?.destroy();
27797
28177
  this.axisScaleButtons?.destroy();
27798
- for (const overlay of this.tableOverlays.values()) overlay.destroy();
27799
- this.tableOverlays.clear();
27800
28178
  this.resizeObserver?.disconnect();
27801
28179
  this.resizeObserver = null;
27802
28180
  this.dprMedia?.removeEventListener("change", this.onDprChange);
@@ -27825,6 +28203,7 @@ var NativeRenderer = class {
27825
28203
  this.attributionEl = null;
27826
28204
  this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
27827
28205
  this.mountContainer?.style.removeProperty("--vela-scale-gutter");
28206
+ this.mountContainer?.style.removeProperty("--vela-bottom-gutter");
27828
28207
  this.mountContainer?.style.removeProperty("--vela-price-pane-top");
27829
28208
  this.mountContainer?.style.removeProperty("--vela-price-pane-bottom");
27830
28209
  this.mountContainer = null;
@@ -27862,7 +28241,8 @@ var NativeRenderer = class {
27862
28241
  }
27863
28242
  const skipFit = opts?.preserveView === true && this.didInitialFit;
27864
28243
  if (this.coords.width > 0 && !skipFit) {
27865
- this.fitContent();
28244
+ if (this.didInitialFit) this.reframeKeepZoom();
28245
+ else this.fitContent();
27866
28246
  this.didInitialFit = true;
27867
28247
  }
27868
28248
  if (!this.introPlayed && this.bars.length > 0) {
@@ -27917,7 +28297,6 @@ var NativeRenderer = class {
27917
28297
  ensurePane(pane) {
27918
28298
  this.scene.ensurePane(pane.id, pane.kind, pane.order, pane.heightWeight ?? (pane.kind === "price" ? 3 : 1));
27919
28299
  this.layoutPanes();
27920
- this.repositionTables();
27921
28300
  this.paneControls?.refresh();
27922
28301
  this.scheduler.invalidate(4 /* Full */);
27923
28302
  }
@@ -27939,17 +28318,14 @@ var NativeRenderer = class {
27939
28318
  if (!model.ownScale) this.scene.dropIndicatorScale(handle.id);
27940
28319
  this.inputsUI.setPane(handle.id, paneId);
27941
28320
  this.refreshAnchorOffset(model);
27942
- this.syncTables(model);
27943
28321
  this.refreshAxisWidth();
27944
28322
  this.layoutPanes();
27945
- this.repositionTables();
27946
28323
  this.paneControls?.refresh();
27947
28324
  this.scheduler.invalidate(4 /* Full */);
27948
28325
  }
27949
28326
  orderPanes(orderedIds) {
27950
28327
  this.scene.orderPanes(orderedIds);
27951
28328
  this.layoutPanes();
27952
- this.repositionTables();
27953
28329
  this.paneControls?.refresh();
27954
28330
  this.scheduler.invalidate(4 /* Full */);
27955
28331
  }
@@ -27958,7 +28334,6 @@ var NativeRenderer = class {
27958
28334
  if (!pane || pane.collapsed === collapsed) return;
27959
28335
  pane.collapsed = collapsed;
27960
28336
  this.layoutPanes();
27961
- this.repositionTables();
27962
28337
  this.paneControls?.refresh();
27963
28338
  this.scheduler.invalidate(4 /* Full */);
27964
28339
  }
@@ -27966,7 +28341,6 @@ var NativeRenderer = class {
27966
28341
  if (paneId !== null && !this.scene.panes.has(paneId)) paneId = null;
27967
28342
  this.maximizedPaneId = paneId;
27968
28343
  this.layoutPanes();
27969
- this.repositionTables();
27970
28344
  this.paneControls?.refresh();
27971
28345
  this.scheduler.invalidate(4 /* Full */);
27972
28346
  }
@@ -28053,7 +28427,6 @@ var NativeRenderer = class {
28053
28427
  native: !!model.native,
28054
28428
  ...model.props ? { props: model.props, propValues: model.propValues ?? {} } : {}
28055
28429
  });
28056
- this.syncTables(model);
28057
28430
  if (model.native?.type === "volume") {
28058
28431
  this.volumeActive = true;
28059
28432
  this.volumeHidden = false;
@@ -28077,7 +28450,6 @@ var NativeRenderer = class {
28077
28450
  }
28078
28451
  }
28079
28452
  applyPatch(model, patch);
28080
- this.syncTables(model);
28081
28453
  this.scheduler.invalidate(3 /* Light */);
28082
28454
  }
28083
28455
  removeIndicator(handle) {
@@ -28096,8 +28468,6 @@ var NativeRenderer = class {
28096
28468
  this.scene.forgetAnchorOffset(handle.id);
28097
28469
  this.scene.dropIndicatorScale(handle.id);
28098
28470
  this.inputsUI.remove(handle.id);
28099
- this.tableOverlays.get(handle.id)?.destroy();
28100
- this.tableOverlays.delete(handle.id);
28101
28471
  this.refreshAxisWidth();
28102
28472
  this.paneControls?.refresh();
28103
28473
  this.scheduler.invalidate(4 /* Full */);
@@ -28141,8 +28511,6 @@ var NativeRenderer = class {
28141
28511
  }
28142
28512
  if (!visible) {
28143
28513
  this.scene.indicators.delete(handle.id);
28144
- this.tableOverlays.get(handle.id)?.destroy();
28145
- this.tableOverlays.delete(handle.id);
28146
28514
  }
28147
28515
  this.inputsUI.setVisible(handle.id, visible);
28148
28516
  this.scheduler.invalidate(4 /* Full */);
@@ -28188,6 +28556,7 @@ var NativeRenderer = class {
28188
28556
  this.userDrawings?.setLayoutMode(mode);
28189
28557
  this.settingsDialog?.setLayoutMode(mode);
28190
28558
  this.inputsUI?.setLayoutMode(mode);
28559
+ this.paneControls?.setSuspended(mode === "mobile");
28191
28560
  if (this.scrollButton) {
28192
28561
  const px = mode === "mobile" ? SCROLL_BTN_SIZE_TOUCH : SCROLL_BTN_SIZE;
28193
28562
  this.scrollButton.style.width = `${px}px`;
@@ -28521,6 +28890,7 @@ var NativeRenderer = class {
28521
28890
  this.scaleDragHeight = res.height;
28522
28891
  this.scaleDragStart = { ...res.holder.scale };
28523
28892
  res.holder.manualScale = { ...res.holder.scale };
28893
+ this.axisScaleButtons?.reposition();
28524
28894
  this.scheduler.invalidate(4 /* Full */);
28525
28895
  }
28526
28896
  /** Rescale the grabbed scale around its center by the total drag (down ⇒ zoom out). */
@@ -28552,6 +28922,7 @@ var NativeRenderer = class {
28552
28922
  const res = this.resolveScaleHolder(x, y);
28553
28923
  if (!res) return;
28554
28924
  res.holder.manualScale = null;
28925
+ this.axisScaleButtons?.reposition();
28555
28926
  this.scheduler.invalidate(4 /* Full */);
28556
28927
  }
28557
28928
  /**
@@ -28583,7 +28954,6 @@ var NativeRenderer = class {
28583
28954
  /** Relayout + repaint + refresh the hover buttons after a collapse/maximize/order change. */
28584
28955
  afterPaneLayoutChange() {
28585
28956
  this.layoutPanes();
28586
- this.repositionTables();
28587
28957
  this.paneControls?.refresh();
28588
28958
  this.scheduler.invalidate(4 /* Full */);
28589
28959
  }
@@ -28664,7 +29034,6 @@ var NativeRenderer = class {
28664
29034
  above.heightWeight = next.above;
28665
29035
  below.heightWeight = next.below;
28666
29036
  this.layoutPanes();
28667
- this.repositionTables();
28668
29037
  this.scheduler.invalidate(4 /* Full */);
28669
29038
  }
28670
29039
  /** Double-click a separator → split the two adjacent panes evenly (each gets half of
@@ -28679,7 +29048,6 @@ var NativeRenderer = class {
28679
29048
  above.heightWeight = half;
28680
29049
  below.heightWeight = half;
28681
29050
  this.layoutPanes();
28682
- this.repositionTables();
28683
29051
  this.scheduler.invalidate(4 /* Full */);
28684
29052
  }
28685
29053
  // ── keyboard navigation / accessibility (item 11) ──
@@ -28958,7 +29326,10 @@ var NativeRenderer = class {
28958
29326
  const liveActual = li >= 0 ? this.bars[li] : void 0;
28959
29327
  const easeLive = !!liveActual && this.liveEaseTime === liveActual.time && (liveActual.high !== this.liveEaseHigh || liveActual.low !== this.liveEaseLow || liveActual.close !== this.liveEaseClose);
28960
29328
  if (easeLive && liveActual) this.bars[li] = { ...liveActual, high: this.liveEaseHigh, low: this.liveEaseLow, close: this.liveEaseClose };
28961
- this.scene.drawingSlices = this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map();
29329
+ this.scene.drawingSlices = mergeSlices(
29330
+ this.indicatorSlices.prepare(this.scene, this.coords, this.theme, this.dataCanvas),
29331
+ this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map()
29332
+ );
28962
29333
  this.backdropRenderer.render(this.scene, this.coords, this.theme, gridAlpha);
28963
29334
  this.backend.render(this.scene, this.coords, this.theme);
28964
29335
  this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
@@ -29189,6 +29560,20 @@ var NativeRenderer = class {
29189
29560
  this.coords.setViewport(v);
29190
29561
  this.targetBarSpacing = v.barSpacing;
29191
29562
  }
29563
+ /** Re-frame after a series replacement (a symbol/timeframe switch): keep the user's
29564
+ * zoom (bar spacing), re-anchor the newest bars at the default right offset.
29565
+ * `clampViewport`'s fit-all-bars floor deliberately does NOT apply — a progressive
29566
+ * head may still be backfilling toward the previous depth, and raising the spacing
29567
+ * to its temporary bar count would lose the zoom this exists to keep. */
29568
+ reframeKeepZoom() {
29569
+ this.animator?.stop();
29570
+ this.panVelocity = 0;
29571
+ for (const pane of this.scene.panes.values()) pane.manualScale = null;
29572
+ for (const sl of this.scene.indicatorScales.values()) sl.manualScale = null;
29573
+ const v = { barSpacing: clampBarSpacing(this.coords.getViewport().barSpacing), rightOffset: defaultViewport().rightOffset };
29574
+ this.coords.setViewport(v);
29575
+ this.targetBarSpacing = v.barSpacing;
29576
+ }
29192
29577
  paneBoundsFor(paneId) {
29193
29578
  const p = this.scene.panes.get(paneId);
29194
29579
  return { top: p?.bounds.top ?? 0, height: p?.bounds.height ?? 0, rightAxis: this.rightAxisW };
@@ -29396,27 +29781,6 @@ var NativeRenderer = class {
29396
29781
  }
29397
29782
  return maxVol;
29398
29783
  }
29399
- /** Create/update/destroy an indicator's DOM table overlay (anchored off real pane geometry). */
29400
- syncTables(model) {
29401
- const tables = model.tables ?? [];
29402
- let overlay = this.tableOverlays.get(model.id);
29403
- if (tables.length === 0) {
29404
- if (overlay) {
29405
- overlay.destroy();
29406
- this.tableOverlays.delete(model.id);
29407
- }
29408
- return;
29409
- }
29410
- if (!overlay) {
29411
- overlay = new TableOverlay(this.plot, this.theme, (id) => this.paneBoundsFor(id));
29412
- overlay.setVisible(this.loadingEl === null);
29413
- this.tableOverlays.set(model.id, overlay);
29414
- }
29415
- overlay.update(tables);
29416
- }
29417
- repositionTables() {
29418
- for (const overlay of this.tableOverlays.values()) overlay.reposition();
29419
- }
29420
29784
  layoutPanes() {
29421
29785
  const panes = this.scene.orderedPanes();
29422
29786
  const dataHeight = this.coords.height;
@@ -29467,6 +29831,7 @@ var NativeRenderer = class {
29467
29831
  const visible = maxPane ? [maxPane] : this.scene.orderedPanes().filter((p) => !p.collapsed);
29468
29832
  const paneBottom = visible.length ? Math.max(...visible.map((p) => p.bounds.top + p.bounds.height)) : dataHeight;
29469
29833
  this.scrollBtnBottomPx = SCROLL_BTN_BOTTOM + Math.max(0, dataHeight - paneBottom);
29834
+ this.mountContainer?.style.setProperty("--vela-bottom-gutter", `${TIME_AXIS_H + Math.max(0, dataHeight - paneBottom)}px`);
29470
29835
  this.scrollBtnRightPx = this.rightAxisW + SCROLL_BTN_RIGHT_INSET;
29471
29836
  if (this.scrollButton) {
29472
29837
  this.scrollButton.style.bottom = `${this.scrollBtnBottomPx}px`;
@@ -29559,30 +29924,33 @@ var NativeRenderer = class {
29559
29924
  if (w <= 0 || h <= 0) return;
29560
29925
  const dpr = window.devicePixelRatio || 1;
29561
29926
  this.plot.style.left = `${this.toolbarGutter}px`;
29562
- const pw = Math.max(1, w - this.toolbarGutter);
29563
- const ph = h;
29564
- this.dataCanvas.width = Math.round(pw * dpr);
29565
- this.dataCanvas.height = Math.round(ph * dpr);
29566
- this.backdropCanvas.width = this.dataCanvas.width;
29567
- this.backdropCanvas.height = this.dataCanvas.height;
29568
- this.volumeCanvas.width = this.dataCanvas.width;
29569
- this.volumeCanvas.height = this.dataCanvas.height;
29570
- for (const l of this.extLayers) {
29571
- l.canvas.width = this.dataCanvas.width;
29572
- l.canvas.height = this.dataCanvas.height;
29573
- }
29574
- this.vpvrCanvas.width = this.dataCanvas.width;
29575
- this.vpvrCanvas.height = this.dataCanvas.height;
29576
- this.chromeCanvas.width = this.dataCanvas.width;
29577
- this.chromeCanvas.height = this.dataCanvas.height;
29578
- this.drawingsCanvas.width = this.dataCanvas.width;
29579
- this.drawingsCanvas.height = this.dataCanvas.height;
29580
- this.cursorCanvas.width = this.dataCanvas.width;
29581
- this.cursorCanvas.height = this.dataCanvas.height;
29927
+ const rect = this.plot.getBoundingClientRect();
29928
+ let bw = Math.max(1, Math.round(rect.width * dpr));
29929
+ let bh = Math.max(1, Math.round(rect.height * dpr));
29930
+ const dev = this.plotDeviceSize;
29931
+ if (dev && Math.abs(dev.width - rect.width * dpr) <= 1 && Math.abs(dev.height - rect.height * dpr) <= 1) {
29932
+ bw = Math.max(1, dev.width);
29933
+ bh = Math.max(1, dev.height);
29934
+ }
29935
+ const pw = bw / dpr;
29936
+ const ph = bh / dpr;
29937
+ const size = (canvas) => {
29938
+ canvas.width = bw;
29939
+ canvas.height = bh;
29940
+ canvas.style.width = `${pw}px`;
29941
+ canvas.style.height = `${ph}px`;
29942
+ };
29943
+ size(this.dataCanvas);
29944
+ size(this.backdropCanvas);
29945
+ size(this.volumeCanvas);
29946
+ for (const l of this.extLayers) size(l.canvas);
29947
+ size(this.vpvrCanvas);
29948
+ size(this.chromeCanvas);
29949
+ size(this.drawingsCanvas);
29950
+ size(this.cursorCanvas);
29582
29951
  this.coords.setSize(Math.max(1, pw - this.rightAxisW), Math.max(1, ph - TIME_AXIS_H), dpr);
29583
29952
  this.scene.crosshair = null;
29584
29953
  this.layoutPanes();
29585
- this.repositionTables();
29586
29954
  this.userDrawings?.onResize();
29587
29955
  if (!this.didInitialFit && this.coords.barCount > 0) {
29588
29956
  this.fitContent();
@@ -30400,6 +30768,7 @@ exports.drawingTypes = drawingTypes;
30400
30768
  exports.getDrawingType = getDrawingType;
30401
30769
  exports.getNativeIndicator = getNativeIndicator;
30402
30770
  exports.iconMarkup = iconMarkup;
30771
+ exports.inputDeltas = inputDeltas;
30403
30772
  exports.inputVisible = inputVisible;
30404
30773
  exports.legendActions = legendActions;
30405
30774
  exports.legendCallouts = legendCallouts;