@luxalgo/vela 0.6.9 → 0.6.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/{DataProvider-8Z95Q-RJ.d.cts → DataProvider-CWmp31dA.d.ts} +49 -1
  2. package/dist/{DataProvider-DExJrfut.d.ts → DataProvider-l_eLMly_.d.cts} +49 -1
  3. package/dist/{chunk-EQCHJZOT.js → chunk-4KZVZ7QQ.js} +1 -1
  4. package/dist/{chunk-6WDDVMBJ.js → chunk-FFOP37FQ.js} +577 -321
  5. package/dist/{chunk-62SVGONC.js → chunk-G77Y7LK2.js} +2 -2
  6. package/dist/{chunk-WFSZBX3R.js → chunk-KG4YT3TI.js} +3 -1
  7. package/dist/{chunk-FCVIG7JG.js → chunk-MHY7MVXH.js} +5 -1
  8. package/dist/{chunk-STHSKXOR.js → chunk-NMTQXNT4.js} +591 -141
  9. package/dist/{contributions-D7PVZO2i.d.ts → contributions-BCz6Dr6a.d.ts} +6 -6
  10. package/dist/{contributions-C1U2Krwg.d.cts → contributions-D9vTzm5p.d.cts} +6 -6
  11. package/dist/index.cjs +580 -318
  12. package/dist/index.d.cts +31 -10
  13. package/dist/index.d.ts +31 -10
  14. package/dist/index.js +4 -4
  15. package/dist/{options-FM0peknS.d.ts → options-DSqHsQyN.d.cts} +6 -6
  16. package/dist/{options-FM0peknS.d.cts → options-DSqHsQyN.d.ts} +6 -6
  17. package/dist/{plugin-DfVqBz9p.d.cts → plugin-Bt8hLR8Z.d.cts} +3 -3
  18. package/dist/{plugin-7bkF32Rk.d.ts → plugin-CH7pfMrE.d.ts} +3 -3
  19. package/dist/plugin.cjs +3 -1
  20. package/dist/plugin.d.cts +4 -4
  21. package/dist/plugin.d.ts +4 -4
  22. package/dist/plugin.js +2 -2
  23. package/dist/providers/binance.d.cts +2 -2
  24. package/dist/providers/binance.d.ts +2 -2
  25. package/dist/providers/coinbase.d.cts +2 -2
  26. package/dist/providers/coinbase.d.ts +2 -2
  27. package/dist/providers/hyperliquid.d.cts +2 -2
  28. package/dist/providers/hyperliquid.d.ts +2 -2
  29. package/dist/{statusline-zdF4eZLr.d.ts → statusline-5K7y4Ya2.d.ts} +3 -3
  30. package/dist/{statusline-DOPiT6I6.d.cts → statusline-TYLQmoeh.d.cts} +3 -3
  31. package/dist/ui.cjs +7 -1
  32. package/dist/ui.d.cts +3 -1
  33. package/dist/ui.d.ts +3 -1
  34. package/dist/ui.js +3 -3
  35. package/dist/vela.global.js +580 -318
  36. package/dist/vela.global.min.js +50 -48
  37. package/dist/widget.cjs +1052 -340
  38. package/dist/widget.d.cts +17 -6
  39. package/dist/widget.d.ts +17 -6
  40. package/dist/widget.js +7 -7
  41. package/dist/workspace.cjs +1052 -340
  42. package/dist/workspace.d.cts +89 -5
  43. package/dist/workspace.d.ts +89 -5
  44. package/dist/workspace.js +6 -6
  45. package/package.json +1 -1
package/dist/widget.cjs CHANGED
@@ -125,6 +125,23 @@ var TypedEventBus = class {
125
125
  }
126
126
  };
127
127
 
128
+ // src/data/symbol-groups.ts
129
+ function isGroupRow(d) {
130
+ return d.group != null && d.ticker === d.group;
131
+ }
132
+ function groupKeyOf(d) {
133
+ return `${(d.prefix ?? d.provider ?? "").toLowerCase()}:${(d.group ?? "").toUpperCase()}`;
134
+ }
135
+ function groupMembers(pool, groupRow) {
136
+ const key = groupKeyOf(groupRow);
137
+ return pool.filter((s) => s.group != null && !isGroupRow(s) && groupKeyOf(s) === key);
138
+ }
139
+ function defaultMemberOf(pool, groupRow) {
140
+ const members = groupMembers(pool, groupRow);
141
+ const defaults2 = members.filter((m) => m.default);
142
+ return (defaults2.length === 1 ? defaults2[0] : members[0]) ?? null;
143
+ }
144
+
128
145
  // src/data/ProviderRegistry.ts
129
146
  var normName = (n) => n.trim().toLowerCase();
130
147
  var normTicker = (t) => t.trim().toUpperCase();
@@ -214,17 +231,24 @@ var ProviderRegistry = class {
214
231
  resolve(raw, opts = {}) {
215
232
  const { provider, ticker } = this.parse(raw);
216
233
  if (provider) {
217
- if (this.entries.has(provider)) return { provider, ticker };
234
+ if (this.entries.has(provider)) {
235
+ const d = this.entries.get(provider).byTicker?.get(normTicker(ticker));
236
+ return { provider, ticker: d != null && isGroupRow(d) ? this.loadableTicker(provider, d) : ticker };
237
+ }
218
238
  const key = `${provider}:${normTicker(ticker)}`;
219
239
  for (const name of this.candidateOrder(opts.default)) {
220
240
  const d = this.entries.get(name).prefixIndex?.get(key);
221
- if (d) return { provider: name, ticker: d.ticker };
241
+ if (d) return { provider: name, ticker: this.loadableTicker(name, d) };
222
242
  }
223
243
  return null;
224
244
  }
225
245
  const norm = normTicker(ticker);
226
246
  for (const name of this.candidateOrder(opts.default)) {
227
- if (this.entries.get(name).index?.has(norm)) return { provider: name, ticker };
247
+ const e = this.entries.get(name);
248
+ if (e.index?.has(norm)) {
249
+ const d = e.byTicker?.get(norm);
250
+ return { provider: name, ticker: d != null && isGroupRow(d) ? this.loadableTicker(name, d) : ticker };
251
+ }
228
252
  }
229
253
  if (opts.lenient && this.entries.size === 1) {
230
254
  const only = this.names()[0];
@@ -233,6 +257,13 @@ var ProviderRegistry = class {
233
257
  }
234
258
  return null;
235
259
  }
260
+ /** What a resolved descriptor LOADS: itself — unless it is a GROUP row (listed, never
261
+ * served), which loads its default member (single `default`, else first listed). A
262
+ * memberless group keeps its own ticker and fails downstream like any unknown symbol. */
263
+ loadableTicker(name, d) {
264
+ if (!isGroupRow(d)) return d.ticker;
265
+ return defaultMemberOf(this.entries.get(name)?.descriptors ?? [], d)?.ticker ?? d.ticker;
266
+ }
236
267
  /**
237
268
  * Resolve now if possible, else resolve later — re-attempting after each provider
238
269
  * settles. The promise stays pending until some registered provider can serve the
@@ -486,6 +517,28 @@ var CachingDataFeed = class {
486
517
  this.store.merge(key, dropForming(bars));
487
518
  return bars;
488
519
  }
520
+ /**
521
+ * Progressive twin of {@link load}: a COLD load streams through the inner feed's
522
+ * progressive path — batches forwarded verbatim, the FINAL answer cached exactly as
523
+ * `load` caches — while a cache-covered load answers once through `load` itself (a
524
+ * warm chart has nothing to stream). Falls back to `load` wholesale when the inner
525
+ * feed lacks the capability, so callers may prefer this method unconditionally.
526
+ */
527
+ async loadProgressive(cfg, onBatch, opts) {
528
+ if (cfg.data && cfg.data.length > 0) return this.inner.load(cfg);
529
+ if (!this.inner.loadProgressive) return null;
530
+ const symbol = cfg.symbol ?? "TEST";
531
+ const key = cacheKey(symbol, cfg.timeframe ?? "60", cfg.session);
532
+ const cached = this.store.get(key);
533
+ const n = cfg.bars ?? 500;
534
+ const lastCached = cached?.[cached.length - 1];
535
+ if (cached && lastCached && cached.length >= n - 1 && this.inner.loadRange) return this.load(cfg);
536
+ this.store.retainSymbol(symbol);
537
+ const bars = await this.inner.loadProgressive(cfg, onBatch, opts);
538
+ if (bars == null) return null;
539
+ this.store.merge(key, dropForming(bars));
540
+ return bars;
541
+ }
489
542
  /**
490
543
  * Cache-backed ranged fetch — the gateway engines use for secondary series
491
544
  * (`request.security` HTF/LTF/cross-symbol) and the orchestrator's backward
@@ -731,6 +784,17 @@ var MultiProviderFeed = class {
731
784
  this.prefetchSymbolInfo(resolved);
732
785
  return this.cache.load(canonical(cfg, resolved));
733
786
  }
787
+ /** Progressive twin of {@link load} — same resolution, the cache streams the batches. */
788
+ async loadProgressive(cfg, onBatch, opts) {
789
+ if (cfg.data && cfg.data.length > 0) {
790
+ this.liveBars = cfg.data.map((b) => ({ ...b }));
791
+ return cfg.data;
792
+ }
793
+ const resolved = await this.registry.whenResolvable(rawSymbol(cfg), { default: this.primaryProvider });
794
+ this.primaryProvider = resolved.provider;
795
+ this.prefetchSymbolInfo(resolved);
796
+ return this.cache.loadProgressive(canonical(cfg, resolved), onBatch, opts);
797
+ }
734
798
  /**
735
799
  * Synchronous per-symbol metadata for engines (Pine `syminfo.*`), served from the cache
736
800
  * warmed by load(). Undefined until the prefetch lands (the engine then synthesizes a
@@ -818,6 +882,18 @@ var RegistryFetchFeed = class {
818
882
  if (!provider) return Promise.resolve([]);
819
883
  return safeBars(provider, ticker, cfg.timeframe ?? "60", { limit: cfg.bars ?? 500, session: cfg.session });
820
884
  }
885
+ async loadProgressive(cfg, onBatch, opts) {
886
+ const { provider: name, ticker } = parseSymbol(cfg.symbol ?? "");
887
+ const provider = this.registry.get(name ?? "");
888
+ if (!provider) return [];
889
+ if (!provider.getBarsProgressive) return null;
890
+ try {
891
+ return await provider.getBarsProgressive(ticker, cfg.timeframe ?? "60", { limit: cfg.bars ?? 500, session: cfg.session }, onBatch, opts);
892
+ } catch (e) {
893
+ console.warn(`[vela] progressive fetch failed for ${ticker} ${cfg.timeframe ?? "60"} \u2014 ${e instanceof Error ? e.message : String(e)}`);
894
+ return [];
895
+ }
896
+ }
821
897
  loadRange(cfg, range) {
822
898
  const { provider: name, ticker } = parseSymbol(cfg.symbol ?? "");
823
899
  const provider = this.registry.get(name ?? "");
@@ -1134,6 +1210,8 @@ registerIcon("folder-plus", S('<path d="M1.6 4.4a1 1 0 0 1 1-1h2.7l1.3 1.7h6.8a1
1134
1210
  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"/>'));
1135
1211
  registerIcon("collapse", S('<path d="M3 8h10"/>'));
1136
1212
  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"/>'));
1213
+ registerIcon("plus", S('<path d="M8 2.8v10.4M2.8 8h10.4"/>'));
1214
+ registerIcon("minus", S('<path d="M2.8 8h10.4"/>'));
1137
1215
  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"/>'));
1138
1216
  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"/>'));
1139
1217
  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"/>'));
@@ -1141,7 +1219,7 @@ registerIcon("star-filled", S('<path d="M8 2.2l1.75 3.55 3.9.55-2.8 2.75.65 3.9L
1141
1219
  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"'));
1142
1220
  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"'));
1143
1221
  registerIcon("burger", S('<path d="M2.5 4.5h11M2.5 8h11M2.5 11.5h11"/>'));
1144
- 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"/>'));
1222
+ 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"/>'));
1145
1223
  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"/>'));
1146
1224
  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"/>'));
1147
1225
  registerIcon("cursor", svg24('<path d="M5 3l6 16 2-6 6-2z"/>', 'fill="currentColor" stroke="none"'));
@@ -4307,6 +4385,10 @@ var CalloutBubble = class {
4307
4385
  get open() {
4308
4386
  return this.pop?.open ?? false;
4309
4387
  }
4388
+ /** Close the deployed panel, if any. The bubble itself stays. */
4389
+ hidePanel() {
4390
+ this.pop?.hide();
4391
+ }
4310
4392
  destroy() {
4311
4393
  this.pop?.destroy();
4312
4394
  this.pop = null;
@@ -13740,6 +13822,17 @@ function dedupeSymbols(list) {
13740
13822
  }
13741
13823
  return out;
13742
13824
  }
13825
+ function foldGroups(list) {
13826
+ const groupsAbove = /* @__PURE__ */ new Set();
13827
+ const out = [];
13828
+ for (const s of list) {
13829
+ if (isGroupRow(s)) {
13830
+ groupsAbove.add(groupKeyOf(s));
13831
+ out.push(s);
13832
+ } else if (s.group == null || !groupsAbove.has(groupKeyOf(s))) out.push(s);
13833
+ }
13834
+ return out;
13835
+ }
13743
13836
  var TOP_TICKERS = ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT", "DOGEUSDT", "ADAUSDT", "LINKUSDT"];
13744
13837
  function parseQuery(raw, venues) {
13745
13838
  const m = raw.match(/^\s*([^\s:]+)\s*[:\s]\s*(.*)$/);
@@ -13871,6 +13964,21 @@ var CSS8 = `
13871
13964
  .vela-sp-badge[data-p='binance'] { color: #f0b90b; } /* palette-exempt: venue brand mark */
13872
13965
  .vela-sp-badge[data-p='hyperliquid'] { color: #50d2c1; } /* palette-exempt: venue brand mark */
13873
13966
  .vela-sp-empty { padding: var(--vela-space-3); color: var(--vela-fg-muted); text-align: center; }
13967
+ /* Grouped listings (futures roots): the chevron unfolds members inline, indented. */
13968
+ .vela-sp-expander {
13969
+ all: unset;
13970
+ flex: none;
13971
+ display: inline-flex;
13972
+ align-items: center;
13973
+ justify-content: center;
13974
+ width: 22px;
13975
+ height: 22px;
13976
+ border-radius: 5px;
13977
+ cursor: pointer;
13978
+ color: var(--vela-fg-muted);
13979
+ }
13980
+ .vela-sp-expander:hover { background: var(--vela-surface-elev); color: var(--vela-fg); }
13981
+ .vela-sp-row[data-member] { padding-left: 34px; }
13874
13982
  `;
13875
13983
  var PAGE = 100;
13876
13984
  var SymbolPicker = class {
@@ -13882,6 +13990,11 @@ var SymbolPicker = class {
13882
13990
  this.seed = "";
13883
13991
  this.activeTab = "All";
13884
13992
  this.visible = PAGE;
13993
+ /** The last filter pass returned fewer raw rows than asked — the pool is drained
13994
+ * (checked BEFORE folding: folding shortens pages without meaning exhaustion). */
13995
+ this.exhausted = false;
13996
+ /** Group rows currently expanded (venue-scoped keys) — members shown inline. */
13997
+ this.expanded = /* @__PURE__ */ new Set();
13885
13998
  /** The ranked pool cache — `key` fingerprints the raw pool the ranking ran on. */
13886
13999
  this.ranked = null;
13887
14000
  this.ranking = false;
@@ -13896,7 +14009,7 @@ var SymbolPicker = class {
13896
14009
  searchRow.append(iconEl("search", doc), this.input);
13897
14010
  this.tabs = doc.createElement("div");
13898
14011
  this.tabs.className = "vela-sp-tabs";
13899
- for (const t of ["All", "Stocks", "ETFs", "Crypto", "Forex", "Commodities"]) {
14012
+ for (const t of ["All", "Stocks", "ETFs", "Crypto", "Futures", "Forex", "Commodities"]) {
13900
14013
  const b = doc.createElement("button");
13901
14014
  b.className = "vela-sp-tab";
13902
14015
  b.textContent = t;
@@ -13912,7 +14025,7 @@ var SymbolPicker = class {
13912
14025
  this.list = doc.createElement("div");
13913
14026
  this.list.className = "vela-sp-list";
13914
14027
  this.list.addEventListener("scroll", () => {
13915
- if (this.rows.length < this.visible) return;
14028
+ if (this.exhausted) return;
13916
14029
  if (this.list.scrollTop + this.list.clientHeight < this.list.scrollHeight - 200) return;
13917
14030
  this.visible += PAGE;
13918
14031
  this.grow();
@@ -13941,14 +14054,19 @@ var SymbolPicker = class {
13941
14054
  else if (e.key === "ArrowUp") this.moveHighlight(-1);
13942
14055
  else if (e.key === "Enter") {
13943
14056
  const pick = this.rows[this.highlighted];
13944
- if (pick) this.select(pick.ticker, pick.prefix ?? pick.provider, opts.onSelect);
14057
+ if (pick) this.pick(pick);
13945
14058
  return;
13946
14059
  } else return;
13947
14060
  e.preventDefault();
13948
14061
  });
13949
14062
  this.list.addEventListener("click", (e) => {
13950
- const row = e.target.closest(".vela-sp-row");
13951
- if (row?.dataset.ticker) this.select(row.dataset.ticker, row.dataset.venue, opts.onSelect);
14063
+ const target = e.target;
14064
+ const row = target.closest(".vela-sp-row");
14065
+ if (!row) return;
14066
+ const s = this.rows[Number(row.dataset.i)];
14067
+ if (!s) return;
14068
+ if (target.closest(".vela-sp-expander")) this.toggleExpand(s);
14069
+ else this.pick(s);
13952
14070
  });
13953
14071
  }
13954
14072
  /** Wire where symbols come from (re-called on every widget rebuild). */
@@ -13965,6 +14083,27 @@ var SymbolPicker = class {
13965
14083
  destroy() {
13966
14084
  this.dialog.destroy();
13967
14085
  }
14086
+ /** Route a row activation: a GROUP row loads its default member (the root itself is
14087
+ * listed, never loadable), any other row loads itself. */
14088
+ pick(s) {
14089
+ const target = isGroupRow(s) ? defaultMemberOf(this.pool(), s) ?? s : s;
14090
+ this.select(target.ticker, target.prefix ?? target.provider, this.opts.onSelect);
14091
+ }
14092
+ /** Expand/collapse a group row IN PLACE — same query, same page, same scroll; only
14093
+ * the member rows under the group appear or go. */
14094
+ toggleExpand(s) {
14095
+ const key = groupKeyOf(s);
14096
+ if (!this.expanded.delete(key)) this.expanded.add(key);
14097
+ const scrollTop = this.list.scrollTop;
14098
+ const focus = this.rows[this.highlighted];
14099
+ this.rows = this.computeRows();
14100
+ this.list.replaceChildren();
14101
+ this.rows.forEach((r, i) => this.list.appendChild(this.rowEl(r, i)));
14102
+ const at = focus ? this.rows.indexOf(focus) : -1;
14103
+ this.highlighted = at >= 0 ? at : Math.min(this.highlighted, Math.max(0, this.rows.length - 1));
14104
+ this.renderHighlight();
14105
+ this.list.scrollTop = scrollTop;
14106
+ }
13968
14107
  select(ticker, venue, onSelect) {
13969
14108
  this.close();
13970
14109
  onSelect(venue ? `${venue}:${ticker}` : ticker);
@@ -14006,10 +14145,26 @@ var SymbolPicker = class {
14006
14145
  return this.ranked?.result ?? raw;
14007
14146
  }
14008
14147
  computeRows() {
14009
- const TAB_TYPES = { Crypto: ["crypto"], Stocks: ["stock"], ETFs: ["etf"], Forex: ["forex"], Commodities: ["commodity"] };
14148
+ const TAB_TYPES = { Crypto: ["crypto"], Stocks: ["stock"], ETFs: ["etf"], Futures: ["futures", "root"], Forex: ["forex"], Commodities: ["commodity"] };
14010
14149
  const all = this.pool();
14011
14150
  const pool = this.activeTab === "All" ? all : all.filter((s) => TAB_TYPES[this.activeTab]?.includes((s.type ?? "").toLowerCase()) || this.activeTab === "Crypto" && (s.type ?? "").toLowerCase() === "futures");
14012
- return filterSymbols(pool, this.input.value, this.visible, symbolRanking() ? false : TOP_TICKERS);
14151
+ const filtered = filterSymbols(pool, this.input.value, this.visible, symbolRanking() ? false : TOP_TICKERS);
14152
+ this.exhausted = filtered.length < this.visible;
14153
+ const folded = foldGroups(filtered);
14154
+ if (!this.expanded.size) return folded;
14155
+ const keyOf = (s) => `${(s.prefix ?? s.provider ?? "").toLowerCase()}:${s.ticker.toUpperCase()}`;
14156
+ const present = new Set(folded.map(keyOf));
14157
+ const out = [];
14158
+ for (const s of folded) {
14159
+ out.push(s);
14160
+ if (!isGroupRow(s) || !this.expanded.has(groupKeyOf(s))) continue;
14161
+ for (const m of groupMembers(all, s)) {
14162
+ if (present.has(keyOf(m))) continue;
14163
+ present.add(keyOf(m));
14164
+ out.push(m);
14165
+ }
14166
+ }
14167
+ return out;
14013
14168
  }
14014
14169
  refresh() {
14015
14170
  const doc = this.list.ownerDocument;
@@ -14024,19 +14179,20 @@ var SymbolPicker = class {
14024
14179
  this.list.appendChild(empty);
14025
14180
  return;
14026
14181
  }
14027
- for (const s of this.rows) this.list.appendChild(this.rowEl(s));
14182
+ this.rows.forEach((s, i) => this.list.appendChild(this.rowEl(s, i)));
14028
14183
  this.renderHighlight();
14029
14184
  }
14030
14185
  /** Append the page the grown `visible` just uncovered — rows already on screen stay put. */
14031
14186
  grow() {
14032
14187
  const already = this.rows.length;
14033
14188
  this.rows = this.computeRows();
14034
- for (const s of this.rows.slice(already)) this.list.appendChild(this.rowEl(s));
14189
+ this.rows.slice(already).forEach((s, j) => this.list.appendChild(this.rowEl(s, already + j)));
14035
14190
  }
14036
- rowEl(s) {
14191
+ rowEl(s, i) {
14037
14192
  const doc = this.list.ownerDocument;
14038
14193
  const row = doc.createElement("div");
14039
14194
  row.className = "vela-sp-row";
14195
+ row.dataset.i = String(i);
14040
14196
  row.dataset.ticker = s.ticker;
14041
14197
  const venue = s.prefix ?? s.provider;
14042
14198
  if (venue) row.dataset.venue = venue;
@@ -14051,6 +14207,14 @@ var SymbolPicker = class {
14051
14207
  d.textContent = s.description ?? (s.type ?? "");
14052
14208
  main.append(t, d);
14053
14209
  row.append(av, main);
14210
+ if (isGroupRow(s)) {
14211
+ row.dataset.group = "1";
14212
+ const expander = doc.createElement("button");
14213
+ expander.className = "vela-sp-expander";
14214
+ expander.setAttribute("aria-label", "Show contracts");
14215
+ expander.appendChild(iconEl(this.expanded.has(groupKeyOf(s)) ? "chevron-down" : "chevron-right", doc));
14216
+ row.appendChild(expander);
14217
+ } else if (s.group != null && this.expanded.has(groupKeyOf(s))) row.dataset.member = "1";
14054
14218
  if (venue) {
14055
14219
  const badge = doc.createElement("span");
14056
14220
  badge.className = "vela-sp-badge";
@@ -14667,6 +14831,9 @@ var CSS13 = `
14667
14831
  }
14668
14832
  .vela-mb-item:active { background: var(--vela-hover); }
14669
14833
  .vela-mb-item .vela-icon { font-size: 18px; width: 18px; height: 18px; }
14834
+ /* A lit stop (the maximize toggle while something is isolated): the inverse
14835
+ "selected" chip \u2014 white on the dark theme, dark on the light one. */
14836
+ .vela-mb-item.vela-mb-on, .vela-mb-item.vela-mb-on:active { background: var(--vela-selected-bg); color: var(--vela-selected-fg); }
14670
14837
  /* Left-aligned contributed actions get their own stops (the built-in indicators
14671
14838
  slot) \u2014 the wrapper is layout-transparent so each stop flexes like a sibling. */
14672
14839
  .vela-mb-actions { display: contents; }
@@ -14707,9 +14874,11 @@ var MobileBar = class {
14707
14874
  this.actionsHost.className = "vela-mb-actions";
14708
14875
  const onDrawings = opts.onDrawingsClick;
14709
14876
  const drawings = onDrawings ? item("vela-mb-drawings", "Drawings", onDrawings, "pen") : null;
14877
+ const onMaximize = opts.onMaximizeClick;
14878
+ this.maxEl = onMaximize ? item("vela-mb-maximize", "Maximize chart", onMaximize, "maximize") : null;
14710
14879
  const more = item("vela-mb-more", "More", opts.onMoreClick, "kebab");
14711
14880
  const settings = item("vela-mb-settings", "Chart settings", opts.onSettingsClick, "gear");
14712
- this.el.append(this.symbolEl, this.tfEl, ...indicators ? [indicators] : [], this.actionsHost, ...drawings ? [drawings] : [], more, settings);
14881
+ this.el.append(this.symbolEl, this.tfEl, ...indicators ? [indicators] : [], this.actionsHost, ...drawings ? [drawings] : [], ...this.maxEl ? [this.maxEl] : [], more, settings);
14713
14882
  host.appendChild(this.el);
14714
14883
  this.renderActions();
14715
14884
  }
@@ -14741,6 +14910,14 @@ var MobileBar = class {
14741
14910
  setTimeframe(tf) {
14742
14911
  this.tfEl.textContent = timeframeLabel(tf);
14743
14912
  }
14913
+ /** Light the maximize stop while something is isolated (a chart over the grid,
14914
+ * or a maximized pane inside the active chart) — inverse chip + restore glyph. */
14915
+ setMaximizeActive(on) {
14916
+ if (!this.maxEl) return;
14917
+ this.maxEl.classList.toggle("vela-mb-on", on);
14918
+ this.maxEl.setAttribute("aria-label", on ? "Restore layout" : "Maximize chart");
14919
+ this.maxEl.replaceChildren(iconEl(on ? "restore" : "maximize", this.el.ownerDocument));
14920
+ }
14744
14921
  destroy() {
14745
14922
  this.el.remove();
14746
14923
  }
@@ -16918,7 +17095,7 @@ var DrawingToolbar = class {
16918
17095
  this.root.replaceChildren();
16919
17096
  this.groupCells.clear();
16920
17097
  this.groupIcons.clear();
16921
- this.cursorBtn = this.makeButton(CURSOR_ICON, "Cursor", () => this.onArm(null));
17098
+ this.cursorBtn = this.makeButton(CURSOR_ICON, "Cursor", () => this.onCursorClick());
16922
17099
  this.root.appendChild(this.cursorBtn);
16923
17100
  if (this.def.groups.length > 0) this.root.appendChild(this.divider());
16924
17101
  for (const g of this.def.groups) {
@@ -17022,6 +17199,14 @@ var DrawingToolbar = class {
17022
17199
  this.magnetIcon = icon2;
17023
17200
  return cell;
17024
17201
  }
17202
+ /** Cursor returns to select/idle: an active measure/eraser mode exits through its own
17203
+ * toggle callback (disarming a tool via `onArm(null)` alone can't — the host treats a
17204
+ * null arm as a no-op side effect of entering those modes), then the tool disarms. */
17205
+ onCursorClick() {
17206
+ if (this.measureActive) this.onMeasure();
17207
+ if (this.eraserActive) this.onEraser();
17208
+ this.onArm(null);
17209
+ }
17025
17210
  /** Clicking the icon arms the group's last-used tool (it does NOT open the flyout). */
17026
17211
  onGroupIconClick(group) {
17027
17212
  const type = this.lastUsed.get(group.id) ?? group.tools[0]?.type;
@@ -18020,6 +18205,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
18020
18205
  /** Invalidates detached async work (backfill loops, in-flight loads, gap heals):
18021
18206
  * bumped by init(), setMarket() and destroy(). */
18022
18207
  this.generation = 0;
18208
+ /** Aborts the in-flight PROGRESSIVE load's source polling on supersession — an
18209
+ * abandoned stream left polling to its own budget starves the browser's per-host
18210
+ * connection pool, and the NEXT symbol's very first fetch with it (measured). */
18211
+ this.progressiveAbort = null;
18023
18212
  /** Awaiters racing a superseded load (setMarket callers) — released on every bump so they never hang. */
18024
18213
  this.supersedeWaiters = [];
18025
18214
  /** `history:complete` fired for the CURRENT load. Each market load re-arms the cycle
@@ -18162,6 +18351,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
18162
18351
  * superseded setMarket awaiters so their promises resolve instead of hanging. */
18163
18352
  bumpGeneration() {
18164
18353
  const gen = ++this.generation;
18354
+ this.progressiveAbort?.abort();
18355
+ this.progressiveAbort = null;
18165
18356
  for (const w of this.supersedeWaiters.splice(0)) w();
18166
18357
  return gen;
18167
18358
  }
@@ -18215,7 +18406,47 @@ var EngineOrchestrator = class _EngineOrchestrator {
18215
18406
  const requested = market.bars ?? 500;
18216
18407
  const initialRange = market.visibleRange;
18217
18408
  const deep = !market.data?.length && initialRange == null && requested > SINGLE_LOAD_BARS;
18218
- if (deep && this.feed.loadRange) {
18409
+ let progressiveServed = false;
18410
+ if (!market.data?.length && initialRange == null && this.feed.loadProgressive) {
18411
+ let painted = false;
18412
+ const paint = (bars, final) => {
18413
+ if (this.generation !== gen || !final && bars.length === 0) return;
18414
+ this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
18415
+ if (!painted && bars.length > 0) {
18416
+ painted = true;
18417
+ if (opts.firstLoad) this.activateBarLayers();
18418
+ if (!final) this.historyState = "backfill";
18419
+ }
18420
+ };
18421
+ const abort = new AbortController();
18422
+ this.progressiveAbort = abort;
18423
+ progressiveServed = await new Promise((firstPaint) => {
18424
+ let signaled = false;
18425
+ const signal = (served) => {
18426
+ if (!signaled) {
18427
+ signaled = true;
18428
+ firstPaint(served);
18429
+ }
18430
+ };
18431
+ abort.signal.addEventListener("abort", () => signal(true), { once: true });
18432
+ this.feed.loadProgressive(market, (bars) => {
18433
+ paint(bars, false);
18434
+ if (painted) signal(true);
18435
+ }, { signal: abort.signal }).then((full) => {
18436
+ if (this.progressiveAbort === abort) this.progressiveAbort = null;
18437
+ if (full == null) return signal(false);
18438
+ if (this.generation !== gen) return signal(true);
18439
+ paint(full, true);
18440
+ this.completeHistory(full.length >= requested ? "depth" : "genesis");
18441
+ signal(true);
18442
+ }).catch(() => {
18443
+ if (this.progressiveAbort === abort) this.progressiveAbort = null;
18444
+ if (this.generation === gen) this.completeHistory("aborted");
18445
+ signal(true);
18446
+ });
18447
+ });
18448
+ }
18449
+ if (progressiveServed) ; else if (deep && this.feed.loadRange) {
18219
18450
  const head = await this.feed.load({ ...market, bars: Math.min(requested, CHUNK_BARS) });
18220
18451
  if (this.generation !== gen) return;
18221
18452
  this.setBarSeries(head);
@@ -20958,6 +21189,9 @@ var CLOSE_SVG = iconAt("close", LEGEND_ICON_PX2);
20958
21189
  var FOLD_SVG = iconAt("chevron-up", LEGEND_ICON_PX2);
20959
21190
  var UNFOLD_SVG = iconAt("chevron-down", LEGEND_ICON_PX2);
20960
21191
  var OVERVIEW_SVG = iconAt("objects", LEGEND_ICON_PX2);
21192
+ function legendCalloutsDisplay(open2, hasCallouts) {
21193
+ return !open2 && hasCallouts ? "inline-flex" : "none";
21194
+ }
20961
21195
  var InputsUI = class {
20962
21196
  constructor(container, theme, paneBoundsOf) {
20963
21197
  this.container = container;
@@ -21161,7 +21395,7 @@ var InputsUI = class {
21161
21395
  row.callouts = [];
21162
21396
  row.calloutsEl.replaceChildren();
21163
21397
  const views = this.legendCallouts?.(row.id) ?? [];
21164
- row.calloutsEl.style.display = views.length > 0 ? "inline-flex" : "none";
21398
+ row.calloutsEl.style.display = legendCalloutsDisplay(row.highlighted, views.length > 0);
21165
21399
  for (const view of views) {
21166
21400
  const bubble = new CalloutBubble({
21167
21401
  icon: view.icon,
@@ -21645,11 +21879,11 @@ var InputsUI = class {
21645
21879
  row.controlsEl.style.display = open2 || row.hidden ? "inline-flex" : "none";
21646
21880
  if (open2) {
21647
21881
  row.el.appendChild(row.statusEl);
21648
- row.el.appendChild(row.calloutsEl);
21882
+ for (const bubble of row.callouts) bubble.hidePanel();
21649
21883
  } else {
21650
21884
  row.el.insertBefore(row.statusEl, row.valuesEl);
21651
- row.el.insertBefore(row.calloutsEl, row.statusEl);
21652
21885
  }
21886
+ row.calloutsEl.style.display = legendCalloutsDisplay(open2, row.callouts.length > 0);
21653
21887
  for (const child of Array.from(row.controlsEl.children)) {
21654
21888
  if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
21655
21889
  if (child === row.extrasEl) {
@@ -21739,7 +21973,7 @@ var ICONS = {
21739
21973
  };
21740
21974
  var STYLE_ID21 = "vela-pane-controls";
21741
21975
  var ICON_PX = 12;
21742
- var CLUSTER_PILL = "rgba(0,0,0,0.28)";
21976
+ var CLUSTER_PILL = "rgba(0,0,0,0.65)";
21743
21977
  function ensureStyles3() {
21744
21978
  if (typeof document === "undefined" || document.getElementById(STYLE_ID21)) return;
21745
21979
  const st = document.createElement("style");
@@ -21759,8 +21993,12 @@ var PaneControls = class {
21759
21993
  this.deps = deps;
21760
21994
  this.clusters = /* @__PURE__ */ new Map();
21761
21995
  this.hoverPaneId = null;
21996
+ /** Mobile: hover clusters are meaningless without a cursor — suppressed; a
21997
+ * collapsed pane's standalone expand chip stays (the only way back up). */
21998
+ this.suspended = false;
21762
21999
  /** Reveal the cluster for the pane under the cursor, resolved from the pointer's y in the plot. */
21763
22000
  this.onPlotMove = (e) => {
22001
+ if (this.suspended) return;
21764
22002
  const rect = this.plot.getBoundingClientRect();
21765
22003
  const y = e.clientY - rect.top;
21766
22004
  let hit = null;
@@ -21832,7 +22070,12 @@ var PaneControls = class {
21832
22070
  }
21833
22071
  if (p.count > 1) {
21834
22072
  cluster.appendChild(
21835
- this.button(p.maximized ? ICONS.restore : ICONS.maximize, p.maximized ? "Restore pane" : "Maximize pane", false, () => this.deps.onToggleMaximize(p.id), { role: "maximize" })
22073
+ this.button(p.maximized ? ICONS.restore : ICONS.maximize, p.maximized ? "Restore pane" : "Maximize pane", false, () => this.deps.onToggleMaximize(p.id), {
22074
+ role: "maximize",
22075
+ // Same inverse-chip treatment as the collapsed pane's expand toggle: the
22076
+ // maximized state must read as an active state, not just a swapped glyph.
22077
+ selected: p.maximized
22078
+ })
21836
22079
  );
21837
22080
  }
21838
22081
  }
@@ -21869,17 +22112,18 @@ var PaneControls = class {
21869
22112
  }
21870
22113
  const hovered = id === this.hoverPaneId;
21871
22114
  const hasButtons = cluster.children.length > 0;
21872
- const visible = hasButtons && (hovered || p.collapsed) && p.height > 8;
22115
+ const stateChipRole = p.collapsed ? "collapse" : !this.suspended && p.maximized ? "maximize" : null;
22116
+ const visible = hasButtons && (hovered || stateChipRole != null) && p.height > 8;
21873
22117
  cluster.style.right = `${rightPx}px`;
21874
22118
  cluster.style.top = p.collapsed ? `${p.top + Math.max(1, Math.round((p.height - 24) / 2))}px` : `${p.top + 4}px`;
21875
22119
  cluster.style.display = visible ? "flex" : "none";
21876
22120
  if (!visible) continue;
21877
- const soloExpand = p.collapsed && !hovered;
21878
- cluster.style.background = soloExpand ? "transparent" : CLUSTER_PILL;
22121
+ const soloChip = stateChipRole != null && !hovered;
22122
+ cluster.style.background = soloChip ? "transparent" : CLUSTER_PILL;
21879
22123
  for (const child of cluster.children) {
21880
22124
  const btn2 = child;
21881
22125
  btn2.style.display = "inline-flex";
21882
- btn2.style.visibility = soloExpand && btn2.dataset.role !== "collapse" ? "hidden" : "visible";
22126
+ btn2.style.visibility = soloChip && btn2.dataset.role !== stateChipRole ? "hidden" : "visible";
21883
22127
  }
21884
22128
  }
21885
22129
  }
@@ -21889,6 +22133,14 @@ var PaneControls = class {
21889
22133
  this.hoverPaneId = paneId;
21890
22134
  this.reposition();
21891
22135
  }
22136
+ /** Mobile suppression: no hover clusters (touch has no cursor; the shell's own
22137
+ * chrome covers maximize), while collapsed panes keep their expand chips. */
22138
+ setSuspended(on) {
22139
+ if (on === this.suspended) return;
22140
+ this.suspended = on;
22141
+ if (on) this.hoverPaneId = null;
22142
+ this.reposition();
22143
+ }
21892
22144
  destroy() {
21893
22145
  this.plot.removeEventListener("pointermove", this.onPlotMove);
21894
22146
  this.plot.removeEventListener("pointerleave", this.onPlotLeave);
@@ -22039,160 +22291,6 @@ var AxisScaleButtons = class {
22039
22291
  }
22040
22292
  };
22041
22293
 
22042
- // src/renderers/shared/TableOverlay.ts
22043
- var SIZE_PX3 = {
22044
- auto: 13,
22045
- tiny: 10,
22046
- small: 11,
22047
- normal: 13,
22048
- large: 16,
22049
- huge: 20
22050
- };
22051
- function fontPxOf(size) {
22052
- if (typeof size === "number") return size > 0 ? size : SIZE_PX3.auto;
22053
- return SIZE_PX3[size] ?? SIZE_PX3.auto;
22054
- }
22055
- function tableHasContent(t) {
22056
- return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
22057
- }
22058
- function mergeRenderPlan(t) {
22059
- const span = /* @__PURE__ */ new Map();
22060
- const omit = /* @__PURE__ */ new Set();
22061
- for (const m of t.merges) {
22062
- span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
22063
- for (let r = m.startRow; r <= m.endRow; r += 1) {
22064
- for (let c = m.startCol; c <= m.endCol; c += 1) {
22065
- if (r !== m.startRow || c !== m.startCol) omit.add(`${r}:${c}`);
22066
- }
22067
- }
22068
- }
22069
- for (let r = 0; r < t.rows; r += 1) {
22070
- for (let c = 0; c < t.columns; c += 1) {
22071
- if (t.cells[r]?.[c]?.merged && !span.has(`${r}:${c}`)) omit.add(`${r}:${c}`);
22072
- }
22073
- }
22074
- for (const key of span.keys()) omit.delete(key);
22075
- return { span, omit };
22076
- }
22077
- var TableOverlay = class {
22078
- constructor(container, theme, paneBounds) {
22079
- this.container = container;
22080
- this.theme = theme;
22081
- this.paneBounds = paneBounds;
22082
- this.lastTables = [];
22083
- if (getComputedStyle(container).position === "static") container.style.position = "relative";
22084
- this.root = document.createElement("div");
22085
- Object.assign(this.root.style, {
22086
- position: "absolute",
22087
- inset: "0",
22088
- pointerEvents: "none",
22089
- overflow: "hidden",
22090
- zIndex: "3"
22091
- });
22092
- container.appendChild(this.root);
22093
- }
22094
- update(tables) {
22095
- this.lastTables = tables;
22096
- this.root.replaceChildren();
22097
- for (const t of tables) {
22098
- if (tableHasContent(t)) this.root.appendChild(this.renderTable(t));
22099
- }
22100
- }
22101
- /** Re-render at the current pane geometry — after layout settles or on resize. */
22102
- reposition() {
22103
- if (this.root.isConnected) this.update(this.lastTables);
22104
- }
22105
- /** Show/hide the whole overlay. Tables anchor to pane corners, not to bars, so unlike the
22106
- * series content they DON'T vanish with an emptied chart — the loading state hides them. */
22107
- setVisible(visible) {
22108
- this.root.style.display = visible ? "" : "none";
22109
- }
22110
- destroy() {
22111
- this.root.remove();
22112
- }
22113
- renderTable(t) {
22114
- const b = this.paneBounds(t.paneId);
22115
- const wrap = document.createElement("div");
22116
- wrap.style.position = "absolute";
22117
- if (t.frameColor && t.frameWidth > 0) wrap.style.border = `${t.frameWidth}px solid ${t.frameColor}`;
22118
- this.anchor(wrap, t.position, b);
22119
- const table = document.createElement("table");
22120
- Object.assign(table.style, {
22121
- borderCollapse: "collapse",
22122
- background: t.bgColor ?? "transparent",
22123
- fontFamily: this.theme.fontFamily || "sans-serif",
22124
- border: "none",
22125
- tableLayout: "auto",
22126
- // Re-enable pointer events on the table only (root is none) so cell tooltips work.
22127
- pointerEvents: "auto"
22128
- });
22129
- const { span, omit } = mergeRenderPlan(t);
22130
- const plotW = Math.max(0, (this.root.clientWidth || this.container.clientWidth) - b.rightAxis);
22131
- const paneH = b.height;
22132
- const cellBorder = t.borderColor && t.borderWidth > 0 ? `${t.borderWidth}px solid ${t.borderColor}` : "none";
22133
- for (let r = 0; r < t.rows; r += 1) {
22134
- const tr = document.createElement("tr");
22135
- for (let c = 0; c < t.columns; c += 1) {
22136
- if (omit.has(`${r}:${c}`)) continue;
22137
- const cell = t.cells[r]?.[c] ?? null;
22138
- const td = document.createElement("td");
22139
- if (cell === null) {
22140
- Object.assign(td.style, { padding: "0", border: "none" });
22141
- tr.appendChild(td);
22142
- continue;
22143
- }
22144
- const sp = span.get(`${r}:${c}`);
22145
- if (sp) {
22146
- if (sp.cs > 1) td.colSpan = sp.cs;
22147
- if (sp.rs > 1) td.rowSpan = sp.rs;
22148
- }
22149
- Object.assign(td.style, {
22150
- border: cellBorder,
22151
- padding: "2px 6px",
22152
- background: cell.bgColor ?? "transparent",
22153
- color: cell.textColor ?? this.theme.textColor,
22154
- textAlign: cell.hAlign,
22155
- verticalAlign: cell.vAlign === "top" ? "top" : cell.vAlign === "bottom" ? "bottom" : "middle",
22156
- fontSize: `${fontPxOf(cell.textSize)}px`,
22157
- fontFamily: cell.fontFamily === "monospace" ? "monospace" : "inherit",
22158
- fontWeight: cell.bold ? "bold" : "normal",
22159
- fontStyle: cell.italic ? "italic" : "normal",
22160
- // Pine cell text never wraps; `\n` still breaks lines. Wrapping
22161
- // used to collapse unicode sparklines and ━━━ dividers.
22162
- whiteSpace: "pre"
22163
- });
22164
- if (cell.width) td.style.width = `${cell.width / 100 * plotW}px`;
22165
- if (cell.height) td.style.height = `${cell.height / 100 * paneH}px`;
22166
- if (cell.tooltip) td.title = cell.tooltip;
22167
- td.textContent = cell.text ?? "";
22168
- tr.appendChild(td);
22169
- }
22170
- table.appendChild(tr);
22171
- }
22172
- wrap.appendChild(table);
22173
- return wrap;
22174
- }
22175
- /**
22176
- * Position the wrapper at a Pine `position.*` corner/edge of the table's PANE
22177
- * (not the whole chart), inset past the right price axis so it never overlaps
22178
- * the Y-axis labels.
22179
- */
22180
- anchor(el, position, b) {
22181
- const m = 6;
22182
- const containerH = this.root.clientHeight || this.container.clientHeight;
22183
- const paneBottom = b.top + b.height;
22184
- if (position.startsWith("top")) el.style.top = `${b.top + m}px`;
22185
- else if (position.startsWith("bottom")) el.style.bottom = `${Math.max(0, containerH - paneBottom) + m}px`;
22186
- else el.style.top = `${b.top + b.height / 2}px`;
22187
- if (position.endsWith("left")) el.style.left = `${m}px`;
22188
- else if (position.endsWith("right")) el.style.right = `${b.rightAxis + m}px`;
22189
- else el.style.left = `calc(50% - ${b.rightAxis / 2}px)`;
22190
- const tx = position.endsWith("center") ? "-50%" : "0";
22191
- const ty = position.startsWith("middle") ? "-50%" : "0";
22192
- if (tx !== "0" || ty !== "0") el.style.transform = `translate(${tx}, ${ty})`;
22193
- }
22194
- };
22195
-
22196
22294
  // src/renderers/native/capabilities.ts
22197
22295
  var NATIVE_CAPABILITIES = {
22198
22296
  panes: true,
@@ -22213,7 +22311,7 @@ var NATIVE_CAPABILITIES = {
22213
22311
  drawingDepth: true,
22214
22312
  // drawings share the series' z space (backend-composited interleave layers)
22215
22313
  tables: true,
22216
- // reuses the DOM TableOverlay
22314
+ // canvas-painted into the owning indicator's interleave slice
22217
22315
  trades: true,
22218
22316
  // strategy order-fill markers (arrows + labels + fill-price ticks)
22219
22317
  inputsUI: true
@@ -22244,6 +22342,9 @@ function candleTier(spacing) {
22244
22342
  if (spacing < CANDLE_BODY_MIN_SPACING) return "wick";
22245
22343
  return "full";
22246
22344
  }
22345
+ function snapY(yCss, dpr) {
22346
+ return Math.round(yCss * dpr) / dpr;
22347
+ }
22247
22348
  function candleGeometry(xCss, spacing, dpr, bodyScale = 1) {
22248
22349
  const wickDev = Math.max(1, Math.round(wickWidth(spacing) * dpr));
22249
22350
  const wickLeftDev = Math.round(xCss * dpr - wickDev / 2);
@@ -22767,11 +22868,9 @@ var WebGL2Backend = class {
22767
22868
  };
22768
22869
  b.alpha = this.modelAlpha;
22769
22870
  for (const m of models) for (const bgSpan of m.backgrounds) if (bgSpan.overlay !== true) this.emitBackground(b, bgSpan, pane, coords);
22770
- 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));
22771
22871
  if (isPrice) {
22772
22872
  for (const m of scene.indicators.values()) {
22773
22873
  for (const bgSpan of m.backgrounds) if (bgSpan.overlay === true) this.emitBackground(b, bgSpan, pane, coords);
22774
- for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, i1, scene.offsetOf(m.id));
22775
22874
  }
22776
22875
  }
22777
22876
  const drawCandles = isPrice && !scene.candlesHidden;
@@ -22787,6 +22886,7 @@ var WebGL2Backend = class {
22787
22886
  b.alpha = this.modelAlpha;
22788
22887
  const off = scene.offsetOf(m.id);
22789
22888
  const mp = effPane(m);
22889
+ for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, mp, coords, i0, i1, off);
22790
22890
  for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
22791
22891
  }
22792
22892
  if (drawCandles && !candleDrawn) {
@@ -22794,14 +22894,18 @@ var WebGL2Backend = class {
22794
22894
  b.alpha = this.candleStructureAlpha;
22795
22895
  this.emitPriceSeries(b, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
22796
22896
  }
22797
- drawSlicesUpTo(Infinity);
22798
22897
  b.alpha = this.modelAlpha;
22799
22898
  if (isPrice) {
22899
+ for (const m of scene.indicators.values()) {
22900
+ const off = scene.offsetOf(m.id);
22901
+ for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, i1, off);
22902
+ }
22800
22903
  for (const m of scene.indicators.values()) {
22801
22904
  const off = scene.offsetOf(m.id);
22802
22905
  for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0, i1, theme, off);
22803
22906
  }
22804
22907
  }
22908
+ drawSlicesUpTo(Infinity);
22805
22909
  for (const m of models) {
22806
22910
  const mp = effPane(m);
22807
22911
  for (const pl of m.priceLines) this.emitHline(b, pl, mp, coords, dataW, theme);
@@ -23196,12 +23300,12 @@ var WebGL2Backend = class {
23196
23300
  if (drawBody) {
23197
23301
  const oY = coords.priceToY(bar.open, pane.scale, pane.bounds);
23198
23302
  const cY = coords.priceToY(bar.close, pane.scale, pane.bounds);
23199
- bodyTop = Math.min(oY, cY);
23200
- bodyH = Math.max(1, Math.abs(cY - oY));
23303
+ bodyTop = snapY(Math.min(oY, cY), coords.dpr);
23304
+ bodyH = Math.max(1 / coords.dpr, snapY(Math.max(oY, cY), coords.dpr) - bodyTop);
23201
23305
  }
23202
23306
  if (cs.wickVisible) {
23203
- const hY = coords.priceToY(bar.high, pane.scale, pane.bounds);
23204
- const lY = coords.priceToY(bar.low, pane.scale, pane.bounds);
23307
+ const hY = snapY(coords.priceToY(bar.high, pane.scale, pane.bounds), coords.dpr);
23308
+ const lY = snapY(coords.priceToY(bar.low, pane.scale, pane.bounds), coords.dpr);
23205
23309
  b.alpha = this.candleStructureAlpha;
23206
23310
  const wCol = parseColor((isUp ? cs.wickUpColor : cs.wickDownColor) ?? (drawBody ? dir : bodyColorStr));
23207
23311
  if (drawBody) {
@@ -24534,9 +24638,10 @@ var SceneGraph = class {
24534
24638
  * so each indicator arrives behind the candles (and behind older indicators);
24535
24639
  * `setIndicatorZ`/`bringToFront`/`sendToBack` change it. */
24536
24640
  this.seriesZ = /* @__PURE__ */ new Map();
24537
- /** Per-pane raster layers of user drawings interleaved into the series stack each is a
24641
+ /** Per-pane raster layers of drawings interleaved into the series stack (each
24642
+ * indicator's Pine drawings at its model's z, plus in-stack user drawings) — each a
24538
24643
  * prepainted canvas the backend composites just before the series carrying `beforeZ`.
24539
- * Rebuilt by the renderer per data frame; empty when every drawing sits over the stack. */
24644
+ * Rebuilt by the renderer per data frame. */
24540
24645
  this.drawingSlices = /* @__PURE__ */ new Map();
24541
24646
  /** Per-model index offset: the chart bar index of the model's `anchorTime` — its
24542
24647
  * index-aligned payloads (dense series arrays, `bar_index` drawings) count from that
@@ -24771,11 +24876,9 @@ var Canvas2dBackend = class {
24771
24876
  };
24772
24877
  ctx.globalAlpha = this.modelAlpha;
24773
24878
  for (const m of models) for (const bg of m.backgrounds) if (bg.overlay !== true) this.drawBackground(ctx, bg, pane, coords);
24774
- 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));
24775
24879
  if (isPrice) {
24776
24880
  for (const m of scene.indicators.values()) {
24777
24881
  for (const bg of m.backgrounds) if (bg.overlay === true) this.drawBackground(ctx, bg, pane, coords);
24778
- for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, scene.offsetOf(m.id));
24779
24882
  }
24780
24883
  }
24781
24884
  const slices = scene.drawingSlices.get(pane.id) ?? [];
@@ -24799,6 +24902,7 @@ var Canvas2dBackend = class {
24799
24902
  ctx.globalAlpha = this.modelAlpha;
24800
24903
  const off = scene.offsetOf(m.id);
24801
24904
  const mp = effPane(m);
24905
+ for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, mp, coords, i0, i1, off);
24802
24906
  for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
24803
24907
  }
24804
24908
  if (drawCandles && !candleDrawn) {
@@ -24806,14 +24910,18 @@ var Canvas2dBackend = class {
24806
24910
  ctx.globalAlpha = this.candleStructureAlpha;
24807
24911
  this.drawPriceSeries(ctx, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
24808
24912
  }
24809
- drawSlicesUpTo(Infinity);
24810
24913
  if (isPrice) {
24811
24914
  ctx.globalAlpha = this.modelAlpha;
24915
+ for (const m of scene.indicators.values()) {
24916
+ const off = scene.offsetOf(m.id);
24917
+ for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, off);
24918
+ }
24812
24919
  for (const m of scene.indicators.values()) {
24813
24920
  const off = scene.offsetOf(m.id);
24814
24921
  for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0, i1, theme, off);
24815
24922
  }
24816
24923
  }
24924
+ drawSlicesUpTo(Infinity);
24817
24925
  ctx.globalAlpha = this.modelAlpha;
24818
24926
  for (const m of models) {
24819
24927
  const mp = effPane(m);
@@ -25057,13 +25165,13 @@ var Canvas2dBackend = class {
25057
25165
  if (drawBody) {
25058
25166
  const oY = coords.priceToY(b.open, pane.scale, pane.bounds);
25059
25167
  const cY = coords.priceToY(b.close, pane.scale, pane.bounds);
25060
- top = Math.min(oY, cY);
25061
- bodyH = Math.max(1, Math.abs(cY - oY));
25168
+ top = snapY(Math.min(oY, cY), coords.dpr);
25169
+ bodyH = Math.max(1 / coords.dpr, snapY(Math.max(oY, cY), coords.dpr) - top);
25062
25170
  }
25063
25171
  if (cs.wickVisible) {
25064
25172
  const wick = (up ? cs.wickUpColor : cs.wickDownColor) ?? (drawBody ? dir : color);
25065
- const hY = coords.priceToY(b.high, pane.scale, pane.bounds);
25066
- const lY = coords.priceToY(b.low, pane.scale, pane.bounds);
25173
+ const hY = snapY(coords.priceToY(b.high, pane.scale, pane.bounds), coords.dpr);
25174
+ const lY = snapY(coords.priceToY(b.low, pane.scale, pane.bounds), coords.dpr);
25067
25175
  ctx.globalAlpha = this.candleStructureAlpha;
25068
25176
  ctx.strokeStyle = wick;
25069
25177
  ctx.lineWidth = g.wickW;
@@ -25510,6 +25618,19 @@ function autoFontSize(lines, boxW, boxH, bold) {
25510
25618
 
25511
25619
  // src/renderers/shared/DrawingSceneRenderer.ts
25512
25620
  var EMPTY_DRAWING_SET = { lines: [], boxes: [], labels: [], polylines: [], linefills: [] };
25621
+ function modelDrawingSet(m, overlay) {
25622
+ const want = (d) => Boolean(d.overlay) === overlay;
25623
+ return {
25624
+ lines: (m.lines ?? []).filter(want),
25625
+ boxes: (m.boxes ?? []).filter(want),
25626
+ labels: (m.labels ?? []).filter(want),
25627
+ polylines: (m.polylines ?? []).filter(want),
25628
+ linefills: (m.linefills ?? []).filter(want)
25629
+ };
25630
+ }
25631
+ function drawingSetEmpty(s) {
25632
+ return !s.lines.length && !s.boxes.length && !s.labels.length && !s.polylines.length && !s.linefills.length;
25633
+ }
25513
25634
  function fontSizePx(size) {
25514
25635
  return size === "auto" ? 12 : namedFontSize(size);
25515
25636
  }
@@ -26362,10 +26483,8 @@ var ChromeRenderer = class {
26362
26483
  this.ctx = null;
26363
26484
  // The color for axis tick labels — the host-passed surface text, set each frame in render().
26364
26485
  this.axisTextColor = DARK_THEME.textColor;
26365
- // Shared Pine-drawing renderer (line/box/label/polyline/linefill); widthCache persists.
26486
+ // Shared Pine-drawing renderer, used here for autoscale geometry only; widthCache persists.
26366
26487
  this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
26367
- // Tooltip hit-rects of every label drawn this frame, in plot coords (rebuilt per render).
26368
- this.labelTips = [];
26369
26488
  }
26370
26489
  mount(canvas) {
26371
26490
  this.canvas = canvas;
@@ -26389,8 +26508,8 @@ var ChromeRenderer = class {
26389
26508
  */
26390
26509
  paneDrawingsRange(ownModels, scene, isPricePane, vr) {
26391
26510
  let dr = null;
26392
- for (const m of ownModels) dr = unionRange(dr, this.drawingsRange(this.ownDrawings(m), vr, scene.offsetOf(m.id)));
26393
- if (isPricePane) for (const m of scene.indicators.values()) dr = unionRange(dr, this.drawingsRange(this.overlayDrawings(m), vr, scene.offsetOf(m.id)));
26511
+ for (const m of ownModels) dr = unionRange(dr, this.drawingsRange(modelDrawingSet(m, false), vr, scene.offsetOf(m.id)));
26512
+ if (isPricePane) for (const m of scene.indicators.values()) dr = unionRange(dr, this.drawingsRange(modelDrawingSet(m, true), vr, scene.offsetOf(m.id)));
26394
26513
  return dr;
26395
26514
  }
26396
26515
  /** Clear the chrome canvas and draw drawings + axes + current-price line.
@@ -26407,7 +26526,6 @@ var ChromeRenderer = class {
26407
26526
  const dataW = coords.width;
26408
26527
  const dataH = coords.height;
26409
26528
  this.axisTextColor = surface?.textColor ?? theme.textColor;
26410
- this.labelTips = [];
26411
26529
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
26412
26530
  ctx.clearRect(0, 0, fullW, fullH);
26413
26531
  if (surface && (fullW > dataW || fullH > dataH)) {
@@ -26423,17 +26541,6 @@ var ChromeRenderer = class {
26423
26541
  return;
26424
26542
  }
26425
26543
  const pricePane = panes.find((p) => p.kind === "price") ?? null;
26426
- for (const pane of panes) {
26427
- if (pane.collapsed) continue;
26428
- for (const m of scene.indicatorsForPane(pane.id)) {
26429
- const sc = scene.scaleFor(m, pane);
26430
- const mp = sc === pane.scale ? pane : { ...pane, scale: sc };
26431
- this.renderDrawings(ctx, coords, this.ownDrawings(m), mp, dataW, scene.offsetOf(m.id));
26432
- }
26433
- }
26434
- if (pricePane) {
26435
- for (const m of scene.indicators.values()) this.renderDrawings(ctx, coords, this.overlayDrawings(m), pricePane, dataW, scene.offsetOf(m.id));
26436
- }
26437
26544
  if (pricePane && !pricePane.collapsed && scene.tradeMarkers.visible) {
26438
26545
  for (const m of scene.indicators.values()) {
26439
26546
  if (m.trades?.length) this.renderTrades(ctx, coords, scene, theme, m.trades, pricePane, dataW);
@@ -26449,25 +26556,6 @@ var ChromeRenderer = class {
26449
26556
  this.canvas = null;
26450
26557
  this.ctx = null;
26451
26558
  }
26452
- // ── Pine-drawing helpers (own vs force_overlay routing) ──
26453
- ownDrawings(m) {
26454
- return {
26455
- lines: (m.lines ?? []).filter((d) => !d.overlay),
26456
- boxes: (m.boxes ?? []).filter((d) => !d.overlay),
26457
- labels: (m.labels ?? []).filter((d) => !d.overlay),
26458
- polylines: (m.polylines ?? []).filter((d) => !d.overlay),
26459
- linefills: (m.linefills ?? []).filter((d) => !d.overlay)
26460
- };
26461
- }
26462
- overlayDrawings(m) {
26463
- return {
26464
- lines: (m.lines ?? []).filter((d) => d.overlay),
26465
- boxes: (m.boxes ?? []).filter((d) => d.overlay),
26466
- labels: (m.labels ?? []).filter((d) => d.overlay),
26467
- polylines: (m.polylines ?? []).filter((d) => d.overlay),
26468
- linefills: (m.linefills ?? []).filter((d) => d.overlay)
26469
- };
26470
- }
26471
26559
  drawingsRange(set, vr, indexOffset = 0) {
26472
26560
  this.drawScene.setSet(set, indexOffset);
26473
26561
  if (this.drawScene.isEmpty()) return null;
@@ -26501,34 +26589,6 @@ var ChromeRenderer = class {
26501
26589
  );
26502
26590
  ctx.restore();
26503
26591
  }
26504
- renderDrawings(ctx, coords, set, pane, dataW, indexOffset = 0) {
26505
- this.drawScene.setSet(set, indexOffset);
26506
- if (this.drawScene.isEmpty()) return;
26507
- ctx.save();
26508
- ctx.translate(0, pane.bounds.top);
26509
- ctx.beginPath();
26510
- ctx.rect(0, 0, dataW, pane.bounds.height);
26511
- ctx.clip();
26512
- this.drawScene.render(
26513
- ctx,
26514
- dataW,
26515
- pane.bounds.height,
26516
- (l) => coords.logicalToX(l),
26517
- (price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
26518
- );
26519
- ctx.restore();
26520
- for (const r of this.drawScene.labelTipRegions()) {
26521
- this.labelTips.push({ ...r, top: r.top + pane.bounds.top, bottom: r.bottom + pane.bounds.top });
26522
- }
26523
- }
26524
- /** Tooltip of the topmost label under a plot-space point, or null. Fed by the last render. */
26525
- labelTooltipAt(x, y) {
26526
- for (let i = this.labelTips.length - 1; i >= 0; i -= 1) {
26527
- const r = this.labelTips[i];
26528
- if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return r.text;
26529
- }
26530
- return null;
26531
- }
26532
26592
  // ── axes ──
26533
26593
  drawPriceAxes(ctx, scene, coords, theme, dataW, panes) {
26534
26594
  ctx.strokeStyle = scene.style.borderColor ?? theme.borderColor;
@@ -30847,17 +30907,17 @@ function glyphIcon(glyph) {
30847
30907
  return textGlyph(String(glyph), 15);
30848
30908
  }
30849
30909
  function stampSizeIcon(size) {
30850
- return textGlyph("\u25CF", (SIZE_PX4[String(size)] ?? 13) + 4);
30910
+ return textGlyph("\u25CF", (SIZE_PX3[String(size)] ?? 13) + 4);
30851
30911
  }
30852
30912
  function sizeIcon(size) {
30853
30913
  return textGlyph(String(size).charAt(0).toUpperCase(), 15);
30854
30914
  }
30855
- var SIZE_PX4 = { small: 10, normal: 13, large: 16, huge: 20 };
30915
+ var SIZE_PX3 = { small: 10, normal: 13, large: 16, huge: 20 };
30856
30916
  function numbersSizeIcon(size) {
30857
- return textGlyph("12", (SIZE_PX4[String(size)] ?? 13) - 1, 16.5, 'font-weight="600"');
30917
+ return textGlyph("12", (SIZE_PX3[String(size)] ?? 13) - 1, 16.5, 'font-weight="600"');
30858
30918
  }
30859
30919
  function labelSizeIcon(size) {
30860
- return textGlyph("T", (SIZE_PX4[String(size)] ?? 13) + 2);
30920
+ return textGlyph("T", (SIZE_PX3[String(size)] ?? 13) + 2);
30861
30921
  }
30862
30922
  function capitalize(s) {
30863
30923
  return s.charAt(0).toUpperCase() + s.slice(1);
@@ -31832,6 +31892,315 @@ var UserDrawingController = class {
31832
31892
  }
31833
31893
  };
31834
31894
 
31895
+ // src/renderers/shared/TableOverlay.ts
31896
+ var SIZE_PX4 = {
31897
+ auto: 13,
31898
+ tiny: 10,
31899
+ small: 11,
31900
+ normal: 13,
31901
+ large: 16,
31902
+ huge: 20
31903
+ };
31904
+ function fontPxOf(size) {
31905
+ if (typeof size === "number") return size > 0 ? size : SIZE_PX4.auto;
31906
+ return SIZE_PX4[size] ?? SIZE_PX4.auto;
31907
+ }
31908
+ function tableHasContent(t) {
31909
+ return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
31910
+ }
31911
+ function mergeRenderPlan(t) {
31912
+ const span = /* @__PURE__ */ new Map();
31913
+ const omit = /* @__PURE__ */ new Set();
31914
+ for (const m of t.merges) {
31915
+ span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
31916
+ for (let r = m.startRow; r <= m.endRow; r += 1) {
31917
+ for (let c = m.startCol; c <= m.endCol; c += 1) {
31918
+ if (r !== m.startRow || c !== m.startCol) omit.add(`${r}:${c}`);
31919
+ }
31920
+ }
31921
+ }
31922
+ for (let r = 0; r < t.rows; r += 1) {
31923
+ for (let c = 0; c < t.columns; c += 1) {
31924
+ if (t.cells[r]?.[c]?.merged && !span.has(`${r}:${c}`)) omit.add(`${r}:${c}`);
31925
+ }
31926
+ }
31927
+ for (const key of span.keys()) omit.delete(key);
31928
+ return { span, omit };
31929
+ }
31930
+
31931
+ // src/renderers/shared/TableCanvasRenderer.ts
31932
+ var PAD_X = 6;
31933
+ var PAD_Y = 2;
31934
+ var MARGIN = 6;
31935
+ var LINE_HEIGHT = 1.2;
31936
+ function paintTable(ctx, t, args, tips) {
31937
+ if (!tableHasContent(t)) return;
31938
+ const layout = layoutTable(ctx, t, args);
31939
+ if (!layout || layout.w <= 0 || layout.h <= 0) return;
31940
+ const fw = t.frameColor && t.frameWidth > 0 ? t.frameWidth : 0;
31941
+ const { x, y } = anchorOrigin(t.position, layout.w + 2 * fw, layout.h + 2 * fw, args);
31942
+ const x0 = x + fw;
31943
+ const y0 = y + fw;
31944
+ if (t.bgColor) {
31945
+ ctx.fillStyle = t.bgColor;
31946
+ ctx.fillRect(x0, y0, layout.w, layout.h);
31947
+ }
31948
+ if (fw > 0 && t.frameColor) {
31949
+ ctx.strokeStyle = t.frameColor;
31950
+ ctx.lineWidth = fw;
31951
+ ctx.strokeRect(x + fw / 2, y + fw / 2, layout.w + fw, layout.h + fw);
31952
+ }
31953
+ const colX = [0];
31954
+ for (const w of layout.colW) colX.push(colX[colX.length - 1] + w);
31955
+ const rowY = [0];
31956
+ for (const h of layout.rowH) rowY.push(rowY[rowY.length - 1] + h);
31957
+ const prevBaseline = ctx.textBaseline;
31958
+ const prevAlign = ctx.textAlign;
31959
+ ctx.textBaseline = "middle";
31960
+ for (const box of layout.boxes) {
31961
+ const rx = x0 + colX[box.c];
31962
+ const ry = y0 + rowY[box.r];
31963
+ const rw = colX[box.c + box.cs] - colX[box.c];
31964
+ const rh = rowY[box.r + box.rs] - rowY[box.r];
31965
+ const cell = box.cell;
31966
+ if (cell.bgColor) {
31967
+ ctx.fillStyle = cell.bgColor;
31968
+ ctx.fillRect(rx, ry, rw, rh);
31969
+ }
31970
+ const text = cell.text ?? "";
31971
+ if (text.length > 0) {
31972
+ const px = fontPxOf(cell.textSize);
31973
+ ctx.font = cellFont(cell, px, args.theme);
31974
+ ctx.fillStyle = cell.textColor ?? args.theme.textColor;
31975
+ const lines = text.split("\n");
31976
+ const blockH = lines.length * px * LINE_HEIGHT;
31977
+ const blockTop = cell.vAlign === "top" ? ry + PAD_Y : cell.vAlign === "bottom" ? ry + rh - PAD_Y - blockH : ry + (rh - blockH) / 2;
31978
+ const tx = cell.hAlign === "left" ? rx + PAD_X : cell.hAlign === "right" ? rx + rw - PAD_X : rx + rw / 2;
31979
+ ctx.textAlign = cell.hAlign;
31980
+ lines.forEach((line, i) => ctx.fillText(line, tx, blockTop + (i + 0.5) * px * LINE_HEIGHT));
31981
+ }
31982
+ if (cell.tooltip) tips.push({ left: rx, top: ry, right: rx + rw, bottom: ry + rh, text: cell.tooltip });
31983
+ }
31984
+ ctx.textBaseline = prevBaseline;
31985
+ ctx.textAlign = prevAlign;
31986
+ if (t.borderColor && t.borderWidth > 0) {
31987
+ ctx.strokeStyle = t.borderColor;
31988
+ ctx.lineWidth = t.borderWidth;
31989
+ const seen = /* @__PURE__ */ new Set();
31990
+ ctx.beginPath();
31991
+ const edge = (ax, ay, bx, by) => {
31992
+ const key = `${ax},${ay},${bx},${by}`;
31993
+ if (seen.has(key)) return;
31994
+ seen.add(key);
31995
+ ctx.moveTo(ax, ay);
31996
+ ctx.lineTo(bx, by);
31997
+ };
31998
+ for (const box of layout.boxes) {
31999
+ const l = Math.round(x0 + colX[box.c]);
32000
+ const r = Math.round(x0 + colX[box.c + box.cs]);
32001
+ const tp = Math.round(y0 + rowY[box.r]);
32002
+ const bt = Math.round(y0 + rowY[box.r + box.rs]);
32003
+ edge(l, tp, r, tp);
32004
+ edge(l, bt, r, bt);
32005
+ edge(l, tp, l, bt);
32006
+ edge(r, tp, r, bt);
32007
+ }
32008
+ ctx.stroke();
32009
+ }
32010
+ }
32011
+ function layoutTable(ctx, t, args) {
32012
+ const { span, omit } = mergeRenderPlan(t);
32013
+ const colW = new Array(t.columns).fill(0);
32014
+ const rowH = new Array(t.rows).fill(0);
32015
+ const boxes = [];
32016
+ for (let r = 0; r < t.rows; r += 1) {
32017
+ for (let c = 0; c < t.columns; c += 1) {
32018
+ if (omit.has(`${r}:${c}`)) continue;
32019
+ const cell = t.cells[r]?.[c];
32020
+ if (cell == null) continue;
32021
+ const sp = span.get(`${r}:${c}`);
32022
+ boxes.push({ cell, r, c, cs: Math.min(sp?.cs ?? 1, t.columns - c), rs: Math.min(sp?.rs ?? 1, t.rows - r) });
32023
+ }
32024
+ }
32025
+ if (boxes.length === 0) return null;
32026
+ const sizeOf = (cell) => {
32027
+ const px = fontPxOf(cell.textSize);
32028
+ ctx.font = cellFont(cell, px, args.theme);
32029
+ const lines = (cell.text ?? "").split("\n");
32030
+ let maxW = 0;
32031
+ for (const line of lines) maxW = Math.max(maxW, ctx.measureText(line).width);
32032
+ let w2 = Math.ceil(maxW) + 2 * PAD_X;
32033
+ let h2 = Math.ceil(lines.length * px * LINE_HEIGHT) + 2 * PAD_Y;
32034
+ if (cell.width) w2 = Math.max(w2, cell.width / 100 * args.plotWidth);
32035
+ if (cell.height) h2 = Math.max(h2, cell.height / 100 * args.paneHeight);
32036
+ return { w: w2, h: h2 };
32037
+ };
32038
+ const spanning = [];
32039
+ for (const box of boxes) {
32040
+ const { w: w2, h: h2 } = sizeOf(box.cell);
32041
+ if (box.cs === 1) colW[box.c] = Math.max(colW[box.c], w2);
32042
+ if (box.rs === 1) rowH[box.r] = Math.max(rowH[box.r], h2);
32043
+ if (box.cs > 1 || box.rs > 1) spanning.push({ box, w: w2, h: h2 });
32044
+ }
32045
+ for (const { box, w: w2, h: h2 } of spanning) {
32046
+ if (box.cs > 1) {
32047
+ let sum = 0;
32048
+ for (let c = box.c; c < box.c + box.cs; c += 1) sum += colW[c];
32049
+ if (w2 > sum) for (let c = box.c; c < box.c + box.cs; c += 1) colW[c] += (w2 - sum) / box.cs;
32050
+ }
32051
+ if (box.rs > 1) {
32052
+ let sum = 0;
32053
+ for (let r = box.r; r < box.r + box.rs; r += 1) sum += rowH[r];
32054
+ if (h2 > sum) for (let r = box.r; r < box.r + box.rs; r += 1) rowH[r] += (h2 - sum) / box.rs;
32055
+ }
32056
+ }
32057
+ let w = 0;
32058
+ for (const cw of colW) w += cw;
32059
+ let h = 0;
32060
+ for (const rh of rowH) h += rh;
32061
+ return { colW, rowH, w, h, boxes };
32062
+ }
32063
+ function anchorOrigin(position, totalW, totalH, args) {
32064
+ let y;
32065
+ if (position.startsWith("top")) y = MARGIN;
32066
+ else if (position.startsWith("bottom")) y = args.paneHeight - MARGIN - totalH;
32067
+ else y = args.paneHeight / 2 - totalH / 2;
32068
+ let x;
32069
+ if (position.endsWith("left")) x = MARGIN;
32070
+ else if (position.endsWith("right")) x = args.plotWidth - MARGIN - totalW;
32071
+ else x = args.plotWidth / 2 - totalW / 2;
32072
+ return { x, y };
32073
+ }
32074
+ function cellFont(cell, px, theme) {
32075
+ const family = cell.fontFamily === "monospace" ? "monospace" : theme.fontFamily || "sans-serif";
32076
+ return `${cell.italic ? "italic " : ""}${cell.bold ? "bold " : ""}${px}px ${family}`;
32077
+ }
32078
+
32079
+ // src/renderers/native/drawings/IndicatorDrawingSlices.ts
32080
+ function indicatorSliceKey(z, boundaries) {
32081
+ return boundaries.find((b) => b > z) ?? Infinity;
32082
+ }
32083
+ var IndicatorDrawingSlices = class {
32084
+ constructor() {
32085
+ this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
32086
+ /** Slice canvas cache, keyed `paneId|beforeZ` — same lifecycle as the user-drawing cache. */
32087
+ this.sliceCache = /* @__PURE__ */ new Map();
32088
+ /** Tooltip hit-rects of every label drawn this frame, in plot coords (rebuilt per prepare). */
32089
+ this.tips = [];
32090
+ }
32091
+ /**
32092
+ * Rebuild the per-indicator drawing slices for this data frame. `ref` is the data
32093
+ * canvas the slices must match pixel-for-pixel (the backend composites them 1:1).
32094
+ * Runs from the renderer's data paint, just before the backend composites the scene.
32095
+ */
32096
+ prepare(scene, coords, theme, ref) {
32097
+ this.tips = [];
32098
+ const out = /* @__PURE__ */ new Map();
32099
+ if (ref.width === 0 || ref.height === 0) {
32100
+ this.sliceCache.clear();
32101
+ return out;
32102
+ }
32103
+ this.drawScene.setDeps({
32104
+ timeToLogical: (ms) => coords.timeToLogical(ms),
32105
+ barAt: (logical) => {
32106
+ const b = scene.bars[Math.round(logical)];
32107
+ return b ? { high: b.high, low: b.low } : null;
32108
+ },
32109
+ theme
32110
+ });
32111
+ const dpr = coords.dpr;
32112
+ const dataW = coords.width;
32113
+ const buckets = /* @__PURE__ */ new Map();
32114
+ const add = (paneId, beforeZ, entry) => {
32115
+ const key = `${paneId}|${beforeZ}`;
32116
+ const bucket = buckets.get(key);
32117
+ if (bucket) bucket.entries.push(entry);
32118
+ else buckets.set(key, { paneId, beforeZ, entries: [entry] });
32119
+ };
32120
+ for (const pane of scene.orderedPanes()) {
32121
+ if (pane.collapsed) continue;
32122
+ const boundaries = scene.seriesBoundaries(pane.id);
32123
+ for (const m of scene.orderedIndicatorsForPane(pane.id)) {
32124
+ const set = modelDrawingSet(m, false);
32125
+ const tables = (m.tables ?? []).filter((t) => !t.overlay);
32126
+ if (drawingSetEmpty(set) && tables.length === 0) continue;
32127
+ const sc = scene.scaleFor(m, pane);
32128
+ const mp = sc === pane.scale ? pane : { ...pane, scale: sc };
32129
+ const beforeZ = indicatorSliceKey(scene.zOf(m.id), boundaries);
32130
+ add(pane.id, beforeZ, { set, tables, pane: mp, indexOffset: scene.offsetOf(m.id) });
32131
+ }
32132
+ if (pane.kind === "price") {
32133
+ for (const m of scene.indicators.values()) {
32134
+ const set = modelDrawingSet(m, true);
32135
+ const tables = (m.tables ?? []).filter((t) => t.overlay === true);
32136
+ if (drawingSetEmpty(set) && tables.length === 0) continue;
32137
+ add(pane.id, Infinity, { set, tables, pane, indexOffset: scene.offsetOf(m.id) });
32138
+ }
32139
+ }
32140
+ }
32141
+ for (const [key, { paneId, beforeZ, entries }] of buckets) {
32142
+ let canvas = this.sliceCache.get(key);
32143
+ if (!canvas) {
32144
+ canvas = document.createElement("canvas");
32145
+ this.sliceCache.set(key, canvas);
32146
+ }
32147
+ if (canvas.width !== ref.width || canvas.height !== ref.height) {
32148
+ canvas.width = ref.width;
32149
+ canvas.height = ref.height;
32150
+ }
32151
+ const ctx = canvas.getContext("2d");
32152
+ if (!ctx) continue;
32153
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
32154
+ ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
32155
+ for (const e of entries) this.paintEntry(ctx, e, coords, dataW, theme);
32156
+ const slices = out.get(paneId) ?? [];
32157
+ slices.push({ beforeZ, canvas });
32158
+ out.set(paneId, slices);
32159
+ }
32160
+ for (const key of [...this.sliceCache.keys()]) if (!buckets.has(key)) this.sliceCache.delete(key);
32161
+ for (const slices of out.values()) slices.sort((a, b) => a.beforeZ - b.beforeZ);
32162
+ return out;
32163
+ }
32164
+ paintEntry(ctx, e, coords, dataW, theme) {
32165
+ const { pane } = e;
32166
+ const paneTips = [];
32167
+ ctx.save();
32168
+ ctx.translate(0, pane.bounds.top);
32169
+ ctx.beginPath();
32170
+ ctx.rect(0, 0, dataW, pane.bounds.height);
32171
+ ctx.clip();
32172
+ this.drawScene.setSet(e.set, e.indexOffset);
32173
+ this.drawScene.render(
32174
+ ctx,
32175
+ dataW,
32176
+ pane.bounds.height,
32177
+ (l) => coords.logicalToX(l),
32178
+ (price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
32179
+ );
32180
+ paneTips.push(...this.drawScene.labelTipRegions());
32181
+ for (const t of e.tables) paintTable(ctx, t, { paneHeight: pane.bounds.height, plotWidth: dataW, theme }, paneTips);
32182
+ ctx.restore();
32183
+ for (const r of paneTips) {
32184
+ this.tips.push({ ...r, top: r.top + pane.bounds.top, bottom: r.bottom + pane.bounds.top });
32185
+ }
32186
+ }
32187
+ /** Tooltip of the topmost label or table cell under a plot-space point, or null. Fed by the last prepare. */
32188
+ labelTooltipAt(x, y) {
32189
+ for (let i = this.tips.length - 1; i >= 0; i -= 1) {
32190
+ const r = this.tips[i];
32191
+ if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return r.text;
32192
+ }
32193
+ return null;
32194
+ }
32195
+ };
32196
+ function mergeSlices(indicator, user) {
32197
+ const out = /* @__PURE__ */ new Map();
32198
+ for (const [paneId, slices] of indicator) out.set(paneId, [...slices]);
32199
+ for (const [paneId, slices] of user) out.set(paneId, [...out.get(paneId) ?? [], ...slices]);
32200
+ for (const slices of out.values()) slices.sort((a, b) => a.beforeZ - b.beforeZ);
32201
+ return out;
32202
+ }
32203
+
31835
32204
  // src/renderers/native/drawings/Projector.ts
31836
32205
  function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
31837
32206
  return {
@@ -32577,6 +32946,11 @@ var NativeRenderer = class {
32577
32946
  this.vpvrRenderer = new VpvrRenderer();
32578
32947
  this.resizeObserver = null;
32579
32948
  this.dprMedia = null;
32949
+ /** Plot size in INTEGER device px, as last reported by the resize observer's
32950
+ * device-pixel-content-box — the browser's own statement of how many device pixels
32951
+ * it paints the plot into. `null` until the first report or where the box type is
32952
+ * unsupported (WebKit); syncSize then falls back to rounding the client rect. */
32953
+ this.plotDeviceSize = null;
32580
32954
  this.coords = new CoordinateSystem();
32581
32955
  this.scene = new SceneGraph();
32582
32956
  // chosen at mount (WebGL2 if available, else canvas2d)
@@ -32584,6 +32958,8 @@ var NativeRenderer = class {
32584
32958
  this.glowAmount = 0;
32585
32959
  // WebGL2 neon-glow intensity (canvas2d ignores it)
32586
32960
  this.chrome = new ChromeRenderer();
32961
+ /** Prepaints each indicator's Pine drawings into interleave slices at the model's z. */
32962
+ this.indicatorSlices = new IndicatorDrawingSlices();
32587
32963
  /** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
32588
32964
  this.labelTooltip = null;
32589
32965
  this.crosshairLayer = new CrosshairRenderer();
@@ -32732,7 +33108,6 @@ var NativeRenderer = class {
32732
33108
  this.toggleVisibleCbs = /* @__PURE__ */ new Set();
32733
33109
  this.moveIndicatorCbs = /* @__PURE__ */ new Set();
32734
33110
  this.priceStyleCbs = /* @__PURE__ */ new Set();
32735
- this.tableOverlays = /* @__PURE__ */ new Map();
32736
33111
  this.name = "native";
32737
33112
  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"];
32738
33113
  /** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
@@ -33046,7 +33421,6 @@ var NativeRenderer = class {
33046
33421
  * hidden) on clear so a re-show picks up the current theme.
33047
33422
  */
33048
33423
  setLoading(loading) {
33049
- for (const overlay of this.tableOverlays.values()) overlay.setVisible(!loading);
33050
33424
  if (!loading || !this.wrapper) {
33051
33425
  this.loadingEl?.remove();
33052
33426
  this.loadingEl = null;
@@ -33708,7 +34082,7 @@ var NativeRenderer = class {
33708
34082
  this.plot.addEventListener("pointerleave", this.onScrollProximityLeave);
33709
34083
  this.labelTooltip = new LabelTooltip(this.plot, {
33710
34084
  theme: () => this.chromeTheme(),
33711
- lookup: (x, y) => this.chrome.labelTooltipAt(x, y)
34085
+ lookup: (x, y) => this.indicatorSlices.labelTooltipAt(x, y)
33712
34086
  });
33713
34087
  this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
33714
34088
  projector: () => this.drawingProjector(),
@@ -33793,6 +34167,7 @@ var NativeRenderer = class {
33793
34167
  this.emitPaneAction({ type: "maximize", paneId, maximized });
33794
34168
  }
33795
34169
  });
34170
+ this.paneControls.setSuspended(this.layoutMode === "mobile");
33796
34171
  this.axisScaleButtons = new AxisScaleButtons(this.plot, theme, {
33797
34172
  panes: () => this.axisScaleViews(),
33798
34173
  rightAxis: () => this.rightAxisW,
@@ -33802,8 +34177,19 @@ var NativeRenderer = class {
33802
34177
  if (pane) this.setPaneLog(paneId, !paneLogScale(this.scene, pane));
33803
34178
  }
33804
34179
  });
33805
- this.resizeObserver = new ResizeObserver(() => this.resize());
34180
+ this.resizeObserver = new ResizeObserver((entries) => {
34181
+ for (const e of entries) {
34182
+ if (e.target !== this.plot) continue;
34183
+ const s = e.devicePixelContentBoxSize?.[0];
34184
+ if (s) this.plotDeviceSize = { width: s.inlineSize, height: s.blockSize };
34185
+ }
34186
+ this.resize();
34187
+ });
33806
34188
  this.resizeObserver.observe(this.wrapper);
34189
+ try {
34190
+ this.resizeObserver.observe(this.plot, { box: "device-pixel-content-box" });
34191
+ } catch {
34192
+ }
33807
34193
  this.watchDpr();
33808
34194
  this.syncSize();
33809
34195
  }
@@ -33928,8 +34314,6 @@ var NativeRenderer = class {
33928
34314
  this.inputsUI?.destroy();
33929
34315
  this.paneControls?.destroy();
33930
34316
  this.axisScaleButtons?.destroy();
33931
- for (const overlay of this.tableOverlays.values()) overlay.destroy();
33932
- this.tableOverlays.clear();
33933
34317
  this.resizeObserver?.disconnect();
33934
34318
  this.resizeObserver = null;
33935
34319
  this.dprMedia?.removeEventListener("change", this.onDprChange);
@@ -33958,6 +34342,7 @@ var NativeRenderer = class {
33958
34342
  this.attributionEl = null;
33959
34343
  this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
33960
34344
  this.mountContainer?.style.removeProperty("--vela-scale-gutter");
34345
+ this.mountContainer?.style.removeProperty("--vela-bottom-gutter");
33961
34346
  this.mountContainer?.style.removeProperty("--vela-price-pane-top");
33962
34347
  this.mountContainer?.style.removeProperty("--vela-price-pane-bottom");
33963
34348
  this.mountContainer = null;
@@ -34050,7 +34435,6 @@ var NativeRenderer = class {
34050
34435
  ensurePane(pane) {
34051
34436
  this.scene.ensurePane(pane.id, pane.kind, pane.order, pane.heightWeight ?? (pane.kind === "price" ? 3 : 1));
34052
34437
  this.layoutPanes();
34053
- this.repositionTables();
34054
34438
  this.paneControls?.refresh();
34055
34439
  this.scheduler.invalidate(4 /* Full */);
34056
34440
  }
@@ -34072,17 +34456,14 @@ var NativeRenderer = class {
34072
34456
  if (!model.ownScale) this.scene.dropIndicatorScale(handle.id);
34073
34457
  this.inputsUI.setPane(handle.id, paneId);
34074
34458
  this.refreshAnchorOffset(model);
34075
- this.syncTables(model);
34076
34459
  this.refreshAxisWidth();
34077
34460
  this.layoutPanes();
34078
- this.repositionTables();
34079
34461
  this.paneControls?.refresh();
34080
34462
  this.scheduler.invalidate(4 /* Full */);
34081
34463
  }
34082
34464
  orderPanes(orderedIds) {
34083
34465
  this.scene.orderPanes(orderedIds);
34084
34466
  this.layoutPanes();
34085
- this.repositionTables();
34086
34467
  this.paneControls?.refresh();
34087
34468
  this.scheduler.invalidate(4 /* Full */);
34088
34469
  }
@@ -34091,7 +34472,6 @@ var NativeRenderer = class {
34091
34472
  if (!pane || pane.collapsed === collapsed) return;
34092
34473
  pane.collapsed = collapsed;
34093
34474
  this.layoutPanes();
34094
- this.repositionTables();
34095
34475
  this.paneControls?.refresh();
34096
34476
  this.scheduler.invalidate(4 /* Full */);
34097
34477
  }
@@ -34099,7 +34479,6 @@ var NativeRenderer = class {
34099
34479
  if (paneId !== null && !this.scene.panes.has(paneId)) paneId = null;
34100
34480
  this.maximizedPaneId = paneId;
34101
34481
  this.layoutPanes();
34102
- this.repositionTables();
34103
34482
  this.paneControls?.refresh();
34104
34483
  this.scheduler.invalidate(4 /* Full */);
34105
34484
  }
@@ -34186,7 +34565,6 @@ var NativeRenderer = class {
34186
34565
  native: !!model.native,
34187
34566
  ...model.props ? { props: model.props, propValues: model.propValues ?? {} } : {}
34188
34567
  });
34189
- this.syncTables(model);
34190
34568
  if (model.native?.type === "volume") {
34191
34569
  this.volumeActive = true;
34192
34570
  this.volumeHidden = false;
@@ -34210,7 +34588,6 @@ var NativeRenderer = class {
34210
34588
  }
34211
34589
  }
34212
34590
  applyPatch(model, patch);
34213
- this.syncTables(model);
34214
34591
  this.scheduler.invalidate(3 /* Light */);
34215
34592
  }
34216
34593
  removeIndicator(handle) {
@@ -34229,8 +34606,6 @@ var NativeRenderer = class {
34229
34606
  this.scene.forgetAnchorOffset(handle.id);
34230
34607
  this.scene.dropIndicatorScale(handle.id);
34231
34608
  this.inputsUI.remove(handle.id);
34232
- this.tableOverlays.get(handle.id)?.destroy();
34233
- this.tableOverlays.delete(handle.id);
34234
34609
  this.refreshAxisWidth();
34235
34610
  this.paneControls?.refresh();
34236
34611
  this.scheduler.invalidate(4 /* Full */);
@@ -34274,8 +34649,6 @@ var NativeRenderer = class {
34274
34649
  }
34275
34650
  if (!visible) {
34276
34651
  this.scene.indicators.delete(handle.id);
34277
- this.tableOverlays.get(handle.id)?.destroy();
34278
- this.tableOverlays.delete(handle.id);
34279
34652
  }
34280
34653
  this.inputsUI.setVisible(handle.id, visible);
34281
34654
  this.scheduler.invalidate(4 /* Full */);
@@ -34321,6 +34694,7 @@ var NativeRenderer = class {
34321
34694
  this.userDrawings?.setLayoutMode(mode);
34322
34695
  this.settingsDialog?.setLayoutMode(mode);
34323
34696
  this.inputsUI?.setLayoutMode(mode);
34697
+ this.paneControls?.setSuspended(mode === "mobile");
34324
34698
  if (this.scrollButton) {
34325
34699
  const px = mode === "mobile" ? SCROLL_BTN_SIZE_TOUCH : SCROLL_BTN_SIZE;
34326
34700
  this.scrollButton.style.width = `${px}px`;
@@ -34716,7 +35090,6 @@ var NativeRenderer = class {
34716
35090
  /** Relayout + repaint + refresh the hover buttons after a collapse/maximize/order change. */
34717
35091
  afterPaneLayoutChange() {
34718
35092
  this.layoutPanes();
34719
- this.repositionTables();
34720
35093
  this.paneControls?.refresh();
34721
35094
  this.scheduler.invalidate(4 /* Full */);
34722
35095
  }
@@ -34797,7 +35170,6 @@ var NativeRenderer = class {
34797
35170
  above.heightWeight = next.above;
34798
35171
  below.heightWeight = next.below;
34799
35172
  this.layoutPanes();
34800
- this.repositionTables();
34801
35173
  this.scheduler.invalidate(4 /* Full */);
34802
35174
  }
34803
35175
  /** Double-click a separator → split the two adjacent panes evenly (each gets half of
@@ -34812,7 +35184,6 @@ var NativeRenderer = class {
34812
35184
  above.heightWeight = half;
34813
35185
  below.heightWeight = half;
34814
35186
  this.layoutPanes();
34815
- this.repositionTables();
34816
35187
  this.scheduler.invalidate(4 /* Full */);
34817
35188
  }
34818
35189
  // ── keyboard navigation / accessibility (item 11) ──
@@ -35091,7 +35462,10 @@ var NativeRenderer = class {
35091
35462
  const liveActual = li >= 0 ? this.bars[li] : void 0;
35092
35463
  const easeLive = !!liveActual && this.liveEaseTime === liveActual.time && (liveActual.high !== this.liveEaseHigh || liveActual.low !== this.liveEaseLow || liveActual.close !== this.liveEaseClose);
35093
35464
  if (easeLive && liveActual) this.bars[li] = { ...liveActual, high: this.liveEaseHigh, low: this.liveEaseLow, close: this.liveEaseClose };
35094
- this.scene.drawingSlices = this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map();
35465
+ this.scene.drawingSlices = mergeSlices(
35466
+ this.indicatorSlices.prepare(this.scene, this.coords, this.theme, this.dataCanvas),
35467
+ this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map()
35468
+ );
35095
35469
  this.backdropRenderer.render(this.scene, this.coords, this.theme, gridAlpha);
35096
35470
  this.backend.render(this.scene, this.coords, this.theme);
35097
35471
  this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
@@ -35529,27 +35903,6 @@ var NativeRenderer = class {
35529
35903
  }
35530
35904
  return maxVol;
35531
35905
  }
35532
- /** Create/update/destroy an indicator's DOM table overlay (anchored off real pane geometry). */
35533
- syncTables(model) {
35534
- const tables = model.tables ?? [];
35535
- let overlay = this.tableOverlays.get(model.id);
35536
- if (tables.length === 0) {
35537
- if (overlay) {
35538
- overlay.destroy();
35539
- this.tableOverlays.delete(model.id);
35540
- }
35541
- return;
35542
- }
35543
- if (!overlay) {
35544
- overlay = new TableOverlay(this.plot, this.theme, (id) => this.paneBoundsFor(id));
35545
- overlay.setVisible(this.loadingEl === null);
35546
- this.tableOverlays.set(model.id, overlay);
35547
- }
35548
- overlay.update(tables);
35549
- }
35550
- repositionTables() {
35551
- for (const overlay of this.tableOverlays.values()) overlay.reposition();
35552
- }
35553
35906
  layoutPanes() {
35554
35907
  const panes = this.scene.orderedPanes();
35555
35908
  const dataHeight = this.coords.height;
@@ -35600,6 +35953,7 @@ var NativeRenderer = class {
35600
35953
  const visible = maxPane ? [maxPane] : this.scene.orderedPanes().filter((p) => !p.collapsed);
35601
35954
  const paneBottom = visible.length ? Math.max(...visible.map((p) => p.bounds.top + p.bounds.height)) : dataHeight;
35602
35955
  this.scrollBtnBottomPx = SCROLL_BTN_BOTTOM + Math.max(0, dataHeight - paneBottom);
35956
+ this.mountContainer?.style.setProperty("--vela-bottom-gutter", `${TIME_AXIS_H + Math.max(0, dataHeight - paneBottom)}px`);
35603
35957
  this.scrollBtnRightPx = this.rightAxisW + SCROLL_BTN_RIGHT_INSET;
35604
35958
  if (this.scrollButton) {
35605
35959
  this.scrollButton.style.bottom = `${this.scrollBtnBottomPx}px`;
@@ -35692,30 +36046,33 @@ var NativeRenderer = class {
35692
36046
  if (w <= 0 || h <= 0) return;
35693
36047
  const dpr = window.devicePixelRatio || 1;
35694
36048
  this.plot.style.left = `${this.toolbarGutter}px`;
35695
- const pw = Math.max(1, w - this.toolbarGutter);
35696
- const ph = h;
35697
- this.dataCanvas.width = Math.round(pw * dpr);
35698
- this.dataCanvas.height = Math.round(ph * dpr);
35699
- this.backdropCanvas.width = this.dataCanvas.width;
35700
- this.backdropCanvas.height = this.dataCanvas.height;
35701
- this.volumeCanvas.width = this.dataCanvas.width;
35702
- this.volumeCanvas.height = this.dataCanvas.height;
35703
- for (const l of this.extLayers) {
35704
- l.canvas.width = this.dataCanvas.width;
35705
- l.canvas.height = this.dataCanvas.height;
35706
- }
35707
- this.vpvrCanvas.width = this.dataCanvas.width;
35708
- this.vpvrCanvas.height = this.dataCanvas.height;
35709
- this.chromeCanvas.width = this.dataCanvas.width;
35710
- this.chromeCanvas.height = this.dataCanvas.height;
35711
- this.drawingsCanvas.width = this.dataCanvas.width;
35712
- this.drawingsCanvas.height = this.dataCanvas.height;
35713
- this.cursorCanvas.width = this.dataCanvas.width;
35714
- this.cursorCanvas.height = this.dataCanvas.height;
36049
+ const rect = this.plot.getBoundingClientRect();
36050
+ let bw = Math.max(1, Math.round(rect.width * dpr));
36051
+ let bh = Math.max(1, Math.round(rect.height * dpr));
36052
+ const dev = this.plotDeviceSize;
36053
+ if (dev && Math.abs(dev.width - rect.width * dpr) <= 1 && Math.abs(dev.height - rect.height * dpr) <= 1) {
36054
+ bw = Math.max(1, dev.width);
36055
+ bh = Math.max(1, dev.height);
36056
+ }
36057
+ const pw = bw / dpr;
36058
+ const ph = bh / dpr;
36059
+ const size = (canvas) => {
36060
+ canvas.width = bw;
36061
+ canvas.height = bh;
36062
+ canvas.style.width = `${pw}px`;
36063
+ canvas.style.height = `${ph}px`;
36064
+ };
36065
+ size(this.dataCanvas);
36066
+ size(this.backdropCanvas);
36067
+ size(this.volumeCanvas);
36068
+ for (const l of this.extLayers) size(l.canvas);
36069
+ size(this.vpvrCanvas);
36070
+ size(this.chromeCanvas);
36071
+ size(this.drawingsCanvas);
36072
+ size(this.cursorCanvas);
35715
36073
  this.coords.setSize(Math.max(1, pw - this.rightAxisW), Math.max(1, ph - TIME_AXIS_H), dpr);
35716
36074
  this.scene.crosshair = null;
35717
36075
  this.layoutPanes();
35718
- this.repositionTables();
35719
36076
  this.userDrawings?.onResize();
35720
36077
  if (!this.didInitialFit && this.coords.barCount > 0) {
35721
36078
  this.fitContent();
@@ -37034,9 +37391,168 @@ var Watermark = class {
37034
37391
  }
37035
37392
  };
37036
37393
 
37394
+ // src/widget/cell-controls.ts
37395
+ var CELL_CONTROLS_PROXIMITY_PX = 120;
37396
+ var TIME_AXIS_H2 = 22;
37397
+ var CONTROLS_BOTTOM_PX = TIME_AXIS_H2 + 12;
37398
+ var CLUSTER_H2 = 24;
37399
+ var CLUSTER_PILL2 = "rgba(0,0,0,0.65)";
37400
+ var STYLE_ID26 = "vela-cell-controls";
37401
+ var CSS23 = `
37402
+ .vela-cc-btn{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;padding:0;border:none;border-radius:var(--vela-radius-sm);background:transparent;line-height:0;font-size:12px;color:var(--vela-fg-muted);cursor:pointer;}
37403
+ .vela-cc-btn svg{display:block;}
37404
+ .vela-cc-btn:hover{background:var(--vela-active);color:var(--vela-fg-bright);}
37405
+ .vela-cc-on,.vela-cc-on:hover{background:var(--vela-selected-bg);color:var(--vela-selected-fg);}
37406
+ .vela-cc-grip{cursor:grab;touch-action:none;}
37407
+ .vela-cc-grip:active{cursor:grabbing;}
37408
+ `;
37409
+ function nearBottomCenter(x, y, width, height, proximityPx = CELL_CONTROLS_PROXIMITY_PX) {
37410
+ const cx = width / 2;
37411
+ const cy = height - CONTROLS_BOTTOM_PX - CLUSTER_H2 / 2;
37412
+ return Math.hypot(x - cx, y - cy) <= proximityPx;
37413
+ }
37414
+ var CellControls = class {
37415
+ constructor(host, deps) {
37416
+ this.host = host;
37417
+ this.deps = deps;
37418
+ this.near = false;
37419
+ /** A grip drag is underway — the proximity reveal must not hide the cluster
37420
+ * while captured pointer moves sweep across the whole grid. */
37421
+ this.dragging = false;
37422
+ /** Mobile: the proximity reveal is meaningless without a cursor — the mobile
37423
+ * bar's maximize stop replaces the cluster. */
37424
+ this.suspended = false;
37425
+ this.onHostMove = (e) => {
37426
+ if (this.suspended) return;
37427
+ if (this.dragging) return;
37428
+ const rect = this.host.getBoundingClientRect();
37429
+ this.setNear(nearBottomCenter(e.clientX - rect.left, e.clientY - rect.top, rect.width, rect.height));
37430
+ };
37431
+ this.onHostLeave = () => {
37432
+ if (this.dragging) return;
37433
+ this.setNear(false);
37434
+ };
37435
+ injectStyles(STYLE_ID26, CSS23, host.ownerDocument);
37436
+ this.glider = new Glider(deps.chart);
37437
+ this.root = host.ownerDocument.createElement("div");
37438
+ Object.assign(this.root.style, {
37439
+ position: "absolute",
37440
+ left: "50%",
37441
+ bottom: `${CONTROLS_BOTTOM_PX}px`,
37442
+ transform: "translateX(-50%)",
37443
+ zIndex: "6",
37444
+ display: "none",
37445
+ // revealed by cursor proximity (onHostMove)
37446
+ gap: "2px",
37447
+ padding: "2px",
37448
+ borderRadius: "var(--vela-radius-md)",
37449
+ background: CLUSTER_PILL2,
37450
+ pointerEvents: "auto"
37451
+ });
37452
+ this.host.addEventListener("pointermove", this.onHostMove);
37453
+ this.host.addEventListener("pointerleave", this.onHostLeave);
37454
+ this.host.appendChild(this.root);
37455
+ this.refresh();
37456
+ }
37457
+ /** Rebuild the buttons (the multi-cell gate or the maximized state changed). */
37458
+ refresh() {
37459
+ this.root.textContent = "";
37460
+ const multi = this.deps.multiCell();
37461
+ const maximized = multi && this.deps.isMaximized();
37462
+ if (multi && !maximized) this.root.appendChild(this.makeGrip());
37463
+ this.root.appendChild(this.button("minus", "Zoom out", () => this.glider.zoom(ZOOM_OUT)));
37464
+ this.root.appendChild(this.button("plus", "Zoom in", () => this.glider.zoom(ZOOM_IN)));
37465
+ if (multi) {
37466
+ this.root.appendChild(
37467
+ this.button(maximized ? "restore" : "maximize", maximized ? "Restore layout" : "Maximize chart", () => this.deps.toggleMaximize(), {
37468
+ // The maximized state reads as an inverse chip (white-on-dark, dark-on-light),
37469
+ // the same active-state affordance as a collapsed pane's expand button.
37470
+ selected: maximized
37471
+ })
37472
+ );
37473
+ }
37474
+ this.root.appendChild(
37475
+ this.button("reset", "Reset chart", () => {
37476
+ this.glider.stop();
37477
+ this.deps.reset();
37478
+ })
37479
+ );
37480
+ }
37481
+ button(iconId, title, onClick, opts = {}) {
37482
+ const b = this.host.ownerDocument.createElement("button");
37483
+ b.type = "button";
37484
+ b.title = title;
37485
+ b.setAttribute("aria-label", title);
37486
+ b.className = opts.selected === true ? "vela-cc-btn vela-cc-on" : "vela-cc-btn";
37487
+ b.innerHTML = icon(iconId);
37488
+ b.addEventListener("click", (e) => {
37489
+ e.stopPropagation();
37490
+ onClick();
37491
+ });
37492
+ return b;
37493
+ }
37494
+ /** The drag handle (2×3 dot grip): press and drag onto another cell to trade
37495
+ * slots with it. The preview highlight follows the pointer; releasing outside
37496
+ * any other cell cancels. */
37497
+ makeGrip() {
37498
+ const b = this.host.ownerDocument.createElement("button");
37499
+ b.type = "button";
37500
+ b.title = "Drag to move chart";
37501
+ b.setAttribute("aria-label", "Drag to move chart");
37502
+ b.className = "vela-cc-btn vela-cc-grip";
37503
+ b.innerHTML = icon("grip");
37504
+ b.addEventListener("pointerdown", (e) => this.onGripDown(b, e));
37505
+ return b;
37506
+ }
37507
+ onGripDown(btn2, e) {
37508
+ if (e.button !== 0 && e.pointerType === "mouse") return;
37509
+ e.preventDefault();
37510
+ e.stopPropagation();
37511
+ try {
37512
+ btn2.setPointerCapture(e.pointerId);
37513
+ } catch {
37514
+ }
37515
+ this.dragging = true;
37516
+ let target = null;
37517
+ const move = (ev) => {
37518
+ target = this.deps.dragTargetAt(ev.clientX, ev.clientY);
37519
+ this.deps.previewDrop(target);
37520
+ };
37521
+ const finish = (commit) => () => {
37522
+ this.dragging = false;
37523
+ this.deps.previewDrop(null);
37524
+ btn2.removeEventListener("pointermove", move);
37525
+ btn2.removeEventListener("pointerup", onUp);
37526
+ btn2.removeEventListener("pointercancel", onCancel);
37527
+ if (commit && target != null) this.deps.dropOn(target);
37528
+ };
37529
+ const onUp = finish(true);
37530
+ const onCancel = finish(false);
37531
+ btn2.addEventListener("pointermove", move);
37532
+ btn2.addEventListener("pointerup", onUp);
37533
+ btn2.addEventListener("pointercancel", onCancel);
37534
+ }
37535
+ /** Mobile flips the cluster off entirely (and hides it if currently revealed). */
37536
+ setSuspended(on) {
37537
+ this.suspended = on;
37538
+ if (on) this.setNear(false);
37539
+ }
37540
+ setNear(near) {
37541
+ if (near === this.near) return;
37542
+ this.near = near;
37543
+ this.root.style.display = near ? "flex" : "none";
37544
+ }
37545
+ destroy() {
37546
+ this.glider.stop();
37547
+ this.host.removeEventListener("pointermove", this.onHostMove);
37548
+ this.host.removeEventListener("pointerleave", this.onHostLeave);
37549
+ this.root.remove();
37550
+ }
37551
+ };
37552
+
37037
37553
  // src/widget/context-menu.ts
37038
37554
  var PRICE_AXIS_W = 60;
37039
- var TIME_AXIS_H2 = 26;
37555
+ var TIME_AXIS_H3 = 26;
37040
37556
  var ChartContextMenu = class {
37041
37557
  constructor(host, cbs) {
37042
37558
  this.cbs = cbs;
@@ -37075,7 +37591,7 @@ var ChartContextMenu = class {
37075
37591
  zoneOf(e) {
37076
37592
  const rect = this.host.getBoundingClientRect();
37077
37593
  if (e.clientX - rect.left > rect.width - PRICE_AXIS_W) return "price-axis";
37078
- if (e.clientY - rect.top > rect.height - TIME_AXIS_H2) return "time-axis";
37594
+ if (e.clientY - rect.top > rect.height - TIME_AXIS_H3) return "time-axis";
37079
37595
  return "body";
37080
37596
  }
37081
37597
  /** The pane under the pointer, so every pane's price scale has its own menu. */
@@ -37409,11 +37925,18 @@ var ChartCell = class {
37409
37925
  if (this.inner && this.state.symbol) this.marketStatus?.track(this.inner.data, this.state.symbol);
37410
37926
  });
37411
37927
  this.syncStatuslineColors();
37928
+ this.cellControls = new CellControls(this.host, {
37929
+ chart: () => this.inner,
37930
+ reset: () => this.resetView(),
37931
+ multiCell: () => deps.multiCell(),
37932
+ isMaximized: () => deps.isMaximized(id),
37933
+ toggleMaximize: () => deps.toggleMaximize(id),
37934
+ dragTargetAt: (x, y) => deps.cellDragTarget(id, x, y),
37935
+ previewDrop: (target) => deps.previewDropTarget(target),
37936
+ dropOn: (target) => deps.dropCell(id, target)
37937
+ });
37412
37938
  this.contextMenu = new ChartContextMenu(this.host, {
37413
- resetView: () => {
37414
- this.inner?.renderer.set("autoScale", true);
37415
- this.inner?.setVisibleRangePreset("ALL");
37416
- },
37939
+ resetView: () => this.resetView(),
37417
37940
  timezone: () => this.deps.timezone(),
37418
37941
  setTimezone: (zone) => this.deps.setTimezone(zone),
37419
37942
  // Right-clicking activates the cell first (capture-phase pointerdown), so the
@@ -37787,6 +38310,20 @@ var ChartCell = class {
37787
38310
  this.inner.setVisibleRangePreset(preset.preset);
37788
38311
  }
37789
38312
  }
38313
+ /** Reset this cell's view: re-enable auto scale and frame the full history —
38314
+ * the same action the chart context menu offers. */
38315
+ resetView() {
38316
+ this.inner?.renderer.set("autoScale", true);
38317
+ this.inner?.setVisibleRangePreset("ALL");
38318
+ }
38319
+ /** Rebuild the view-controls cluster (the maximize gate or state changed). */
38320
+ refreshControls() {
38321
+ this.cellControls.refresh();
38322
+ }
38323
+ /** Mobile flips the per-cell cluster off (the shell's mobile bar replaces it). */
38324
+ setControlsSuspended(on) {
38325
+ this.cellControls.setSuspended(on);
38326
+ }
37790
38327
  /** Make this cell the active one and put keyboard focus on its chart surface. */
37791
38328
  focus() {
37792
38329
  this.deps.activate(this.id);
@@ -38100,6 +38637,7 @@ var ChartCell = class {
38100
38637
  destroy() {
38101
38638
  this.destroyed = true;
38102
38639
  this.offMarket();
38640
+ this.cellControls.destroy();
38103
38641
  this.contextMenu.destroy();
38104
38642
  this.history.destroy();
38105
38643
  this.marketStatus?.stop();
@@ -38392,10 +38930,10 @@ var SplitterLayer = class {
38392
38930
  var DEFAULT_TIMEFRAMES = ["1", "5", "15", "60", "240", "D", "W"];
38393
38931
  var GAP_PX = 2;
38394
38932
  var POOL_CAP = 16;
38395
- var TIME_AXIS_H3 = 22;
38933
+ var TIME_AXIS_H4 = 22;
38396
38934
  var ALERT_CAP = 50;
38397
- var STYLE_ID26 = "vela-workspace";
38398
- var CSS23 = `
38935
+ var STYLE_ID27 = "vela-workspace";
38936
+ var CSS24 = `
38399
38937
  .vela-workspace { position: relative; width: 100%; height: 100%; display: flex; flex-direction: column; background: var(--vela-bg); }
38400
38938
  .vela-ws-main { position: relative; display: flex; flex-direction: row; flex: 1 1 auto; min-height: 0; }
38401
38939
  .vela-ws-toolbar { position: relative; flex: none; }
@@ -38422,6 +38960,21 @@ var CSS23 = `
38422
38960
  /* Mobile: the docked drawing-toolbar column would eat a phone-width grid \u2014 the shell's
38423
38961
  drawings drawer + on-chart pill replace it (same policy as the widget's in-chart bar). */
38424
38962
  [data-layout='mobile'] .vela-ws-toolbar { display: none; }
38963
+ /* A maximized cell owns the whole grid: the splitter strips have no seams to grab and
38964
+ the active ring would just outline the only visible chart \u2014 both are noise here. */
38965
+ .vela-ws-grid[data-maximized='1'] .vela-ws-splitter { display: none; }
38966
+ .vela-ws-grid[data-maximized='1'] .vela-cell[data-active='1']::after { display: none; }
38967
+ /* Drop-target preview while a cell's drag handle is held: a dashed ring + the same
38968
+ soft wash the splitter hover uses, over the chart, inert to the pointer. */
38969
+ .vela-cell[data-drop-target='1']::before {
38970
+ content: '';
38971
+ position: absolute;
38972
+ inset: 0;
38973
+ border: 2px dashed var(--vela-fg-bright);
38974
+ background: var(--vela-separator-hover-band);
38975
+ pointer-events: none;
38976
+ z-index: 11;
38977
+ }
38425
38978
  `;
38426
38979
  registerIcon("layout", svg16('<rect x="1.5" y="1.5" width="13" height="13" rx="1.5"/><path d="M8 1.5v13M1.5 8h13"/>'));
38427
38980
  function declaredOrder(cells) {
@@ -38451,6 +39004,9 @@ var VelaWorkspace = class {
38451
39004
  * slots beyond the list get auto identities. Grows, never reorders. */
38452
39005
  this.order = [];
38453
39006
  this.activeId = null;
39007
+ /** The cell maximized over the whole grid (null = normal grid). TRANSIENT view
39008
+ * state — never persisted; any structural change (layout, applyState) restores. */
39009
+ this.maximizedId = null;
38454
39010
  this.cellBackend = "auto";
38455
39011
  this.destroyed = false;
38456
39012
  this.shortcutsHelp = null;
@@ -38555,7 +39111,7 @@ var VelaWorkspace = class {
38555
39111
  this.order = boot?.charts ? boot.charts.map((c) => c.id) : declaredOrder(opts.cells);
38556
39112
  const bootActive = boot?.activeCellId ?? null;
38557
39113
  const doc = hostEl.ownerDocument;
38558
- injectStyles(STYLE_ID26, CSS23, doc);
39114
+ injectStyles(STYLE_ID27, CSS24, doc);
38559
39115
  this.root = doc.createElement("div");
38560
39116
  this.root.className = "vela-workspace";
38561
39117
  ensureUIHost(this.root, resolveTheme(opts.theme));
@@ -38665,7 +39221,11 @@ var VelaWorkspace = class {
38665
39221
  if (attribution !== false) {
38666
39222
  const background = resolveTheme(opts.theme).background;
38667
39223
  const mark = typeof attribution === "string" && attribution.trim() ? createCustomMark(doc, attribution, background) : createAttributionMark(doc, background);
38668
- Object.assign(mark.style, { left: "12px", bottom: `${TIME_AXIS_H3 + 10}px`, zIndex: "11" });
39224
+ Object.assign(mark.style, {
39225
+ left: "calc(var(--vela-toolbar-gutter, 0px) + 12px)",
39226
+ bottom: `calc(var(--vela-bottom-gutter, ${TIME_AXIS_H4}px) + 10px)`,
39227
+ zIndex: "11"
39228
+ });
38669
39229
  this.gridEl.appendChild(mark);
38670
39230
  this.attributionMark = mark;
38671
39231
  }
@@ -38731,6 +39291,9 @@ var VelaWorkspace = class {
38731
39291
  ...topbarHas(this.topbarComp, "indicators") && (picker || this.indicatorsOverride) ? { onIndicatorsClick: this.indicatorsOverride ? () => this.runOverride(this.indicatorsOverride) : () => picker.open() } : {},
38732
39292
  getContext: () => this.context(),
38733
39293
  ...this.drawingsEnabled ? { onDrawingsClick: () => this.openDrawingsDrawer() } : {},
39294
+ // Multi-chart only: the stop that isolates the ACTIVE chart (the
39295
+ // per-cell hover cluster has no cursor to reveal it on mobile).
39296
+ ...this.monoLayout ? {} : { onMaximizeClick: () => this.toggleMobileMaximize() },
38734
39297
  onMoreClick: () => this.openMoreDrawer(),
38735
39298
  onSettingsClick: () => this.active.chart.renderer.openSettings()
38736
39299
  }) : null;
@@ -38937,7 +39500,9 @@ var VelaWorkspace = class {
38937
39500
  this.pool.clear();
38938
39501
  for (const { id, ...cs } of st.charts.slice(liveCount)) this.pool.set(id, cs);
38939
39502
  this.order = st.charts.map((c) => c.id);
39503
+ this.clearMaximized();
38940
39504
  this.applyGrid();
39505
+ this.refreshCellControls();
38941
39506
  const nextActive2 = st.activeCellId && this.cellsById.has(st.activeCellId) ? st.activeCellId : this.order[0] ?? null;
38942
39507
  if (nextActive2 === this.activeId) this.projectActiveCell();
38943
39508
  else this.setActiveCell(nextActive2);
@@ -38963,6 +39528,7 @@ var VelaWorkspace = class {
38963
39528
  const def = this.monoLayout ? null : ensureLayout(st.layout);
38964
39529
  if (def) this.def = def;
38965
39530
  this.cellBackend = this.backendFor(this.def);
39531
+ this.clearMaximized();
38966
39532
  this.applyGrid();
38967
39533
  this.buildCells();
38968
39534
  this.syncCellPresentation();
@@ -39026,6 +39592,7 @@ var VelaWorkspace = class {
39026
39592
  setLayout(layout) {
39027
39593
  if (this.destroyed) return;
39028
39594
  if (this.monoLayout) return;
39595
+ this.clearMaximized();
39029
39596
  const next = this.resolveLayout(layout);
39030
39597
  const nextBackend = this.backendFor(next);
39031
39598
  const rebuildAll = nextBackend !== this.cellBackend;
@@ -39046,6 +39613,7 @@ var VelaWorkspace = class {
39046
39613
  this.buildCells();
39047
39614
  this.alignNewCellStyles(preexisting);
39048
39615
  this.syncCellPresentation();
39616
+ this.refreshCellControls();
39049
39617
  this.topbar.setLayout(next.id);
39050
39618
  const nextActive = activeAfterLayout(this.activeId, this.order.slice(0, next.cells.length));
39051
39619
  if (nextActive === this.activeId) this.projectActiveCell();
@@ -39054,6 +39622,67 @@ var VelaWorkspace = class {
39054
39622
  this.events.emit("layout:changed", { layout: next.id });
39055
39623
  this.markStateDirty();
39056
39624
  }
39625
+ /** The identity of the cell maximized over the whole grid, or null. */
39626
+ get maximizedCell() {
39627
+ return this.maximizedId;
39628
+ }
39629
+ /**
39630
+ * Maximize one cell over the whole grid, or restore the layout with `null`. Pure
39631
+ * presentation: the other cells stay alive underneath — charts, subscriptions and
39632
+ * state untouched — so restoring is instant. The maximized cell becomes the active
39633
+ * one. Transient view state (also reachable from each cell's bottom-center view
39634
+ * cluster): switching layouts or applying a state document restores the grid.
39635
+ */
39636
+ maximizeCell(id) {
39637
+ if (this.destroyed) return;
39638
+ if (id != null && (!this.cellsById.has(id) || this.def.cells.length <= 1)) return;
39639
+ if (id === this.maximizedId) return;
39640
+ this.maximizedId = id;
39641
+ if (id) this.setActiveCell(id);
39642
+ this.applyGrid();
39643
+ this.refreshCellControls();
39644
+ this.syncMobileMaximize();
39645
+ this.events.emit("cell:maximized", { id });
39646
+ }
39647
+ /** The mobile bar's maximize stop: one press isolates the ACTIVE chart over the
39648
+ * grid; while something is already isolated — the chart, or a pane inside it
39649
+ * (mobile's double-tap) — the press restores that instead. Every branch re-syncs
39650
+ * the stop on its own (`maximizeCell` directly, `panes.maximize` via its
39651
+ * synchronous `pane:changed`). */
39652
+ toggleMobileMaximize() {
39653
+ const cell = this.activeId ? this.cellsById.get(this.activeId) : void 0;
39654
+ if (!cell) return;
39655
+ if (this.maximizedId) this.maximizeCell(null);
39656
+ else if (cell.chart.panes.list().some((p) => p.maximized)) cell.chart.panes.maximize(null);
39657
+ else this.maximizeCell(cell.id);
39658
+ }
39659
+ /** Keep the mobile bar's maximize stop truthful: lit (inverse chip, restore
39660
+ * glyph) while the active chart covers the grid OR one of its panes is
39661
+ * maximized — the state a double-tap toggles is otherwise invisible on mobile. */
39662
+ syncMobileMaximize() {
39663
+ if (!this.mobileBar) return;
39664
+ const cell = this.activeId ? this.cellsById.get(this.activeId) : void 0;
39665
+ const paneMax = cell ? cell.chart.panes.list().some((p) => p.maximized) : false;
39666
+ this.mobileBar.setMaximizeActive(this.maximizedId != null || paneMax);
39667
+ }
39668
+ /**
39669
+ * Trade the SLOTS of two live cells — the grid arrangement changes, the cells
39670
+ * themselves (charts, indicators, drawings, the active flag) stay untouched.
39671
+ * What each cell's drag handle commits; also callable directly by hosts.
39672
+ */
39673
+ swapCells(a, b) {
39674
+ if (this.destroyed || a === b) return;
39675
+ const i = this.order.indexOf(a);
39676
+ const j = this.order.indexOf(b);
39677
+ if (i < 0 || j < 0 || !this.cellsById.has(a) || !this.cellsById.has(b)) return;
39678
+ [this.order[i], this.order[j]] = [this.order[j], this.order[i]];
39679
+ for (const [k] of this.def.cells.entries()) {
39680
+ const host = this.cellsById.get(this.order[k] ?? "")?.host;
39681
+ if (host) this.gridEl.appendChild(host);
39682
+ }
39683
+ this.applyGrid();
39684
+ this.markStateDirty();
39685
+ }
39057
39686
  resize() {
39058
39687
  this.splitters.layout();
39059
39688
  }
@@ -39123,6 +39752,7 @@ var VelaWorkspace = class {
39123
39752
  this.mobileBar?.renderActions();
39124
39753
  this.mobileBar?.setSymbol(cell.symbol);
39125
39754
  this.mobileBar?.setTimeframe(cell.timeframe);
39755
+ this.syncMobileMaximize();
39126
39756
  this.drawingPill?.onChart(cell.chart);
39127
39757
  const pushHistory = () => this.topbar.setHistoryState(cell.history.canUndo, cell.history.canRedo);
39128
39758
  this.historyUnsub?.();
@@ -39212,8 +39842,78 @@ var VelaWorkspace = class {
39212
39842
  const host = this.cellsById.get(this.order[i] ?? "")?.host;
39213
39843
  if (host) host.style.gridArea = perCell[slot.id]?.gridArea ?? "";
39214
39844
  }
39845
+ this.applyMaximizePresentation();
39846
+ this.mountAttributionMark();
39215
39847
  this.splitters.layout();
39216
39848
  }
39849
+ /** Overlay the maximize presentation on the freshly applied grid: EVERY cell spans
39850
+ * the full track grid — the maximized one on top, the siblings invisible beneath
39851
+ * it (their charts stay alive — restoring is instant). The siblings must span too:
39852
+ * left in their slots they would auto-flow into implicit zero-height rows, whose
39853
+ * gaps steal height from the maximized cell and collapse their renderers to 0.
39854
+ * The splitter strips and the active ring hide via the `data-maximized` rules. */
39855
+ applyMaximizePresentation() {
39856
+ const maxId = this.maximizedId;
39857
+ if (maxId) this.gridEl.dataset.maximized = "1";
39858
+ else delete this.gridEl.dataset.maximized;
39859
+ for (const [id, cell] of this.cellsById) {
39860
+ const style = cell.host.style;
39861
+ if (maxId) style.gridArea = "1 / 1 / -1 / -1";
39862
+ style.zIndex = maxId && id === maxId ? "5" : "";
39863
+ style.visibility = maxId && id !== maxId ? "hidden" : "";
39864
+ }
39865
+ }
39866
+ /** Rebuild every cell's view cluster (the maximize gate or state changed). */
39867
+ refreshCellControls() {
39868
+ for (const cell of this.cellsById.values()) cell.refreshControls();
39869
+ }
39870
+ /** Drop the transient maximize on a structural change (layout switch, state
39871
+ * document) — WITH the event, so hosts tracking `cell:maximized` never drift
39872
+ * from `maximizedCell`. The caller's own grid re-apply paints the restore. */
39873
+ clearMaximized() {
39874
+ if (this.maximizedId == null) return;
39875
+ this.maximizedId = null;
39876
+ this.events.emit("cell:maximized", { id: null });
39877
+ }
39878
+ /** The live cell under a viewport point, excluding `excludeId` and any host a
39879
+ * maximize has hidden — the drag handle's hit-test. */
39880
+ cellAtPoint(x, y, excludeId) {
39881
+ for (const [id, cell] of this.cellsById) {
39882
+ if (id === excludeId || cell.host.style.visibility === "hidden") continue;
39883
+ const r = cell.host.getBoundingClientRect();
39884
+ if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return id;
39885
+ }
39886
+ return null;
39887
+ }
39888
+ /** Mark one cell as the live drop target of a grip drag (null clears all) —
39889
+ * the `data-drop-target` stylesheet rule paints the dashed preview ring. */
39890
+ setDropTarget(id) {
39891
+ for (const [cid, cell] of this.cellsById) {
39892
+ if (cid === id) cell.host.dataset.dropTarget = "1";
39893
+ else delete cell.host.dataset.dropTarget;
39894
+ }
39895
+ }
39896
+ /** The cell whose bottom-left corner the grid's attribution mark floats in — the
39897
+ * maximized cell while one covers the grid, else the bottom-left slot's cell. */
39898
+ bottomLeftCell() {
39899
+ if (this.maximizedId) return this.cellsById.get(this.maximizedId);
39900
+ const grid = occupancyGrid(this.def);
39901
+ const slot = grid[grid.length - 1]?.[0];
39902
+ const idx = this.def.cells.findIndex((c) => (c.area ?? c.id) === slot);
39903
+ return this.cellsById.get(this.order[idx >= 0 ? idx : 0] ?? "");
39904
+ }
39905
+ /** Keep the shared attribution mark inside the BOTTOM-LEFT visible cell: its
39906
+ * offsets ride that cell's renderer-published `--vela-bottom-gutter` /
39907
+ * `--vela-toolbar-gutter`, so collapsed pane strips push the mark up without any
39908
+ * bookkeeping here. Re-run after anything that changes which host that is
39909
+ * (layout switch, maximize, cell rebuild); a destroyed host drops the mark from
39910
+ * the DOM, and this re-mount brings it back. */
39911
+ mountAttributionMark() {
39912
+ const mark = this.attributionMark;
39913
+ if (!mark) return;
39914
+ const host = this.bottomLeftCell()?.host ?? this.gridEl;
39915
+ if (mark.parentElement !== host) host.appendChild(mark);
39916
+ }
39217
39917
  /** Create the cells the current layout wants but don't exist yet (pool-first).
39218
39918
  * A slot's CELL IDENTITY is `order[i]` (declaration order — never the slot's own
39219
39919
  * positional id); slots past the declared list mint an auto identity once. */
@@ -39245,6 +39945,12 @@ var VelaWorkspace = class {
39245
39945
  setTimezone: (zone) => this.setTimezone(zone),
39246
39946
  context: () => this.context(),
39247
39947
  activate: (id2) => this.setActiveCell(id2),
39948
+ multiCell: () => !this.monoLayout && this.def.cells.length > 1,
39949
+ isMaximized: (id2) => this.maximizedId === id2,
39950
+ toggleMaximize: (id2) => this.maximizeCell(this.maximizedId === id2 ? null : id2),
39951
+ cellDragTarget: (id2, x, y) => this.cellAtPoint(x, y, id2),
39952
+ previewDropTarget: (target) => this.setDropTarget(target),
39953
+ dropCell: (id2, target) => this.swapCells(id2, target),
39248
39954
  onMarketChanged: (id2) => this.onCellMarketChanged(id2),
39249
39955
  onPriceStyleChanged: (id2) => this.onCellPriceStyleChanged(id2),
39250
39956
  onIndicatorsChanged: (id2) => this.onCellIndicatorsChanged(id2),
@@ -39258,6 +39964,7 @@ var VelaWorkspace = class {
39258
39964
  if (id === this.activeId) cell.host.dataset.active = "1";
39259
39965
  this.wireCell(cell);
39260
39966
  cell.chart.renderer.setLayoutMode(this.layoutCtl.current);
39967
+ cell.setControlsSuspended(this.layoutCtl.current === "mobile");
39261
39968
  if (this.favs.length > 0) cell.chart.drawings.setFavorites(this.favs);
39262
39969
  cell.setManifest(this.manifest, pooled?.indicators == null);
39263
39970
  cell.restorePersistedExt();
@@ -39267,6 +39974,7 @@ var VelaWorkspace = class {
39267
39974
  const host = this.cellsById.get(this.order[i] ?? "")?.host;
39268
39975
  if (host) this.gridEl.appendChild(host);
39269
39976
  }
39977
+ this.mountAttributionMark();
39270
39978
  }
39271
39979
  /** Per-cell chart subscriptions (trigger ② — the chart instance is stable for the
39272
39980
  * cell's whole life, so these live and die with the cell). */
@@ -39326,6 +40034,9 @@ var VelaWorkspace = class {
39326
40034
  chart.on("viewport:changed", (range) => this.propagateViewport(cell.id, range));
39327
40035
  chart.renderer.onConfigChanged(() => this.propagateStylePrefs(cell.id));
39328
40036
  chart.on("theme:changed", (t) => this.setTheme(t));
40037
+ chart.on("pane:changed", () => {
40038
+ if (cell.id === this.activeId) this.syncMobileMaximize();
40039
+ });
39329
40040
  chart.renderer.onAxisLongPress((e) => {
39330
40041
  if (this.layoutCtl.current !== "mobile") return;
39331
40042
  if (e.axis === "time") this.openTimezoneDrawer();
@@ -39646,6 +40357,7 @@ var VelaWorkspace = class {
39646
40357
  for (const cell of this.cellsById.values()) {
39647
40358
  cell.chart.renderer.closeDialogs();
39648
40359
  cell.chart.renderer.setLayoutMode(mode);
40360
+ cell.setControlsSuspended(mode === "mobile");
39649
40361
  }
39650
40362
  this.syncCellPresentation();
39651
40363
  }