@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
@@ -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;
@@ -13673,6 +13755,17 @@ var PanelDock = class {
13673
13755
  for (const entry of this.entries) this.deps.chrome.setPanelActive(entry.id, entry.panel.open);
13674
13756
  }
13675
13757
  };
13758
+ function foldGroups(list) {
13759
+ const groupsAbove = /* @__PURE__ */ new Set();
13760
+ const out = [];
13761
+ for (const s of list) {
13762
+ if (isGroupRow(s)) {
13763
+ groupsAbove.add(groupKeyOf(s));
13764
+ out.push(s);
13765
+ } else if (s.group == null || !groupsAbove.has(groupKeyOf(s))) out.push(s);
13766
+ }
13767
+ return out;
13768
+ }
13676
13769
  var TOP_TICKERS = ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT", "DOGEUSDT", "ADAUSDT", "LINKUSDT"];
13677
13770
  function parseQuery(raw, venues) {
13678
13771
  const m = raw.match(/^\s*([^\s:]+)\s*[:\s]\s*(.*)$/);
@@ -13804,6 +13897,21 @@ var CSS8 = `
13804
13897
  .vela-sp-badge[data-p='binance'] { color: #f0b90b; } /* palette-exempt: venue brand mark */
13805
13898
  .vela-sp-badge[data-p='hyperliquid'] { color: #50d2c1; } /* palette-exempt: venue brand mark */
13806
13899
  .vela-sp-empty { padding: var(--vela-space-3); color: var(--vela-fg-muted); text-align: center; }
13900
+ /* Grouped listings (futures roots): the chevron unfolds members inline, indented. */
13901
+ .vela-sp-expander {
13902
+ all: unset;
13903
+ flex: none;
13904
+ display: inline-flex;
13905
+ align-items: center;
13906
+ justify-content: center;
13907
+ width: 22px;
13908
+ height: 22px;
13909
+ border-radius: 5px;
13910
+ cursor: pointer;
13911
+ color: var(--vela-fg-muted);
13912
+ }
13913
+ .vela-sp-expander:hover { background: var(--vela-surface-elev); color: var(--vela-fg); }
13914
+ .vela-sp-row[data-member] { padding-left: 34px; }
13807
13915
  `;
13808
13916
  var PAGE = 100;
13809
13917
  var SymbolPicker = class {
@@ -13815,6 +13923,11 @@ var SymbolPicker = class {
13815
13923
  this.seed = "";
13816
13924
  this.activeTab = "All";
13817
13925
  this.visible = PAGE;
13926
+ /** The last filter pass returned fewer raw rows than asked — the pool is drained
13927
+ * (checked BEFORE folding: folding shortens pages without meaning exhaustion). */
13928
+ this.exhausted = false;
13929
+ /** Group rows currently expanded (venue-scoped keys) — members shown inline. */
13930
+ this.expanded = /* @__PURE__ */ new Set();
13818
13931
  /** The ranked pool cache — `key` fingerprints the raw pool the ranking ran on. */
13819
13932
  this.ranked = null;
13820
13933
  this.ranking = false;
@@ -13829,7 +13942,7 @@ var SymbolPicker = class {
13829
13942
  searchRow.append(iconEl("search", doc), this.input);
13830
13943
  this.tabs = doc.createElement("div");
13831
13944
  this.tabs.className = "vela-sp-tabs";
13832
- for (const t of ["All", "Stocks", "ETFs", "Crypto", "Forex", "Commodities"]) {
13945
+ for (const t of ["All", "Stocks", "ETFs", "Crypto", "Futures", "Forex", "Commodities"]) {
13833
13946
  const b = doc.createElement("button");
13834
13947
  b.className = "vela-sp-tab";
13835
13948
  b.textContent = t;
@@ -13845,7 +13958,7 @@ var SymbolPicker = class {
13845
13958
  this.list = doc.createElement("div");
13846
13959
  this.list.className = "vela-sp-list";
13847
13960
  this.list.addEventListener("scroll", () => {
13848
- if (this.rows.length < this.visible) return;
13961
+ if (this.exhausted) return;
13849
13962
  if (this.list.scrollTop + this.list.clientHeight < this.list.scrollHeight - 200) return;
13850
13963
  this.visible += PAGE;
13851
13964
  this.grow();
@@ -13874,14 +13987,19 @@ var SymbolPicker = class {
13874
13987
  else if (e.key === "ArrowUp") this.moveHighlight(-1);
13875
13988
  else if (e.key === "Enter") {
13876
13989
  const pick = this.rows[this.highlighted];
13877
- if (pick) this.select(pick.ticker, pick.prefix ?? pick.provider, opts.onSelect);
13990
+ if (pick) this.pick(pick);
13878
13991
  return;
13879
13992
  } else return;
13880
13993
  e.preventDefault();
13881
13994
  });
13882
13995
  this.list.addEventListener("click", (e) => {
13883
- const row = e.target.closest(".vela-sp-row");
13884
- if (row?.dataset.ticker) this.select(row.dataset.ticker, row.dataset.venue, opts.onSelect);
13996
+ const target = e.target;
13997
+ const row = target.closest(".vela-sp-row");
13998
+ if (!row) return;
13999
+ const s = this.rows[Number(row.dataset.i)];
14000
+ if (!s) return;
14001
+ if (target.closest(".vela-sp-expander")) this.toggleExpand(s);
14002
+ else this.pick(s);
13885
14003
  });
13886
14004
  }
13887
14005
  /** Wire where symbols come from (re-called on every widget rebuild). */
@@ -13898,6 +14016,27 @@ var SymbolPicker = class {
13898
14016
  destroy() {
13899
14017
  this.dialog.destroy();
13900
14018
  }
14019
+ /** Route a row activation: a GROUP row loads its default member (the root itself is
14020
+ * listed, never loadable), any other row loads itself. */
14021
+ pick(s) {
14022
+ const target = isGroupRow(s) ? defaultMemberOf(this.pool(), s) ?? s : s;
14023
+ this.select(target.ticker, target.prefix ?? target.provider, this.opts.onSelect);
14024
+ }
14025
+ /** Expand/collapse a group row IN PLACE — same query, same page, same scroll; only
14026
+ * the member rows under the group appear or go. */
14027
+ toggleExpand(s) {
14028
+ const key = groupKeyOf(s);
14029
+ if (!this.expanded.delete(key)) this.expanded.add(key);
14030
+ const scrollTop = this.list.scrollTop;
14031
+ const focus = this.rows[this.highlighted];
14032
+ this.rows = this.computeRows();
14033
+ this.list.replaceChildren();
14034
+ this.rows.forEach((r, i) => this.list.appendChild(this.rowEl(r, i)));
14035
+ const at = focus ? this.rows.indexOf(focus) : -1;
14036
+ this.highlighted = at >= 0 ? at : Math.min(this.highlighted, Math.max(0, this.rows.length - 1));
14037
+ this.renderHighlight();
14038
+ this.list.scrollTop = scrollTop;
14039
+ }
13901
14040
  select(ticker, venue, onSelect) {
13902
14041
  this.close();
13903
14042
  onSelect(venue ? `${venue}:${ticker}` : ticker);
@@ -13924,10 +14063,26 @@ var SymbolPicker = class {
13924
14063
  return raw;
13925
14064
  }
13926
14065
  computeRows() {
13927
- const TAB_TYPES = { Crypto: ["crypto"], Stocks: ["stock"], ETFs: ["etf"], Forex: ["forex"], Commodities: ["commodity"] };
14066
+ const TAB_TYPES = { Crypto: ["crypto"], Stocks: ["stock"], ETFs: ["etf"], Futures: ["futures", "root"], Forex: ["forex"], Commodities: ["commodity"] };
13928
14067
  const all = this.pool();
13929
14068
  const pool = this.activeTab === "All" ? all : all.filter((s) => TAB_TYPES[this.activeTab]?.includes((s.type ?? "").toLowerCase()) || this.activeTab === "Crypto" && (s.type ?? "").toLowerCase() === "futures");
13930
- return filterSymbols(pool, this.input.value, this.visible, TOP_TICKERS);
14069
+ const filtered = filterSymbols(pool, this.input.value, this.visible, TOP_TICKERS);
14070
+ this.exhausted = filtered.length < this.visible;
14071
+ const folded = foldGroups(filtered);
14072
+ if (!this.expanded.size) return folded;
14073
+ const keyOf = (s) => `${(s.prefix ?? s.provider ?? "").toLowerCase()}:${s.ticker.toUpperCase()}`;
14074
+ const present = new Set(folded.map(keyOf));
14075
+ const out = [];
14076
+ for (const s of folded) {
14077
+ out.push(s);
14078
+ if (!isGroupRow(s) || !this.expanded.has(groupKeyOf(s))) continue;
14079
+ for (const m of groupMembers(all, s)) {
14080
+ if (present.has(keyOf(m))) continue;
14081
+ present.add(keyOf(m));
14082
+ out.push(m);
14083
+ }
14084
+ }
14085
+ return out;
13931
14086
  }
13932
14087
  refresh() {
13933
14088
  const doc = this.list.ownerDocument;
@@ -13942,19 +14097,20 @@ var SymbolPicker = class {
13942
14097
  this.list.appendChild(empty);
13943
14098
  return;
13944
14099
  }
13945
- for (const s of this.rows) this.list.appendChild(this.rowEl(s));
14100
+ this.rows.forEach((s, i) => this.list.appendChild(this.rowEl(s, i)));
13946
14101
  this.renderHighlight();
13947
14102
  }
13948
14103
  /** Append the page the grown `visible` just uncovered — rows already on screen stay put. */
13949
14104
  grow() {
13950
14105
  const already = this.rows.length;
13951
14106
  this.rows = this.computeRows();
13952
- for (const s of this.rows.slice(already)) this.list.appendChild(this.rowEl(s));
14107
+ this.rows.slice(already).forEach((s, j) => this.list.appendChild(this.rowEl(s, already + j)));
13953
14108
  }
13954
- rowEl(s) {
14109
+ rowEl(s, i) {
13955
14110
  const doc = this.list.ownerDocument;
13956
14111
  const row = doc.createElement("div");
13957
14112
  row.className = "vela-sp-row";
14113
+ row.dataset.i = String(i);
13958
14114
  row.dataset.ticker = s.ticker;
13959
14115
  const venue = s.prefix ?? s.provider;
13960
14116
  if (venue) row.dataset.venue = venue;
@@ -13969,6 +14125,14 @@ var SymbolPicker = class {
13969
14125
  d.textContent = s.description ?? (s.type ?? "");
13970
14126
  main.append(t, d);
13971
14127
  row.append(av, main);
14128
+ if (isGroupRow(s)) {
14129
+ row.dataset.group = "1";
14130
+ const expander = doc.createElement("button");
14131
+ expander.className = "vela-sp-expander";
14132
+ expander.setAttribute("aria-label", "Show contracts");
14133
+ expander.appendChild(iconEl(this.expanded.has(groupKeyOf(s)) ? "chevron-down" : "chevron-right", doc));
14134
+ row.appendChild(expander);
14135
+ } else if (s.group != null && this.expanded.has(groupKeyOf(s))) row.dataset.member = "1";
13972
14136
  if (venue) {
13973
14137
  const badge = doc.createElement("span");
13974
14138
  badge.className = "vela-sp-badge";
@@ -14585,6 +14749,9 @@ var CSS13 = `
14585
14749
  }
14586
14750
  .vela-mb-item:active { background: var(--vela-hover); }
14587
14751
  .vela-mb-item .vela-icon { font-size: 18px; width: 18px; height: 18px; }
14752
+ /* A lit stop (the maximize toggle while something is isolated): the inverse
14753
+ "selected" chip \u2014 white on the dark theme, dark on the light one. */
14754
+ .vela-mb-item.vela-mb-on, .vela-mb-item.vela-mb-on:active { background: var(--vela-selected-bg); color: var(--vela-selected-fg); }
14588
14755
  /* Left-aligned contributed actions get their own stops (the built-in indicators
14589
14756
  slot) \u2014 the wrapper is layout-transparent so each stop flexes like a sibling. */
14590
14757
  .vela-mb-actions { display: contents; }
@@ -14625,9 +14792,11 @@ var MobileBar = class {
14625
14792
  this.actionsHost.className = "vela-mb-actions";
14626
14793
  const onDrawings = opts.onDrawingsClick;
14627
14794
  const drawings = onDrawings ? item("vela-mb-drawings", "Drawings", onDrawings, "pen") : null;
14795
+ const onMaximize = opts.onMaximizeClick;
14796
+ this.maxEl = onMaximize ? item("vela-mb-maximize", "Maximize chart", onMaximize, "maximize") : null;
14628
14797
  const more = item("vela-mb-more", "More", opts.onMoreClick, "kebab");
14629
14798
  const settings = item("vela-mb-settings", "Chart settings", opts.onSettingsClick, "gear");
14630
- this.el.append(this.symbolEl, this.tfEl, ...indicators ? [indicators] : [], this.actionsHost, ...drawings ? [drawings] : [], more, settings);
14799
+ this.el.append(this.symbolEl, this.tfEl, ...indicators ? [indicators] : [], this.actionsHost, ...drawings ? [drawings] : [], ...this.maxEl ? [this.maxEl] : [], more, settings);
14631
14800
  host.appendChild(this.el);
14632
14801
  this.renderActions();
14633
14802
  }
@@ -14659,6 +14828,14 @@ var MobileBar = class {
14659
14828
  setTimeframe(tf) {
14660
14829
  this.tfEl.textContent = timeframeLabel(tf);
14661
14830
  }
14831
+ /** Light the maximize stop while something is isolated (a chart over the grid,
14832
+ * or a maximized pane inside the active chart) — inverse chip + restore glyph. */
14833
+ setMaximizeActive(on) {
14834
+ if (!this.maxEl) return;
14835
+ this.maxEl.classList.toggle("vela-mb-on", on);
14836
+ this.maxEl.setAttribute("aria-label", on ? "Restore layout" : "Maximize chart");
14837
+ this.maxEl.replaceChildren(iconEl(on ? "restore" : "maximize", this.el.ownerDocument));
14838
+ }
14662
14839
  destroy() {
14663
14840
  this.el.remove();
14664
14841
  }
@@ -16836,7 +17013,7 @@ var DrawingToolbar = class {
16836
17013
  this.root.replaceChildren();
16837
17014
  this.groupCells.clear();
16838
17015
  this.groupIcons.clear();
16839
- this.cursorBtn = this.makeButton(CURSOR_ICON, "Cursor", () => this.onArm(null));
17016
+ this.cursorBtn = this.makeButton(CURSOR_ICON, "Cursor", () => this.onCursorClick());
16840
17017
  this.root.appendChild(this.cursorBtn);
16841
17018
  if (this.def.groups.length > 0) this.root.appendChild(this.divider());
16842
17019
  for (const g of this.def.groups) {
@@ -16940,6 +17117,14 @@ var DrawingToolbar = class {
16940
17117
  this.magnetIcon = icon2;
16941
17118
  return cell;
16942
17119
  }
17120
+ /** Cursor returns to select/idle: an active measure/eraser mode exits through its own
17121
+ * toggle callback (disarming a tool via `onArm(null)` alone can't — the host treats a
17122
+ * null arm as a no-op side effect of entering those modes), then the tool disarms. */
17123
+ onCursorClick() {
17124
+ if (this.measureActive) this.onMeasure();
17125
+ if (this.eraserActive) this.onEraser();
17126
+ this.onArm(null);
17127
+ }
16943
17128
  /** Clicking the icon arms the group's last-used tool (it does NOT open the flyout). */
16944
17129
  onGroupIconClick(group) {
16945
17130
  const type = this.lastUsed.get(group.id) ?? group.tools[0]?.type;
@@ -17952,6 +18137,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
17952
18137
  /** Invalidates detached async work (backfill loops, in-flight loads, gap heals):
17953
18138
  * bumped by init(), setMarket() and destroy(). */
17954
18139
  this.generation = 0;
18140
+ /** Aborts the in-flight PROGRESSIVE load's source polling on supersession — an
18141
+ * abandoned stream left polling to its own budget starves the browser's per-host
18142
+ * connection pool, and the NEXT symbol's very first fetch with it (measured). */
18143
+ this.progressiveAbort = null;
17955
18144
  /** Awaiters racing a superseded load (setMarket callers) — released on every bump so they never hang. */
17956
18145
  this.supersedeWaiters = [];
17957
18146
  /** `history:complete` fired for the CURRENT load. Each market load re-arms the cycle
@@ -18094,6 +18283,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
18094
18283
  * superseded setMarket awaiters so their promises resolve instead of hanging. */
18095
18284
  bumpGeneration() {
18096
18285
  const gen = ++this.generation;
18286
+ this.progressiveAbort?.abort();
18287
+ this.progressiveAbort = null;
18097
18288
  for (const w of this.supersedeWaiters.splice(0)) w();
18098
18289
  return gen;
18099
18290
  }
@@ -18147,7 +18338,47 @@ var EngineOrchestrator = class _EngineOrchestrator {
18147
18338
  const requested = market.bars ?? 500;
18148
18339
  const initialRange = market.visibleRange;
18149
18340
  const deep = !market.data?.length && initialRange == null && requested > SINGLE_LOAD_BARS;
18150
- if (deep && this.feed.loadRange) {
18341
+ let progressiveServed = false;
18342
+ if (!market.data?.length && initialRange == null && this.feed.loadProgressive) {
18343
+ let painted = false;
18344
+ const paint = (bars, final) => {
18345
+ if (this.generation !== gen || !final && bars.length === 0) return;
18346
+ this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
18347
+ if (!painted && bars.length > 0) {
18348
+ painted = true;
18349
+ if (opts.firstLoad) this.activateBarLayers();
18350
+ if (!final) this.historyState = "backfill";
18351
+ }
18352
+ };
18353
+ const abort = new AbortController();
18354
+ this.progressiveAbort = abort;
18355
+ progressiveServed = await new Promise((firstPaint) => {
18356
+ let signaled = false;
18357
+ const signal = (served) => {
18358
+ if (!signaled) {
18359
+ signaled = true;
18360
+ firstPaint(served);
18361
+ }
18362
+ };
18363
+ abort.signal.addEventListener("abort", () => signal(true), { once: true });
18364
+ this.feed.loadProgressive(market, (bars) => {
18365
+ paint(bars, false);
18366
+ if (painted) signal(true);
18367
+ }, { signal: abort.signal }).then((full) => {
18368
+ if (this.progressiveAbort === abort) this.progressiveAbort = null;
18369
+ if (full == null) return signal(false);
18370
+ if (this.generation !== gen) return signal(true);
18371
+ paint(full, true);
18372
+ this.completeHistory(full.length >= requested ? "depth" : "genesis");
18373
+ signal(true);
18374
+ }).catch(() => {
18375
+ if (this.progressiveAbort === abort) this.progressiveAbort = null;
18376
+ if (this.generation === gen) this.completeHistory("aborted");
18377
+ signal(true);
18378
+ });
18379
+ });
18380
+ }
18381
+ if (progressiveServed) ; else if (deep && this.feed.loadRange) {
18151
18382
  const head = await this.feed.load({ ...market, bars: Math.min(requested, CHUNK_BARS) });
18152
18383
  if (this.generation !== gen) return;
18153
18384
  this.setBarSeries(head);
@@ -20890,6 +21121,9 @@ var CLOSE_SVG = iconAt("close", LEGEND_ICON_PX2);
20890
21121
  var FOLD_SVG = iconAt("chevron-up", LEGEND_ICON_PX2);
20891
21122
  var UNFOLD_SVG = iconAt("chevron-down", LEGEND_ICON_PX2);
20892
21123
  var OVERVIEW_SVG = iconAt("objects", LEGEND_ICON_PX2);
21124
+ function legendCalloutsDisplay(open2, hasCallouts) {
21125
+ return !open2 && hasCallouts ? "inline-flex" : "none";
21126
+ }
20893
21127
  var InputsUI = class {
20894
21128
  constructor(container, theme, paneBoundsOf) {
20895
21129
  this.container = container;
@@ -21093,7 +21327,7 @@ var InputsUI = class {
21093
21327
  row.callouts = [];
21094
21328
  row.calloutsEl.replaceChildren();
21095
21329
  const views = this.legendCallouts?.(row.id) ?? [];
21096
- row.calloutsEl.style.display = views.length > 0 ? "inline-flex" : "none";
21330
+ row.calloutsEl.style.display = legendCalloutsDisplay(row.highlighted, views.length > 0);
21097
21331
  for (const view of views) {
21098
21332
  const bubble = new CalloutBubble({
21099
21333
  icon: view.icon,
@@ -21577,11 +21811,11 @@ var InputsUI = class {
21577
21811
  row.controlsEl.style.display = open2 || row.hidden ? "inline-flex" : "none";
21578
21812
  if (open2) {
21579
21813
  row.el.appendChild(row.statusEl);
21580
- row.el.appendChild(row.calloutsEl);
21814
+ for (const bubble of row.callouts) bubble.hidePanel();
21581
21815
  } else {
21582
21816
  row.el.insertBefore(row.statusEl, row.valuesEl);
21583
- row.el.insertBefore(row.calloutsEl, row.statusEl);
21584
21817
  }
21818
+ row.calloutsEl.style.display = legendCalloutsDisplay(open2, row.callouts.length > 0);
21585
21819
  for (const child of Array.from(row.controlsEl.children)) {
21586
21820
  if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
21587
21821
  if (child === row.extrasEl) {
@@ -21671,7 +21905,7 @@ var ICONS = {
21671
21905
  };
21672
21906
  var STYLE_ID21 = "vela-pane-controls";
21673
21907
  var ICON_PX = 12;
21674
- var CLUSTER_PILL = "rgba(0,0,0,0.28)";
21908
+ var CLUSTER_PILL = "rgba(0,0,0,0.65)";
21675
21909
  function ensureStyles3() {
21676
21910
  if (typeof document === "undefined" || document.getElementById(STYLE_ID21)) return;
21677
21911
  const st = document.createElement("style");
@@ -21691,8 +21925,12 @@ var PaneControls = class {
21691
21925
  this.deps = deps;
21692
21926
  this.clusters = /* @__PURE__ */ new Map();
21693
21927
  this.hoverPaneId = null;
21928
+ /** Mobile: hover clusters are meaningless without a cursor — suppressed; a
21929
+ * collapsed pane's standalone expand chip stays (the only way back up). */
21930
+ this.suspended = false;
21694
21931
  /** Reveal the cluster for the pane under the cursor, resolved from the pointer's y in the plot. */
21695
21932
  this.onPlotMove = (e) => {
21933
+ if (this.suspended) return;
21696
21934
  const rect = this.plot.getBoundingClientRect();
21697
21935
  const y = e.clientY - rect.top;
21698
21936
  let hit = null;
@@ -21764,7 +22002,12 @@ var PaneControls = class {
21764
22002
  }
21765
22003
  if (p.count > 1) {
21766
22004
  cluster.appendChild(
21767
- this.button(p.maximized ? ICONS.restore : ICONS.maximize, p.maximized ? "Restore pane" : "Maximize pane", false, () => this.deps.onToggleMaximize(p.id), { role: "maximize" })
22005
+ this.button(p.maximized ? ICONS.restore : ICONS.maximize, p.maximized ? "Restore pane" : "Maximize pane", false, () => this.deps.onToggleMaximize(p.id), {
22006
+ role: "maximize",
22007
+ // Same inverse-chip treatment as the collapsed pane's expand toggle: the
22008
+ // maximized state must read as an active state, not just a swapped glyph.
22009
+ selected: p.maximized
22010
+ })
21768
22011
  );
21769
22012
  }
21770
22013
  }
@@ -21801,17 +22044,18 @@ var PaneControls = class {
21801
22044
  }
21802
22045
  const hovered = id === this.hoverPaneId;
21803
22046
  const hasButtons = cluster.children.length > 0;
21804
- const visible = hasButtons && (hovered || p.collapsed) && p.height > 8;
22047
+ const stateChipRole = p.collapsed ? "collapse" : !this.suspended && p.maximized ? "maximize" : null;
22048
+ const visible = hasButtons && (hovered || stateChipRole != null) && p.height > 8;
21805
22049
  cluster.style.right = `${rightPx}px`;
21806
22050
  cluster.style.top = p.collapsed ? `${p.top + Math.max(1, Math.round((p.height - 24) / 2))}px` : `${p.top + 4}px`;
21807
22051
  cluster.style.display = visible ? "flex" : "none";
21808
22052
  if (!visible) continue;
21809
- const soloExpand = p.collapsed && !hovered;
21810
- cluster.style.background = soloExpand ? "transparent" : CLUSTER_PILL;
22053
+ const soloChip = stateChipRole != null && !hovered;
22054
+ cluster.style.background = soloChip ? "transparent" : CLUSTER_PILL;
21811
22055
  for (const child of cluster.children) {
21812
22056
  const btn2 = child;
21813
22057
  btn2.style.display = "inline-flex";
21814
- btn2.style.visibility = soloExpand && btn2.dataset.role !== "collapse" ? "hidden" : "visible";
22058
+ btn2.style.visibility = soloChip && btn2.dataset.role !== stateChipRole ? "hidden" : "visible";
21815
22059
  }
21816
22060
  }
21817
22061
  }
@@ -21821,6 +22065,14 @@ var PaneControls = class {
21821
22065
  this.hoverPaneId = paneId;
21822
22066
  this.reposition();
21823
22067
  }
22068
+ /** Mobile suppression: no hover clusters (touch has no cursor; the shell's own
22069
+ * chrome covers maximize), while collapsed panes keep their expand chips. */
22070
+ setSuspended(on) {
22071
+ if (on === this.suspended) return;
22072
+ this.suspended = on;
22073
+ if (on) this.hoverPaneId = null;
22074
+ this.reposition();
22075
+ }
21824
22076
  destroy() {
21825
22077
  this.plot.removeEventListener("pointermove", this.onPlotMove);
21826
22078
  this.plot.removeEventListener("pointerleave", this.onPlotLeave);
@@ -21971,160 +22223,6 @@ var AxisScaleButtons = class {
21971
22223
  }
21972
22224
  };
21973
22225
 
21974
- // src/renderers/shared/TableOverlay.ts
21975
- var SIZE_PX3 = {
21976
- auto: 13,
21977
- tiny: 10,
21978
- small: 11,
21979
- normal: 13,
21980
- large: 16,
21981
- huge: 20
21982
- };
21983
- function fontPxOf(size) {
21984
- if (typeof size === "number") return size > 0 ? size : SIZE_PX3.auto;
21985
- return SIZE_PX3[size] ?? SIZE_PX3.auto;
21986
- }
21987
- function tableHasContent(t) {
21988
- return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
21989
- }
21990
- function mergeRenderPlan(t) {
21991
- const span = /* @__PURE__ */ new Map();
21992
- const omit = /* @__PURE__ */ new Set();
21993
- for (const m of t.merges) {
21994
- span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
21995
- for (let r = m.startRow; r <= m.endRow; r += 1) {
21996
- for (let c = m.startCol; c <= m.endCol; c += 1) {
21997
- if (r !== m.startRow || c !== m.startCol) omit.add(`${r}:${c}`);
21998
- }
21999
- }
22000
- }
22001
- for (let r = 0; r < t.rows; r += 1) {
22002
- for (let c = 0; c < t.columns; c += 1) {
22003
- if (t.cells[r]?.[c]?.merged && !span.has(`${r}:${c}`)) omit.add(`${r}:${c}`);
22004
- }
22005
- }
22006
- for (const key of span.keys()) omit.delete(key);
22007
- return { span, omit };
22008
- }
22009
- var TableOverlay = class {
22010
- constructor(container, theme, paneBounds) {
22011
- this.container = container;
22012
- this.theme = theme;
22013
- this.paneBounds = paneBounds;
22014
- this.lastTables = [];
22015
- if (getComputedStyle(container).position === "static") container.style.position = "relative";
22016
- this.root = document.createElement("div");
22017
- Object.assign(this.root.style, {
22018
- position: "absolute",
22019
- inset: "0",
22020
- pointerEvents: "none",
22021
- overflow: "hidden",
22022
- zIndex: "3"
22023
- });
22024
- container.appendChild(this.root);
22025
- }
22026
- update(tables) {
22027
- this.lastTables = tables;
22028
- this.root.replaceChildren();
22029
- for (const t of tables) {
22030
- if (tableHasContent(t)) this.root.appendChild(this.renderTable(t));
22031
- }
22032
- }
22033
- /** Re-render at the current pane geometry — after layout settles or on resize. */
22034
- reposition() {
22035
- if (this.root.isConnected) this.update(this.lastTables);
22036
- }
22037
- /** Show/hide the whole overlay. Tables anchor to pane corners, not to bars, so unlike the
22038
- * series content they DON'T vanish with an emptied chart — the loading state hides them. */
22039
- setVisible(visible) {
22040
- this.root.style.display = visible ? "" : "none";
22041
- }
22042
- destroy() {
22043
- this.root.remove();
22044
- }
22045
- renderTable(t) {
22046
- const b = this.paneBounds(t.paneId);
22047
- const wrap = document.createElement("div");
22048
- wrap.style.position = "absolute";
22049
- if (t.frameColor && t.frameWidth > 0) wrap.style.border = `${t.frameWidth}px solid ${t.frameColor}`;
22050
- this.anchor(wrap, t.position, b);
22051
- const table = document.createElement("table");
22052
- Object.assign(table.style, {
22053
- borderCollapse: "collapse",
22054
- background: t.bgColor ?? "transparent",
22055
- fontFamily: this.theme.fontFamily || "sans-serif",
22056
- border: "none",
22057
- tableLayout: "auto",
22058
- // Re-enable pointer events on the table only (root is none) so cell tooltips work.
22059
- pointerEvents: "auto"
22060
- });
22061
- const { span, omit } = mergeRenderPlan(t);
22062
- const plotW = Math.max(0, (this.root.clientWidth || this.container.clientWidth) - b.rightAxis);
22063
- const paneH = b.height;
22064
- const cellBorder = t.borderColor && t.borderWidth > 0 ? `${t.borderWidth}px solid ${t.borderColor}` : "none";
22065
- for (let r = 0; r < t.rows; r += 1) {
22066
- const tr = document.createElement("tr");
22067
- for (let c = 0; c < t.columns; c += 1) {
22068
- if (omit.has(`${r}:${c}`)) continue;
22069
- const cell = t.cells[r]?.[c] ?? null;
22070
- const td = document.createElement("td");
22071
- if (cell === null) {
22072
- Object.assign(td.style, { padding: "0", border: "none" });
22073
- tr.appendChild(td);
22074
- continue;
22075
- }
22076
- const sp = span.get(`${r}:${c}`);
22077
- if (sp) {
22078
- if (sp.cs > 1) td.colSpan = sp.cs;
22079
- if (sp.rs > 1) td.rowSpan = sp.rs;
22080
- }
22081
- Object.assign(td.style, {
22082
- border: cellBorder,
22083
- padding: "2px 6px",
22084
- background: cell.bgColor ?? "transparent",
22085
- color: cell.textColor ?? this.theme.textColor,
22086
- textAlign: cell.hAlign,
22087
- verticalAlign: cell.vAlign === "top" ? "top" : cell.vAlign === "bottom" ? "bottom" : "middle",
22088
- fontSize: `${fontPxOf(cell.textSize)}px`,
22089
- fontFamily: cell.fontFamily === "monospace" ? "monospace" : "inherit",
22090
- fontWeight: cell.bold ? "bold" : "normal",
22091
- fontStyle: cell.italic ? "italic" : "normal",
22092
- // Pine cell text never wraps; `\n` still breaks lines. Wrapping
22093
- // used to collapse unicode sparklines and ━━━ dividers.
22094
- whiteSpace: "pre"
22095
- });
22096
- if (cell.width) td.style.width = `${cell.width / 100 * plotW}px`;
22097
- if (cell.height) td.style.height = `${cell.height / 100 * paneH}px`;
22098
- if (cell.tooltip) td.title = cell.tooltip;
22099
- td.textContent = cell.text ?? "";
22100
- tr.appendChild(td);
22101
- }
22102
- table.appendChild(tr);
22103
- }
22104
- wrap.appendChild(table);
22105
- return wrap;
22106
- }
22107
- /**
22108
- * Position the wrapper at a Pine `position.*` corner/edge of the table's PANE
22109
- * (not the whole chart), inset past the right price axis so it never overlaps
22110
- * the Y-axis labels.
22111
- */
22112
- anchor(el, position, b) {
22113
- const m = 6;
22114
- const containerH = this.root.clientHeight || this.container.clientHeight;
22115
- const paneBottom = b.top + b.height;
22116
- if (position.startsWith("top")) el.style.top = `${b.top + m}px`;
22117
- else if (position.startsWith("bottom")) el.style.bottom = `${Math.max(0, containerH - paneBottom) + m}px`;
22118
- else el.style.top = `${b.top + b.height / 2}px`;
22119
- if (position.endsWith("left")) el.style.left = `${m}px`;
22120
- else if (position.endsWith("right")) el.style.right = `${b.rightAxis + m}px`;
22121
- else el.style.left = `calc(50% - ${b.rightAxis / 2}px)`;
22122
- const tx = position.endsWith("center") ? "-50%" : "0";
22123
- const ty = position.startsWith("middle") ? "-50%" : "0";
22124
- if (tx !== "0" || ty !== "0") el.style.transform = `translate(${tx}, ${ty})`;
22125
- }
22126
- };
22127
-
22128
22226
  // src/renderers/native/capabilities.ts
22129
22227
  var NATIVE_CAPABILITIES = {
22130
22228
  panes: true,
@@ -22145,7 +22243,7 @@ var NATIVE_CAPABILITIES = {
22145
22243
  drawingDepth: true,
22146
22244
  // drawings share the series' z space (backend-composited interleave layers)
22147
22245
  tables: true,
22148
- // reuses the DOM TableOverlay
22246
+ // canvas-painted into the owning indicator's interleave slice
22149
22247
  trades: true,
22150
22248
  // strategy order-fill markers (arrows + labels + fill-price ticks)
22151
22249
  inputsUI: true
@@ -22176,6 +22274,9 @@ function candleTier(spacing) {
22176
22274
  if (spacing < CANDLE_BODY_MIN_SPACING) return "wick";
22177
22275
  return "full";
22178
22276
  }
22277
+ function snapY(yCss, dpr) {
22278
+ return Math.round(yCss * dpr) / dpr;
22279
+ }
22179
22280
  function candleGeometry(xCss, spacing, dpr, bodyScale = 1) {
22180
22281
  const wickDev = Math.max(1, Math.round(wickWidth(spacing) * dpr));
22181
22282
  const wickLeftDev = Math.round(xCss * dpr - wickDev / 2);
@@ -22699,11 +22800,9 @@ var WebGL2Backend = class {
22699
22800
  };
22700
22801
  b.alpha = this.modelAlpha;
22701
22802
  for (const m of models) for (const bgSpan of m.backgrounds) if (bgSpan.overlay !== true) this.emitBackground(b, bgSpan, pane, coords);
22702
- 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));
22703
22803
  if (isPrice) {
22704
22804
  for (const m of scene.indicators.values()) {
22705
22805
  for (const bgSpan of m.backgrounds) if (bgSpan.overlay === true) this.emitBackground(b, bgSpan, pane, coords);
22706
- for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, i1, scene.offsetOf(m.id));
22707
22806
  }
22708
22807
  }
22709
22808
  const drawCandles = isPrice && !scene.candlesHidden;
@@ -22719,6 +22818,7 @@ var WebGL2Backend = class {
22719
22818
  b.alpha = this.modelAlpha;
22720
22819
  const off = scene.offsetOf(m.id);
22721
22820
  const mp = effPane(m);
22821
+ for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, mp, coords, i0, i1, off);
22722
22822
  for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
22723
22823
  }
22724
22824
  if (drawCandles && !candleDrawn) {
@@ -22726,14 +22826,18 @@ var WebGL2Backend = class {
22726
22826
  b.alpha = this.candleStructureAlpha;
22727
22827
  this.emitPriceSeries(b, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
22728
22828
  }
22729
- drawSlicesUpTo(Infinity);
22730
22829
  b.alpha = this.modelAlpha;
22731
22830
  if (isPrice) {
22831
+ for (const m of scene.indicators.values()) {
22832
+ const off = scene.offsetOf(m.id);
22833
+ for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, i1, off);
22834
+ }
22732
22835
  for (const m of scene.indicators.values()) {
22733
22836
  const off = scene.offsetOf(m.id);
22734
22837
  for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0, i1, theme, off);
22735
22838
  }
22736
22839
  }
22840
+ drawSlicesUpTo(Infinity);
22737
22841
  for (const m of models) {
22738
22842
  const mp = effPane(m);
22739
22843
  for (const pl of m.priceLines) this.emitHline(b, pl, mp, coords, dataW, theme);
@@ -23128,12 +23232,12 @@ var WebGL2Backend = class {
23128
23232
  if (drawBody) {
23129
23233
  const oY = coords.priceToY(bar.open, pane.scale, pane.bounds);
23130
23234
  const cY = coords.priceToY(bar.close, pane.scale, pane.bounds);
23131
- bodyTop = Math.min(oY, cY);
23132
- bodyH = Math.max(1, Math.abs(cY - oY));
23235
+ bodyTop = snapY(Math.min(oY, cY), coords.dpr);
23236
+ bodyH = Math.max(1 / coords.dpr, snapY(Math.max(oY, cY), coords.dpr) - bodyTop);
23133
23237
  }
23134
23238
  if (cs.wickVisible) {
23135
- const hY = coords.priceToY(bar.high, pane.scale, pane.bounds);
23136
- const lY = coords.priceToY(bar.low, pane.scale, pane.bounds);
23239
+ const hY = snapY(coords.priceToY(bar.high, pane.scale, pane.bounds), coords.dpr);
23240
+ const lY = snapY(coords.priceToY(bar.low, pane.scale, pane.bounds), coords.dpr);
23137
23241
  b.alpha = this.candleStructureAlpha;
23138
23242
  const wCol = parseColor((isUp ? cs.wickUpColor : cs.wickDownColor) ?? (drawBody ? dir : bodyColorStr));
23139
23243
  if (drawBody) {
@@ -24466,9 +24570,10 @@ var SceneGraph = class {
24466
24570
  * so each indicator arrives behind the candles (and behind older indicators);
24467
24571
  * `setIndicatorZ`/`bringToFront`/`sendToBack` change it. */
24468
24572
  this.seriesZ = /* @__PURE__ */ new Map();
24469
- /** Per-pane raster layers of user drawings interleaved into the series stack each is a
24573
+ /** Per-pane raster layers of drawings interleaved into the series stack (each
24574
+ * indicator's Pine drawings at its model's z, plus in-stack user drawings) — each a
24470
24575
  * prepainted canvas the backend composites just before the series carrying `beforeZ`.
24471
- * Rebuilt by the renderer per data frame; empty when every drawing sits over the stack. */
24576
+ * Rebuilt by the renderer per data frame. */
24472
24577
  this.drawingSlices = /* @__PURE__ */ new Map();
24473
24578
  /** Per-model index offset: the chart bar index of the model's `anchorTime` — its
24474
24579
  * index-aligned payloads (dense series arrays, `bar_index` drawings) count from that
@@ -24703,11 +24808,9 @@ var Canvas2dBackend = class {
24703
24808
  };
24704
24809
  ctx.globalAlpha = this.modelAlpha;
24705
24810
  for (const m of models) for (const bg of m.backgrounds) if (bg.overlay !== true) this.drawBackground(ctx, bg, pane, coords);
24706
- 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));
24707
24811
  if (isPrice) {
24708
24812
  for (const m of scene.indicators.values()) {
24709
24813
  for (const bg of m.backgrounds) if (bg.overlay === true) this.drawBackground(ctx, bg, pane, coords);
24710
- for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, scene.offsetOf(m.id));
24711
24814
  }
24712
24815
  }
24713
24816
  const slices = scene.drawingSlices.get(pane.id) ?? [];
@@ -24731,6 +24834,7 @@ var Canvas2dBackend = class {
24731
24834
  ctx.globalAlpha = this.modelAlpha;
24732
24835
  const off = scene.offsetOf(m.id);
24733
24836
  const mp = effPane(m);
24837
+ for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, mp, coords, i0, i1, off);
24734
24838
  for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
24735
24839
  }
24736
24840
  if (drawCandles && !candleDrawn) {
@@ -24738,14 +24842,18 @@ var Canvas2dBackend = class {
24738
24842
  ctx.globalAlpha = this.candleStructureAlpha;
24739
24843
  this.drawPriceSeries(ctx, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
24740
24844
  }
24741
- drawSlicesUpTo(Infinity);
24742
24845
  if (isPrice) {
24743
24846
  ctx.globalAlpha = this.modelAlpha;
24847
+ for (const m of scene.indicators.values()) {
24848
+ const off = scene.offsetOf(m.id);
24849
+ for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, off);
24850
+ }
24744
24851
  for (const m of scene.indicators.values()) {
24745
24852
  const off = scene.offsetOf(m.id);
24746
24853
  for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0, i1, theme, off);
24747
24854
  }
24748
24855
  }
24856
+ drawSlicesUpTo(Infinity);
24749
24857
  ctx.globalAlpha = this.modelAlpha;
24750
24858
  for (const m of models) {
24751
24859
  const mp = effPane(m);
@@ -24989,13 +25097,13 @@ var Canvas2dBackend = class {
24989
25097
  if (drawBody) {
24990
25098
  const oY = coords.priceToY(b.open, pane.scale, pane.bounds);
24991
25099
  const cY = coords.priceToY(b.close, pane.scale, pane.bounds);
24992
- top = Math.min(oY, cY);
24993
- bodyH = Math.max(1, Math.abs(cY - oY));
25100
+ top = snapY(Math.min(oY, cY), coords.dpr);
25101
+ bodyH = Math.max(1 / coords.dpr, snapY(Math.max(oY, cY), coords.dpr) - top);
24994
25102
  }
24995
25103
  if (cs.wickVisible) {
24996
25104
  const wick = (up ? cs.wickUpColor : cs.wickDownColor) ?? (drawBody ? dir : color);
24997
- const hY = coords.priceToY(b.high, pane.scale, pane.bounds);
24998
- const lY = coords.priceToY(b.low, pane.scale, pane.bounds);
25105
+ const hY = snapY(coords.priceToY(b.high, pane.scale, pane.bounds), coords.dpr);
25106
+ const lY = snapY(coords.priceToY(b.low, pane.scale, pane.bounds), coords.dpr);
24999
25107
  ctx.globalAlpha = this.candleStructureAlpha;
25000
25108
  ctx.strokeStyle = wick;
25001
25109
  ctx.lineWidth = g.wickW;
@@ -25442,6 +25550,19 @@ function autoFontSize(lines, boxW, boxH, bold) {
25442
25550
 
25443
25551
  // src/renderers/shared/DrawingSceneRenderer.ts
25444
25552
  var EMPTY_DRAWING_SET = { lines: [], boxes: [], labels: [], polylines: [], linefills: [] };
25553
+ function modelDrawingSet(m, overlay) {
25554
+ const want = (d) => Boolean(d.overlay) === overlay;
25555
+ return {
25556
+ lines: (m.lines ?? []).filter(want),
25557
+ boxes: (m.boxes ?? []).filter(want),
25558
+ labels: (m.labels ?? []).filter(want),
25559
+ polylines: (m.polylines ?? []).filter(want),
25560
+ linefills: (m.linefills ?? []).filter(want)
25561
+ };
25562
+ }
25563
+ function drawingSetEmpty(s) {
25564
+ return !s.lines.length && !s.boxes.length && !s.labels.length && !s.polylines.length && !s.linefills.length;
25565
+ }
25445
25566
  function fontSizePx(size) {
25446
25567
  return size === "auto" ? 12 : namedFontSize(size);
25447
25568
  }
@@ -26294,10 +26415,8 @@ var ChromeRenderer = class {
26294
26415
  this.ctx = null;
26295
26416
  // The color for axis tick labels — the host-passed surface text, set each frame in render().
26296
26417
  this.axisTextColor = DARK_THEME.textColor;
26297
- // Shared Pine-drawing renderer (line/box/label/polyline/linefill); widthCache persists.
26418
+ // Shared Pine-drawing renderer, used here for autoscale geometry only; widthCache persists.
26298
26419
  this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
26299
- // Tooltip hit-rects of every label drawn this frame, in plot coords (rebuilt per render).
26300
- this.labelTips = [];
26301
26420
  }
26302
26421
  mount(canvas) {
26303
26422
  this.canvas = canvas;
@@ -26321,8 +26440,8 @@ var ChromeRenderer = class {
26321
26440
  */
26322
26441
  paneDrawingsRange(ownModels, scene, isPricePane, vr) {
26323
26442
  let dr = null;
26324
- for (const m of ownModels) dr = unionRange(dr, this.drawingsRange(this.ownDrawings(m), vr, scene.offsetOf(m.id)));
26325
- if (isPricePane) for (const m of scene.indicators.values()) dr = unionRange(dr, this.drawingsRange(this.overlayDrawings(m), vr, scene.offsetOf(m.id)));
26443
+ for (const m of ownModels) dr = unionRange(dr, this.drawingsRange(modelDrawingSet(m, false), vr, scene.offsetOf(m.id)));
26444
+ if (isPricePane) for (const m of scene.indicators.values()) dr = unionRange(dr, this.drawingsRange(modelDrawingSet(m, true), vr, scene.offsetOf(m.id)));
26326
26445
  return dr;
26327
26446
  }
26328
26447
  /** Clear the chrome canvas and draw drawings + axes + current-price line.
@@ -26339,7 +26458,6 @@ var ChromeRenderer = class {
26339
26458
  const dataW = coords.width;
26340
26459
  const dataH = coords.height;
26341
26460
  this.axisTextColor = surface?.textColor ?? theme.textColor;
26342
- this.labelTips = [];
26343
26461
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
26344
26462
  ctx.clearRect(0, 0, fullW, fullH);
26345
26463
  if (surface && (fullW > dataW || fullH > dataH)) {
@@ -26355,17 +26473,6 @@ var ChromeRenderer = class {
26355
26473
  return;
26356
26474
  }
26357
26475
  const pricePane = panes.find((p) => p.kind === "price") ?? null;
26358
- for (const pane of panes) {
26359
- if (pane.collapsed) continue;
26360
- for (const m of scene.indicatorsForPane(pane.id)) {
26361
- const sc = scene.scaleFor(m, pane);
26362
- const mp = sc === pane.scale ? pane : { ...pane, scale: sc };
26363
- this.renderDrawings(ctx, coords, this.ownDrawings(m), mp, dataW, scene.offsetOf(m.id));
26364
- }
26365
- }
26366
- if (pricePane) {
26367
- for (const m of scene.indicators.values()) this.renderDrawings(ctx, coords, this.overlayDrawings(m), pricePane, dataW, scene.offsetOf(m.id));
26368
- }
26369
26476
  if (pricePane && !pricePane.collapsed && scene.tradeMarkers.visible) {
26370
26477
  for (const m of scene.indicators.values()) {
26371
26478
  if (m.trades?.length) this.renderTrades(ctx, coords, scene, theme, m.trades, pricePane, dataW);
@@ -26381,25 +26488,6 @@ var ChromeRenderer = class {
26381
26488
  this.canvas = null;
26382
26489
  this.ctx = null;
26383
26490
  }
26384
- // ── Pine-drawing helpers (own vs force_overlay routing) ──
26385
- ownDrawings(m) {
26386
- return {
26387
- lines: (m.lines ?? []).filter((d) => !d.overlay),
26388
- boxes: (m.boxes ?? []).filter((d) => !d.overlay),
26389
- labels: (m.labels ?? []).filter((d) => !d.overlay),
26390
- polylines: (m.polylines ?? []).filter((d) => !d.overlay),
26391
- linefills: (m.linefills ?? []).filter((d) => !d.overlay)
26392
- };
26393
- }
26394
- overlayDrawings(m) {
26395
- return {
26396
- lines: (m.lines ?? []).filter((d) => d.overlay),
26397
- boxes: (m.boxes ?? []).filter((d) => d.overlay),
26398
- labels: (m.labels ?? []).filter((d) => d.overlay),
26399
- polylines: (m.polylines ?? []).filter((d) => d.overlay),
26400
- linefills: (m.linefills ?? []).filter((d) => d.overlay)
26401
- };
26402
- }
26403
26491
  drawingsRange(set, vr, indexOffset = 0) {
26404
26492
  this.drawScene.setSet(set, indexOffset);
26405
26493
  if (this.drawScene.isEmpty()) return null;
@@ -26433,34 +26521,6 @@ var ChromeRenderer = class {
26433
26521
  );
26434
26522
  ctx.restore();
26435
26523
  }
26436
- renderDrawings(ctx, coords, set, pane, dataW, indexOffset = 0) {
26437
- this.drawScene.setSet(set, indexOffset);
26438
- if (this.drawScene.isEmpty()) return;
26439
- ctx.save();
26440
- ctx.translate(0, pane.bounds.top);
26441
- ctx.beginPath();
26442
- ctx.rect(0, 0, dataW, pane.bounds.height);
26443
- ctx.clip();
26444
- this.drawScene.render(
26445
- ctx,
26446
- dataW,
26447
- pane.bounds.height,
26448
- (l) => coords.logicalToX(l),
26449
- (price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
26450
- );
26451
- ctx.restore();
26452
- for (const r of this.drawScene.labelTipRegions()) {
26453
- this.labelTips.push({ ...r, top: r.top + pane.bounds.top, bottom: r.bottom + pane.bounds.top });
26454
- }
26455
- }
26456
- /** Tooltip of the topmost label under a plot-space point, or null. Fed by the last render. */
26457
- labelTooltipAt(x, y) {
26458
- for (let i = this.labelTips.length - 1; i >= 0; i -= 1) {
26459
- const r = this.labelTips[i];
26460
- if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return r.text;
26461
- }
26462
- return null;
26463
- }
26464
26524
  // ── axes ──
26465
26525
  drawPriceAxes(ctx, scene, coords, theme, dataW, panes) {
26466
26526
  ctx.strokeStyle = scene.style.borderColor ?? theme.borderColor;
@@ -30779,17 +30839,17 @@ function glyphIcon(glyph) {
30779
30839
  return textGlyph(String(glyph), 15);
30780
30840
  }
30781
30841
  function stampSizeIcon(size) {
30782
- return textGlyph("\u25CF", (SIZE_PX4[String(size)] ?? 13) + 4);
30842
+ return textGlyph("\u25CF", (SIZE_PX3[String(size)] ?? 13) + 4);
30783
30843
  }
30784
30844
  function sizeIcon(size) {
30785
30845
  return textGlyph(String(size).charAt(0).toUpperCase(), 15);
30786
30846
  }
30787
- var SIZE_PX4 = { small: 10, normal: 13, large: 16, huge: 20 };
30847
+ var SIZE_PX3 = { small: 10, normal: 13, large: 16, huge: 20 };
30788
30848
  function numbersSizeIcon(size) {
30789
- return textGlyph("12", (SIZE_PX4[String(size)] ?? 13) - 1, 16.5, 'font-weight="600"');
30849
+ return textGlyph("12", (SIZE_PX3[String(size)] ?? 13) - 1, 16.5, 'font-weight="600"');
30790
30850
  }
30791
30851
  function labelSizeIcon(size) {
30792
- return textGlyph("T", (SIZE_PX4[String(size)] ?? 13) + 2);
30852
+ return textGlyph("T", (SIZE_PX3[String(size)] ?? 13) + 2);
30793
30853
  }
30794
30854
  function capitalize(s) {
30795
30855
  return s.charAt(0).toUpperCase() + s.slice(1);
@@ -31764,6 +31824,315 @@ var UserDrawingController = class {
31764
31824
  }
31765
31825
  };
31766
31826
 
31827
+ // src/renderers/shared/TableOverlay.ts
31828
+ var SIZE_PX4 = {
31829
+ auto: 13,
31830
+ tiny: 10,
31831
+ small: 11,
31832
+ normal: 13,
31833
+ large: 16,
31834
+ huge: 20
31835
+ };
31836
+ function fontPxOf(size) {
31837
+ if (typeof size === "number") return size > 0 ? size : SIZE_PX4.auto;
31838
+ return SIZE_PX4[size] ?? SIZE_PX4.auto;
31839
+ }
31840
+ function tableHasContent(t) {
31841
+ return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
31842
+ }
31843
+ function mergeRenderPlan(t) {
31844
+ const span = /* @__PURE__ */ new Map();
31845
+ const omit = /* @__PURE__ */ new Set();
31846
+ for (const m of t.merges) {
31847
+ span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
31848
+ for (let r = m.startRow; r <= m.endRow; r += 1) {
31849
+ for (let c = m.startCol; c <= m.endCol; c += 1) {
31850
+ if (r !== m.startRow || c !== m.startCol) omit.add(`${r}:${c}`);
31851
+ }
31852
+ }
31853
+ }
31854
+ for (let r = 0; r < t.rows; r += 1) {
31855
+ for (let c = 0; c < t.columns; c += 1) {
31856
+ if (t.cells[r]?.[c]?.merged && !span.has(`${r}:${c}`)) omit.add(`${r}:${c}`);
31857
+ }
31858
+ }
31859
+ for (const key of span.keys()) omit.delete(key);
31860
+ return { span, omit };
31861
+ }
31862
+
31863
+ // src/renderers/shared/TableCanvasRenderer.ts
31864
+ var PAD_X = 6;
31865
+ var PAD_Y = 2;
31866
+ var MARGIN = 6;
31867
+ var LINE_HEIGHT = 1.2;
31868
+ function paintTable(ctx, t, args, tips) {
31869
+ if (!tableHasContent(t)) return;
31870
+ const layout = layoutTable(ctx, t, args);
31871
+ if (!layout || layout.w <= 0 || layout.h <= 0) return;
31872
+ const fw = t.frameColor && t.frameWidth > 0 ? t.frameWidth : 0;
31873
+ const { x, y } = anchorOrigin(t.position, layout.w + 2 * fw, layout.h + 2 * fw, args);
31874
+ const x0 = x + fw;
31875
+ const y0 = y + fw;
31876
+ if (t.bgColor) {
31877
+ ctx.fillStyle = t.bgColor;
31878
+ ctx.fillRect(x0, y0, layout.w, layout.h);
31879
+ }
31880
+ if (fw > 0 && t.frameColor) {
31881
+ ctx.strokeStyle = t.frameColor;
31882
+ ctx.lineWidth = fw;
31883
+ ctx.strokeRect(x + fw / 2, y + fw / 2, layout.w + fw, layout.h + fw);
31884
+ }
31885
+ const colX = [0];
31886
+ for (const w of layout.colW) colX.push(colX[colX.length - 1] + w);
31887
+ const rowY = [0];
31888
+ for (const h of layout.rowH) rowY.push(rowY[rowY.length - 1] + h);
31889
+ const prevBaseline = ctx.textBaseline;
31890
+ const prevAlign = ctx.textAlign;
31891
+ ctx.textBaseline = "middle";
31892
+ for (const box of layout.boxes) {
31893
+ const rx = x0 + colX[box.c];
31894
+ const ry = y0 + rowY[box.r];
31895
+ const rw = colX[box.c + box.cs] - colX[box.c];
31896
+ const rh = rowY[box.r + box.rs] - rowY[box.r];
31897
+ const cell = box.cell;
31898
+ if (cell.bgColor) {
31899
+ ctx.fillStyle = cell.bgColor;
31900
+ ctx.fillRect(rx, ry, rw, rh);
31901
+ }
31902
+ const text = cell.text ?? "";
31903
+ if (text.length > 0) {
31904
+ const px = fontPxOf(cell.textSize);
31905
+ ctx.font = cellFont(cell, px, args.theme);
31906
+ ctx.fillStyle = cell.textColor ?? args.theme.textColor;
31907
+ const lines = text.split("\n");
31908
+ const blockH = lines.length * px * LINE_HEIGHT;
31909
+ const blockTop = cell.vAlign === "top" ? ry + PAD_Y : cell.vAlign === "bottom" ? ry + rh - PAD_Y - blockH : ry + (rh - blockH) / 2;
31910
+ const tx = cell.hAlign === "left" ? rx + PAD_X : cell.hAlign === "right" ? rx + rw - PAD_X : rx + rw / 2;
31911
+ ctx.textAlign = cell.hAlign;
31912
+ lines.forEach((line, i) => ctx.fillText(line, tx, blockTop + (i + 0.5) * px * LINE_HEIGHT));
31913
+ }
31914
+ if (cell.tooltip) tips.push({ left: rx, top: ry, right: rx + rw, bottom: ry + rh, text: cell.tooltip });
31915
+ }
31916
+ ctx.textBaseline = prevBaseline;
31917
+ ctx.textAlign = prevAlign;
31918
+ if (t.borderColor && t.borderWidth > 0) {
31919
+ ctx.strokeStyle = t.borderColor;
31920
+ ctx.lineWidth = t.borderWidth;
31921
+ const seen = /* @__PURE__ */ new Set();
31922
+ ctx.beginPath();
31923
+ const edge = (ax, ay, bx, by) => {
31924
+ const key = `${ax},${ay},${bx},${by}`;
31925
+ if (seen.has(key)) return;
31926
+ seen.add(key);
31927
+ ctx.moveTo(ax, ay);
31928
+ ctx.lineTo(bx, by);
31929
+ };
31930
+ for (const box of layout.boxes) {
31931
+ const l = Math.round(x0 + colX[box.c]);
31932
+ const r = Math.round(x0 + colX[box.c + box.cs]);
31933
+ const tp = Math.round(y0 + rowY[box.r]);
31934
+ const bt = Math.round(y0 + rowY[box.r + box.rs]);
31935
+ edge(l, tp, r, tp);
31936
+ edge(l, bt, r, bt);
31937
+ edge(l, tp, l, bt);
31938
+ edge(r, tp, r, bt);
31939
+ }
31940
+ ctx.stroke();
31941
+ }
31942
+ }
31943
+ function layoutTable(ctx, t, args) {
31944
+ const { span, omit } = mergeRenderPlan(t);
31945
+ const colW = new Array(t.columns).fill(0);
31946
+ const rowH = new Array(t.rows).fill(0);
31947
+ const boxes = [];
31948
+ for (let r = 0; r < t.rows; r += 1) {
31949
+ for (let c = 0; c < t.columns; c += 1) {
31950
+ if (omit.has(`${r}:${c}`)) continue;
31951
+ const cell = t.cells[r]?.[c];
31952
+ if (cell == null) continue;
31953
+ const sp = span.get(`${r}:${c}`);
31954
+ boxes.push({ cell, r, c, cs: Math.min(sp?.cs ?? 1, t.columns - c), rs: Math.min(sp?.rs ?? 1, t.rows - r) });
31955
+ }
31956
+ }
31957
+ if (boxes.length === 0) return null;
31958
+ const sizeOf = (cell) => {
31959
+ const px = fontPxOf(cell.textSize);
31960
+ ctx.font = cellFont(cell, px, args.theme);
31961
+ const lines = (cell.text ?? "").split("\n");
31962
+ let maxW = 0;
31963
+ for (const line of lines) maxW = Math.max(maxW, ctx.measureText(line).width);
31964
+ let w2 = Math.ceil(maxW) + 2 * PAD_X;
31965
+ let h2 = Math.ceil(lines.length * px * LINE_HEIGHT) + 2 * PAD_Y;
31966
+ if (cell.width) w2 = Math.max(w2, cell.width / 100 * args.plotWidth);
31967
+ if (cell.height) h2 = Math.max(h2, cell.height / 100 * args.paneHeight);
31968
+ return { w: w2, h: h2 };
31969
+ };
31970
+ const spanning = [];
31971
+ for (const box of boxes) {
31972
+ const { w: w2, h: h2 } = sizeOf(box.cell);
31973
+ if (box.cs === 1) colW[box.c] = Math.max(colW[box.c], w2);
31974
+ if (box.rs === 1) rowH[box.r] = Math.max(rowH[box.r], h2);
31975
+ if (box.cs > 1 || box.rs > 1) spanning.push({ box, w: w2, h: h2 });
31976
+ }
31977
+ for (const { box, w: w2, h: h2 } of spanning) {
31978
+ if (box.cs > 1) {
31979
+ let sum = 0;
31980
+ for (let c = box.c; c < box.c + box.cs; c += 1) sum += colW[c];
31981
+ if (w2 > sum) for (let c = box.c; c < box.c + box.cs; c += 1) colW[c] += (w2 - sum) / box.cs;
31982
+ }
31983
+ if (box.rs > 1) {
31984
+ let sum = 0;
31985
+ for (let r = box.r; r < box.r + box.rs; r += 1) sum += rowH[r];
31986
+ if (h2 > sum) for (let r = box.r; r < box.r + box.rs; r += 1) rowH[r] += (h2 - sum) / box.rs;
31987
+ }
31988
+ }
31989
+ let w = 0;
31990
+ for (const cw of colW) w += cw;
31991
+ let h = 0;
31992
+ for (const rh of rowH) h += rh;
31993
+ return { colW, rowH, w, h, boxes };
31994
+ }
31995
+ function anchorOrigin(position, totalW, totalH, args) {
31996
+ let y;
31997
+ if (position.startsWith("top")) y = MARGIN;
31998
+ else if (position.startsWith("bottom")) y = args.paneHeight - MARGIN - totalH;
31999
+ else y = args.paneHeight / 2 - totalH / 2;
32000
+ let x;
32001
+ if (position.endsWith("left")) x = MARGIN;
32002
+ else if (position.endsWith("right")) x = args.plotWidth - MARGIN - totalW;
32003
+ else x = args.plotWidth / 2 - totalW / 2;
32004
+ return { x, y };
32005
+ }
32006
+ function cellFont(cell, px, theme) {
32007
+ const family = cell.fontFamily === "monospace" ? "monospace" : theme.fontFamily || "sans-serif";
32008
+ return `${cell.italic ? "italic " : ""}${cell.bold ? "bold " : ""}${px}px ${family}`;
32009
+ }
32010
+
32011
+ // src/renderers/native/drawings/IndicatorDrawingSlices.ts
32012
+ function indicatorSliceKey(z, boundaries) {
32013
+ return boundaries.find((b) => b > z) ?? Infinity;
32014
+ }
32015
+ var IndicatorDrawingSlices = class {
32016
+ constructor() {
32017
+ this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
32018
+ /** Slice canvas cache, keyed `paneId|beforeZ` — same lifecycle as the user-drawing cache. */
32019
+ this.sliceCache = /* @__PURE__ */ new Map();
32020
+ /** Tooltip hit-rects of every label drawn this frame, in plot coords (rebuilt per prepare). */
32021
+ this.tips = [];
32022
+ }
32023
+ /**
32024
+ * Rebuild the per-indicator drawing slices for this data frame. `ref` is the data
32025
+ * canvas the slices must match pixel-for-pixel (the backend composites them 1:1).
32026
+ * Runs from the renderer's data paint, just before the backend composites the scene.
32027
+ */
32028
+ prepare(scene, coords, theme, ref) {
32029
+ this.tips = [];
32030
+ const out = /* @__PURE__ */ new Map();
32031
+ if (ref.width === 0 || ref.height === 0) {
32032
+ this.sliceCache.clear();
32033
+ return out;
32034
+ }
32035
+ this.drawScene.setDeps({
32036
+ timeToLogical: (ms) => coords.timeToLogical(ms),
32037
+ barAt: (logical) => {
32038
+ const b = scene.bars[Math.round(logical)];
32039
+ return b ? { high: b.high, low: b.low } : null;
32040
+ },
32041
+ theme
32042
+ });
32043
+ const dpr = coords.dpr;
32044
+ const dataW = coords.width;
32045
+ const buckets = /* @__PURE__ */ new Map();
32046
+ const add = (paneId, beforeZ, entry) => {
32047
+ const key = `${paneId}|${beforeZ}`;
32048
+ const bucket = buckets.get(key);
32049
+ if (bucket) bucket.entries.push(entry);
32050
+ else buckets.set(key, { paneId, beforeZ, entries: [entry] });
32051
+ };
32052
+ for (const pane of scene.orderedPanes()) {
32053
+ if (pane.collapsed) continue;
32054
+ const boundaries = scene.seriesBoundaries(pane.id);
32055
+ for (const m of scene.orderedIndicatorsForPane(pane.id)) {
32056
+ const set = modelDrawingSet(m, false);
32057
+ const tables = (m.tables ?? []).filter((t) => !t.overlay);
32058
+ if (drawingSetEmpty(set) && tables.length === 0) continue;
32059
+ const sc = scene.scaleFor(m, pane);
32060
+ const mp = sc === pane.scale ? pane : { ...pane, scale: sc };
32061
+ const beforeZ = indicatorSliceKey(scene.zOf(m.id), boundaries);
32062
+ add(pane.id, beforeZ, { set, tables, pane: mp, indexOffset: scene.offsetOf(m.id) });
32063
+ }
32064
+ if (pane.kind === "price") {
32065
+ for (const m of scene.indicators.values()) {
32066
+ const set = modelDrawingSet(m, true);
32067
+ const tables = (m.tables ?? []).filter((t) => t.overlay === true);
32068
+ if (drawingSetEmpty(set) && tables.length === 0) continue;
32069
+ add(pane.id, Infinity, { set, tables, pane, indexOffset: scene.offsetOf(m.id) });
32070
+ }
32071
+ }
32072
+ }
32073
+ for (const [key, { paneId, beforeZ, entries }] of buckets) {
32074
+ let canvas = this.sliceCache.get(key);
32075
+ if (!canvas) {
32076
+ canvas = document.createElement("canvas");
32077
+ this.sliceCache.set(key, canvas);
32078
+ }
32079
+ if (canvas.width !== ref.width || canvas.height !== ref.height) {
32080
+ canvas.width = ref.width;
32081
+ canvas.height = ref.height;
32082
+ }
32083
+ const ctx = canvas.getContext("2d");
32084
+ if (!ctx) continue;
32085
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
32086
+ ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
32087
+ for (const e of entries) this.paintEntry(ctx, e, coords, dataW, theme);
32088
+ const slices = out.get(paneId) ?? [];
32089
+ slices.push({ beforeZ, canvas });
32090
+ out.set(paneId, slices);
32091
+ }
32092
+ for (const key of [...this.sliceCache.keys()]) if (!buckets.has(key)) this.sliceCache.delete(key);
32093
+ for (const slices of out.values()) slices.sort((a, b) => a.beforeZ - b.beforeZ);
32094
+ return out;
32095
+ }
32096
+ paintEntry(ctx, e, coords, dataW, theme) {
32097
+ const { pane } = e;
32098
+ const paneTips = [];
32099
+ ctx.save();
32100
+ ctx.translate(0, pane.bounds.top);
32101
+ ctx.beginPath();
32102
+ ctx.rect(0, 0, dataW, pane.bounds.height);
32103
+ ctx.clip();
32104
+ this.drawScene.setSet(e.set, e.indexOffset);
32105
+ this.drawScene.render(
32106
+ ctx,
32107
+ dataW,
32108
+ pane.bounds.height,
32109
+ (l) => coords.logicalToX(l),
32110
+ (price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
32111
+ );
32112
+ paneTips.push(...this.drawScene.labelTipRegions());
32113
+ for (const t of e.tables) paintTable(ctx, t, { paneHeight: pane.bounds.height, plotWidth: dataW, theme }, paneTips);
32114
+ ctx.restore();
32115
+ for (const r of paneTips) {
32116
+ this.tips.push({ ...r, top: r.top + pane.bounds.top, bottom: r.bottom + pane.bounds.top });
32117
+ }
32118
+ }
32119
+ /** Tooltip of the topmost label or table cell under a plot-space point, or null. Fed by the last prepare. */
32120
+ labelTooltipAt(x, y) {
32121
+ for (let i = this.tips.length - 1; i >= 0; i -= 1) {
32122
+ const r = this.tips[i];
32123
+ if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return r.text;
32124
+ }
32125
+ return null;
32126
+ }
32127
+ };
32128
+ function mergeSlices(indicator, user) {
32129
+ const out = /* @__PURE__ */ new Map();
32130
+ for (const [paneId, slices] of indicator) out.set(paneId, [...slices]);
32131
+ for (const [paneId, slices] of user) out.set(paneId, [...out.get(paneId) ?? [], ...slices]);
32132
+ for (const slices of out.values()) slices.sort((a, b) => a.beforeZ - b.beforeZ);
32133
+ return out;
32134
+ }
32135
+
31767
32136
  // src/renderers/native/drawings/Projector.ts
31768
32137
  function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
31769
32138
  return {
@@ -32509,6 +32878,11 @@ var NativeRenderer = class {
32509
32878
  this.vpvrRenderer = new VpvrRenderer();
32510
32879
  this.resizeObserver = null;
32511
32880
  this.dprMedia = null;
32881
+ /** Plot size in INTEGER device px, as last reported by the resize observer's
32882
+ * device-pixel-content-box — the browser's own statement of how many device pixels
32883
+ * it paints the plot into. `null` until the first report or where the box type is
32884
+ * unsupported (WebKit); syncSize then falls back to rounding the client rect. */
32885
+ this.plotDeviceSize = null;
32512
32886
  this.coords = new CoordinateSystem();
32513
32887
  this.scene = new SceneGraph();
32514
32888
  // chosen at mount (WebGL2 if available, else canvas2d)
@@ -32516,6 +32890,8 @@ var NativeRenderer = class {
32516
32890
  this.glowAmount = 0;
32517
32891
  // WebGL2 neon-glow intensity (canvas2d ignores it)
32518
32892
  this.chrome = new ChromeRenderer();
32893
+ /** Prepaints each indicator's Pine drawings into interleave slices at the model's z. */
32894
+ this.indicatorSlices = new IndicatorDrawingSlices();
32519
32895
  /** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
32520
32896
  this.labelTooltip = null;
32521
32897
  this.crosshairLayer = new CrosshairRenderer();
@@ -32664,7 +33040,6 @@ var NativeRenderer = class {
32664
33040
  this.toggleVisibleCbs = /* @__PURE__ */ new Set();
32665
33041
  this.moveIndicatorCbs = /* @__PURE__ */ new Set();
32666
33042
  this.priceStyleCbs = /* @__PURE__ */ new Set();
32667
- this.tableOverlays = /* @__PURE__ */ new Map();
32668
33043
  this.name = "native";
32669
33044
  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"];
32670
33045
  /** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
@@ -32978,7 +33353,6 @@ var NativeRenderer = class {
32978
33353
  * hidden) on clear so a re-show picks up the current theme.
32979
33354
  */
32980
33355
  setLoading(loading) {
32981
- for (const overlay of this.tableOverlays.values()) overlay.setVisible(!loading);
32982
33356
  if (!loading || !this.wrapper) {
32983
33357
  this.loadingEl?.remove();
32984
33358
  this.loadingEl = null;
@@ -33640,7 +34014,7 @@ var NativeRenderer = class {
33640
34014
  this.plot.addEventListener("pointerleave", this.onScrollProximityLeave);
33641
34015
  this.labelTooltip = new LabelTooltip(this.plot, {
33642
34016
  theme: () => this.chromeTheme(),
33643
- lookup: (x, y) => this.chrome.labelTooltipAt(x, y)
34017
+ lookup: (x, y) => this.indicatorSlices.labelTooltipAt(x, y)
33644
34018
  });
33645
34019
  this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
33646
34020
  projector: () => this.drawingProjector(),
@@ -33725,6 +34099,7 @@ var NativeRenderer = class {
33725
34099
  this.emitPaneAction({ type: "maximize", paneId, maximized });
33726
34100
  }
33727
34101
  });
34102
+ this.paneControls.setSuspended(this.layoutMode === "mobile");
33728
34103
  this.axisScaleButtons = new AxisScaleButtons(this.plot, theme, {
33729
34104
  panes: () => this.axisScaleViews(),
33730
34105
  rightAxis: () => this.rightAxisW,
@@ -33734,8 +34109,19 @@ var NativeRenderer = class {
33734
34109
  if (pane) this.setPaneLog(paneId, !paneLogScale(this.scene, pane));
33735
34110
  }
33736
34111
  });
33737
- this.resizeObserver = new ResizeObserver(() => this.resize());
34112
+ this.resizeObserver = new ResizeObserver((entries) => {
34113
+ for (const e of entries) {
34114
+ if (e.target !== this.plot) continue;
34115
+ const s = e.devicePixelContentBoxSize?.[0];
34116
+ if (s) this.plotDeviceSize = { width: s.inlineSize, height: s.blockSize };
34117
+ }
34118
+ this.resize();
34119
+ });
33738
34120
  this.resizeObserver.observe(this.wrapper);
34121
+ try {
34122
+ this.resizeObserver.observe(this.plot, { box: "device-pixel-content-box" });
34123
+ } catch {
34124
+ }
33739
34125
  this.watchDpr();
33740
34126
  this.syncSize();
33741
34127
  }
@@ -33860,8 +34246,6 @@ var NativeRenderer = class {
33860
34246
  this.inputsUI?.destroy();
33861
34247
  this.paneControls?.destroy();
33862
34248
  this.axisScaleButtons?.destroy();
33863
- for (const overlay of this.tableOverlays.values()) overlay.destroy();
33864
- this.tableOverlays.clear();
33865
34249
  this.resizeObserver?.disconnect();
33866
34250
  this.resizeObserver = null;
33867
34251
  this.dprMedia?.removeEventListener("change", this.onDprChange);
@@ -33890,6 +34274,7 @@ var NativeRenderer = class {
33890
34274
  this.attributionEl = null;
33891
34275
  this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
33892
34276
  this.mountContainer?.style.removeProperty("--vela-scale-gutter");
34277
+ this.mountContainer?.style.removeProperty("--vela-bottom-gutter");
33893
34278
  this.mountContainer?.style.removeProperty("--vela-price-pane-top");
33894
34279
  this.mountContainer?.style.removeProperty("--vela-price-pane-bottom");
33895
34280
  this.mountContainer = null;
@@ -33982,7 +34367,6 @@ var NativeRenderer = class {
33982
34367
  ensurePane(pane) {
33983
34368
  this.scene.ensurePane(pane.id, pane.kind, pane.order, pane.heightWeight ?? (pane.kind === "price" ? 3 : 1));
33984
34369
  this.layoutPanes();
33985
- this.repositionTables();
33986
34370
  this.paneControls?.refresh();
33987
34371
  this.scheduler.invalidate(4 /* Full */);
33988
34372
  }
@@ -34004,17 +34388,14 @@ var NativeRenderer = class {
34004
34388
  if (!model.ownScale) this.scene.dropIndicatorScale(handle.id);
34005
34389
  this.inputsUI.setPane(handle.id, paneId);
34006
34390
  this.refreshAnchorOffset(model);
34007
- this.syncTables(model);
34008
34391
  this.refreshAxisWidth();
34009
34392
  this.layoutPanes();
34010
- this.repositionTables();
34011
34393
  this.paneControls?.refresh();
34012
34394
  this.scheduler.invalidate(4 /* Full */);
34013
34395
  }
34014
34396
  orderPanes(orderedIds) {
34015
34397
  this.scene.orderPanes(orderedIds);
34016
34398
  this.layoutPanes();
34017
- this.repositionTables();
34018
34399
  this.paneControls?.refresh();
34019
34400
  this.scheduler.invalidate(4 /* Full */);
34020
34401
  }
@@ -34023,7 +34404,6 @@ var NativeRenderer = class {
34023
34404
  if (!pane || pane.collapsed === collapsed) return;
34024
34405
  pane.collapsed = collapsed;
34025
34406
  this.layoutPanes();
34026
- this.repositionTables();
34027
34407
  this.paneControls?.refresh();
34028
34408
  this.scheduler.invalidate(4 /* Full */);
34029
34409
  }
@@ -34031,7 +34411,6 @@ var NativeRenderer = class {
34031
34411
  if (paneId !== null && !this.scene.panes.has(paneId)) paneId = null;
34032
34412
  this.maximizedPaneId = paneId;
34033
34413
  this.layoutPanes();
34034
- this.repositionTables();
34035
34414
  this.paneControls?.refresh();
34036
34415
  this.scheduler.invalidate(4 /* Full */);
34037
34416
  }
@@ -34118,7 +34497,6 @@ var NativeRenderer = class {
34118
34497
  native: !!model.native,
34119
34498
  ...model.props ? { props: model.props, propValues: model.propValues ?? {} } : {}
34120
34499
  });
34121
- this.syncTables(model);
34122
34500
  if (model.native?.type === "volume") {
34123
34501
  this.volumeActive = true;
34124
34502
  this.volumeHidden = false;
@@ -34142,7 +34520,6 @@ var NativeRenderer = class {
34142
34520
  }
34143
34521
  }
34144
34522
  applyPatch(model, patch);
34145
- this.syncTables(model);
34146
34523
  this.scheduler.invalidate(3 /* Light */);
34147
34524
  }
34148
34525
  removeIndicator(handle) {
@@ -34161,8 +34538,6 @@ var NativeRenderer = class {
34161
34538
  this.scene.forgetAnchorOffset(handle.id);
34162
34539
  this.scene.dropIndicatorScale(handle.id);
34163
34540
  this.inputsUI.remove(handle.id);
34164
- this.tableOverlays.get(handle.id)?.destroy();
34165
- this.tableOverlays.delete(handle.id);
34166
34541
  this.refreshAxisWidth();
34167
34542
  this.paneControls?.refresh();
34168
34543
  this.scheduler.invalidate(4 /* Full */);
@@ -34206,8 +34581,6 @@ var NativeRenderer = class {
34206
34581
  }
34207
34582
  if (!visible) {
34208
34583
  this.scene.indicators.delete(handle.id);
34209
- this.tableOverlays.get(handle.id)?.destroy();
34210
- this.tableOverlays.delete(handle.id);
34211
34584
  }
34212
34585
  this.inputsUI.setVisible(handle.id, visible);
34213
34586
  this.scheduler.invalidate(4 /* Full */);
@@ -34253,6 +34626,7 @@ var NativeRenderer = class {
34253
34626
  this.userDrawings?.setLayoutMode(mode);
34254
34627
  this.settingsDialog?.setLayoutMode(mode);
34255
34628
  this.inputsUI?.setLayoutMode(mode);
34629
+ this.paneControls?.setSuspended(mode === "mobile");
34256
34630
  if (this.scrollButton) {
34257
34631
  const px = mode === "mobile" ? SCROLL_BTN_SIZE_TOUCH : SCROLL_BTN_SIZE;
34258
34632
  this.scrollButton.style.width = `${px}px`;
@@ -34648,7 +35022,6 @@ var NativeRenderer = class {
34648
35022
  /** Relayout + repaint + refresh the hover buttons after a collapse/maximize/order change. */
34649
35023
  afterPaneLayoutChange() {
34650
35024
  this.layoutPanes();
34651
- this.repositionTables();
34652
35025
  this.paneControls?.refresh();
34653
35026
  this.scheduler.invalidate(4 /* Full */);
34654
35027
  }
@@ -34729,7 +35102,6 @@ var NativeRenderer = class {
34729
35102
  above.heightWeight = next.above;
34730
35103
  below.heightWeight = next.below;
34731
35104
  this.layoutPanes();
34732
- this.repositionTables();
34733
35105
  this.scheduler.invalidate(4 /* Full */);
34734
35106
  }
34735
35107
  /** Double-click a separator → split the two adjacent panes evenly (each gets half of
@@ -34744,7 +35116,6 @@ var NativeRenderer = class {
34744
35116
  above.heightWeight = half;
34745
35117
  below.heightWeight = half;
34746
35118
  this.layoutPanes();
34747
- this.repositionTables();
34748
35119
  this.scheduler.invalidate(4 /* Full */);
34749
35120
  }
34750
35121
  // ── keyboard navigation / accessibility (item 11) ──
@@ -35023,7 +35394,10 @@ var NativeRenderer = class {
35023
35394
  const liveActual = li >= 0 ? this.bars[li] : void 0;
35024
35395
  const easeLive = !!liveActual && this.liveEaseTime === liveActual.time && (liveActual.high !== this.liveEaseHigh || liveActual.low !== this.liveEaseLow || liveActual.close !== this.liveEaseClose);
35025
35396
  if (easeLive && liveActual) this.bars[li] = { ...liveActual, high: this.liveEaseHigh, low: this.liveEaseLow, close: this.liveEaseClose };
35026
- this.scene.drawingSlices = this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map();
35397
+ this.scene.drawingSlices = mergeSlices(
35398
+ this.indicatorSlices.prepare(this.scene, this.coords, this.theme, this.dataCanvas),
35399
+ this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map()
35400
+ );
35027
35401
  this.backdropRenderer.render(this.scene, this.coords, this.theme, gridAlpha);
35028
35402
  this.backend.render(this.scene, this.coords, this.theme);
35029
35403
  this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
@@ -35461,27 +35835,6 @@ var NativeRenderer = class {
35461
35835
  }
35462
35836
  return maxVol;
35463
35837
  }
35464
- /** Create/update/destroy an indicator's DOM table overlay (anchored off real pane geometry). */
35465
- syncTables(model) {
35466
- const tables = model.tables ?? [];
35467
- let overlay = this.tableOverlays.get(model.id);
35468
- if (tables.length === 0) {
35469
- if (overlay) {
35470
- overlay.destroy();
35471
- this.tableOverlays.delete(model.id);
35472
- }
35473
- return;
35474
- }
35475
- if (!overlay) {
35476
- overlay = new TableOverlay(this.plot, this.theme, (id) => this.paneBoundsFor(id));
35477
- overlay.setVisible(this.loadingEl === null);
35478
- this.tableOverlays.set(model.id, overlay);
35479
- }
35480
- overlay.update(tables);
35481
- }
35482
- repositionTables() {
35483
- for (const overlay of this.tableOverlays.values()) overlay.reposition();
35484
- }
35485
35838
  layoutPanes() {
35486
35839
  const panes = this.scene.orderedPanes();
35487
35840
  const dataHeight = this.coords.height;
@@ -35532,6 +35885,7 @@ var NativeRenderer = class {
35532
35885
  const visible = maxPane ? [maxPane] : this.scene.orderedPanes().filter((p) => !p.collapsed);
35533
35886
  const paneBottom = visible.length ? Math.max(...visible.map((p) => p.bounds.top + p.bounds.height)) : dataHeight;
35534
35887
  this.scrollBtnBottomPx = SCROLL_BTN_BOTTOM + Math.max(0, dataHeight - paneBottom);
35888
+ this.mountContainer?.style.setProperty("--vela-bottom-gutter", `${TIME_AXIS_H + Math.max(0, dataHeight - paneBottom)}px`);
35535
35889
  this.scrollBtnRightPx = this.rightAxisW + SCROLL_BTN_RIGHT_INSET;
35536
35890
  if (this.scrollButton) {
35537
35891
  this.scrollButton.style.bottom = `${this.scrollBtnBottomPx}px`;
@@ -35624,30 +35978,33 @@ var NativeRenderer = class {
35624
35978
  if (w <= 0 || h <= 0) return;
35625
35979
  const dpr = window.devicePixelRatio || 1;
35626
35980
  this.plot.style.left = `${this.toolbarGutter}px`;
35627
- const pw = Math.max(1, w - this.toolbarGutter);
35628
- const ph = h;
35629
- this.dataCanvas.width = Math.round(pw * dpr);
35630
- this.dataCanvas.height = Math.round(ph * dpr);
35631
- this.backdropCanvas.width = this.dataCanvas.width;
35632
- this.backdropCanvas.height = this.dataCanvas.height;
35633
- this.volumeCanvas.width = this.dataCanvas.width;
35634
- this.volumeCanvas.height = this.dataCanvas.height;
35635
- for (const l of this.extLayers) {
35636
- l.canvas.width = this.dataCanvas.width;
35637
- l.canvas.height = this.dataCanvas.height;
35638
- }
35639
- this.vpvrCanvas.width = this.dataCanvas.width;
35640
- this.vpvrCanvas.height = this.dataCanvas.height;
35641
- this.chromeCanvas.width = this.dataCanvas.width;
35642
- this.chromeCanvas.height = this.dataCanvas.height;
35643
- this.drawingsCanvas.width = this.dataCanvas.width;
35644
- this.drawingsCanvas.height = this.dataCanvas.height;
35645
- this.cursorCanvas.width = this.dataCanvas.width;
35646
- this.cursorCanvas.height = this.dataCanvas.height;
35981
+ const rect = this.plot.getBoundingClientRect();
35982
+ let bw = Math.max(1, Math.round(rect.width * dpr));
35983
+ let bh = Math.max(1, Math.round(rect.height * dpr));
35984
+ const dev = this.plotDeviceSize;
35985
+ if (dev && Math.abs(dev.width - rect.width * dpr) <= 1 && Math.abs(dev.height - rect.height * dpr) <= 1) {
35986
+ bw = Math.max(1, dev.width);
35987
+ bh = Math.max(1, dev.height);
35988
+ }
35989
+ const pw = bw / dpr;
35990
+ const ph = bh / dpr;
35991
+ const size = (canvas) => {
35992
+ canvas.width = bw;
35993
+ canvas.height = bh;
35994
+ canvas.style.width = `${pw}px`;
35995
+ canvas.style.height = `${ph}px`;
35996
+ };
35997
+ size(this.dataCanvas);
35998
+ size(this.backdropCanvas);
35999
+ size(this.volumeCanvas);
36000
+ for (const l of this.extLayers) size(l.canvas);
36001
+ size(this.vpvrCanvas);
36002
+ size(this.chromeCanvas);
36003
+ size(this.drawingsCanvas);
36004
+ size(this.cursorCanvas);
35647
36005
  this.coords.setSize(Math.max(1, pw - this.rightAxisW), Math.max(1, ph - TIME_AXIS_H), dpr);
35648
36006
  this.scene.crosshair = null;
35649
36007
  this.layoutPanes();
35650
- this.repositionTables();
35651
36008
  this.userDrawings?.onResize();
35652
36009
  if (!this.didInitialFit && this.coords.barCount > 0) {
35653
36010
  this.fitContent();
@@ -36966,9 +37323,168 @@ var Watermark = class {
36966
37323
  }
36967
37324
  };
36968
37325
 
37326
+ // src/widget/cell-controls.ts
37327
+ var CELL_CONTROLS_PROXIMITY_PX = 120;
37328
+ var TIME_AXIS_H2 = 22;
37329
+ var CONTROLS_BOTTOM_PX = TIME_AXIS_H2 + 12;
37330
+ var CLUSTER_H2 = 24;
37331
+ var CLUSTER_PILL2 = "rgba(0,0,0,0.65)";
37332
+ var STYLE_ID26 = "vela-cell-controls";
37333
+ var CSS23 = `
37334
+ .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;}
37335
+ .vela-cc-btn svg{display:block;}
37336
+ .vela-cc-btn:hover{background:var(--vela-active);color:var(--vela-fg-bright);}
37337
+ .vela-cc-on,.vela-cc-on:hover{background:var(--vela-selected-bg);color:var(--vela-selected-fg);}
37338
+ .vela-cc-grip{cursor:grab;touch-action:none;}
37339
+ .vela-cc-grip:active{cursor:grabbing;}
37340
+ `;
37341
+ function nearBottomCenter(x, y, width, height, proximityPx = CELL_CONTROLS_PROXIMITY_PX) {
37342
+ const cx = width / 2;
37343
+ const cy = height - CONTROLS_BOTTOM_PX - CLUSTER_H2 / 2;
37344
+ return Math.hypot(x - cx, y - cy) <= proximityPx;
37345
+ }
37346
+ var CellControls = class {
37347
+ constructor(host, deps) {
37348
+ this.host = host;
37349
+ this.deps = deps;
37350
+ this.near = false;
37351
+ /** A grip drag is underway — the proximity reveal must not hide the cluster
37352
+ * while captured pointer moves sweep across the whole grid. */
37353
+ this.dragging = false;
37354
+ /** Mobile: the proximity reveal is meaningless without a cursor — the mobile
37355
+ * bar's maximize stop replaces the cluster. */
37356
+ this.suspended = false;
37357
+ this.onHostMove = (e) => {
37358
+ if (this.suspended) return;
37359
+ if (this.dragging) return;
37360
+ const rect = this.host.getBoundingClientRect();
37361
+ this.setNear(nearBottomCenter(e.clientX - rect.left, e.clientY - rect.top, rect.width, rect.height));
37362
+ };
37363
+ this.onHostLeave = () => {
37364
+ if (this.dragging) return;
37365
+ this.setNear(false);
37366
+ };
37367
+ injectStyles(STYLE_ID26, CSS23, host.ownerDocument);
37368
+ this.glider = new Glider(deps.chart);
37369
+ this.root = host.ownerDocument.createElement("div");
37370
+ Object.assign(this.root.style, {
37371
+ position: "absolute",
37372
+ left: "50%",
37373
+ bottom: `${CONTROLS_BOTTOM_PX}px`,
37374
+ transform: "translateX(-50%)",
37375
+ zIndex: "6",
37376
+ display: "none",
37377
+ // revealed by cursor proximity (onHostMove)
37378
+ gap: "2px",
37379
+ padding: "2px",
37380
+ borderRadius: "var(--vela-radius-md)",
37381
+ background: CLUSTER_PILL2,
37382
+ pointerEvents: "auto"
37383
+ });
37384
+ this.host.addEventListener("pointermove", this.onHostMove);
37385
+ this.host.addEventListener("pointerleave", this.onHostLeave);
37386
+ this.host.appendChild(this.root);
37387
+ this.refresh();
37388
+ }
37389
+ /** Rebuild the buttons (the multi-cell gate or the maximized state changed). */
37390
+ refresh() {
37391
+ this.root.textContent = "";
37392
+ const multi = this.deps.multiCell();
37393
+ const maximized = multi && this.deps.isMaximized();
37394
+ if (multi && !maximized) this.root.appendChild(this.makeGrip());
37395
+ this.root.appendChild(this.button("minus", "Zoom out", () => this.glider.zoom(ZOOM_OUT)));
37396
+ this.root.appendChild(this.button("plus", "Zoom in", () => this.glider.zoom(ZOOM_IN)));
37397
+ if (multi) {
37398
+ this.root.appendChild(
37399
+ this.button(maximized ? "restore" : "maximize", maximized ? "Restore layout" : "Maximize chart", () => this.deps.toggleMaximize(), {
37400
+ // The maximized state reads as an inverse chip (white-on-dark, dark-on-light),
37401
+ // the same active-state affordance as a collapsed pane's expand button.
37402
+ selected: maximized
37403
+ })
37404
+ );
37405
+ }
37406
+ this.root.appendChild(
37407
+ this.button("reset", "Reset chart", () => {
37408
+ this.glider.stop();
37409
+ this.deps.reset();
37410
+ })
37411
+ );
37412
+ }
37413
+ button(iconId, title, onClick, opts = {}) {
37414
+ const b = this.host.ownerDocument.createElement("button");
37415
+ b.type = "button";
37416
+ b.title = title;
37417
+ b.setAttribute("aria-label", title);
37418
+ b.className = opts.selected === true ? "vela-cc-btn vela-cc-on" : "vela-cc-btn";
37419
+ b.innerHTML = icon(iconId);
37420
+ b.addEventListener("click", (e) => {
37421
+ e.stopPropagation();
37422
+ onClick();
37423
+ });
37424
+ return b;
37425
+ }
37426
+ /** The drag handle (2×3 dot grip): press and drag onto another cell to trade
37427
+ * slots with it. The preview highlight follows the pointer; releasing outside
37428
+ * any other cell cancels. */
37429
+ makeGrip() {
37430
+ const b = this.host.ownerDocument.createElement("button");
37431
+ b.type = "button";
37432
+ b.title = "Drag to move chart";
37433
+ b.setAttribute("aria-label", "Drag to move chart");
37434
+ b.className = "vela-cc-btn vela-cc-grip";
37435
+ b.innerHTML = icon("grip");
37436
+ b.addEventListener("pointerdown", (e) => this.onGripDown(b, e));
37437
+ return b;
37438
+ }
37439
+ onGripDown(btn2, e) {
37440
+ if (e.button !== 0 && e.pointerType === "mouse") return;
37441
+ e.preventDefault();
37442
+ e.stopPropagation();
37443
+ try {
37444
+ btn2.setPointerCapture(e.pointerId);
37445
+ } catch {
37446
+ }
37447
+ this.dragging = true;
37448
+ let target = null;
37449
+ const move = (ev) => {
37450
+ target = this.deps.dragTargetAt(ev.clientX, ev.clientY);
37451
+ this.deps.previewDrop(target);
37452
+ };
37453
+ const finish = (commit) => () => {
37454
+ this.dragging = false;
37455
+ this.deps.previewDrop(null);
37456
+ btn2.removeEventListener("pointermove", move);
37457
+ btn2.removeEventListener("pointerup", onUp);
37458
+ btn2.removeEventListener("pointercancel", onCancel);
37459
+ if (commit && target != null) this.deps.dropOn(target);
37460
+ };
37461
+ const onUp = finish(true);
37462
+ const onCancel = finish(false);
37463
+ btn2.addEventListener("pointermove", move);
37464
+ btn2.addEventListener("pointerup", onUp);
37465
+ btn2.addEventListener("pointercancel", onCancel);
37466
+ }
37467
+ /** Mobile flips the cluster off entirely (and hides it if currently revealed). */
37468
+ setSuspended(on) {
37469
+ this.suspended = on;
37470
+ if (on) this.setNear(false);
37471
+ }
37472
+ setNear(near) {
37473
+ if (near === this.near) return;
37474
+ this.near = near;
37475
+ this.root.style.display = near ? "flex" : "none";
37476
+ }
37477
+ destroy() {
37478
+ this.glider.stop();
37479
+ this.host.removeEventListener("pointermove", this.onHostMove);
37480
+ this.host.removeEventListener("pointerleave", this.onHostLeave);
37481
+ this.root.remove();
37482
+ }
37483
+ };
37484
+
36969
37485
  // src/widget/context-menu.ts
36970
37486
  var PRICE_AXIS_W = 60;
36971
- var TIME_AXIS_H2 = 26;
37487
+ var TIME_AXIS_H3 = 26;
36972
37488
  var ChartContextMenu = class {
36973
37489
  constructor(host, cbs) {
36974
37490
  this.cbs = cbs;
@@ -37007,7 +37523,7 @@ var ChartContextMenu = class {
37007
37523
  zoneOf(e) {
37008
37524
  const rect = this.host.getBoundingClientRect();
37009
37525
  if (e.clientX - rect.left > rect.width - PRICE_AXIS_W) return "price-axis";
37010
- if (e.clientY - rect.top > rect.height - TIME_AXIS_H2) return "time-axis";
37526
+ if (e.clientY - rect.top > rect.height - TIME_AXIS_H3) return "time-axis";
37011
37527
  return "body";
37012
37528
  }
37013
37529
  /** The pane under the pointer, so every pane's price scale has its own menu. */
@@ -37341,11 +37857,18 @@ var ChartCell = class {
37341
37857
  if (this.inner && this.state.symbol) this.marketStatus?.track(this.inner.data, this.state.symbol);
37342
37858
  });
37343
37859
  this.syncStatuslineColors();
37860
+ this.cellControls = new CellControls(this.host, {
37861
+ chart: () => this.inner,
37862
+ reset: () => this.resetView(),
37863
+ multiCell: () => deps.multiCell(),
37864
+ isMaximized: () => deps.isMaximized(id),
37865
+ toggleMaximize: () => deps.toggleMaximize(id),
37866
+ dragTargetAt: (x, y) => deps.cellDragTarget(id, x, y),
37867
+ previewDrop: (target) => deps.previewDropTarget(target),
37868
+ dropOn: (target) => deps.dropCell(id, target)
37869
+ });
37344
37870
  this.contextMenu = new ChartContextMenu(this.host, {
37345
- resetView: () => {
37346
- this.inner?.renderer.set("autoScale", true);
37347
- this.inner?.setVisibleRangePreset("ALL");
37348
- },
37871
+ resetView: () => this.resetView(),
37349
37872
  timezone: () => this.deps.timezone(),
37350
37873
  setTimezone: (zone) => this.deps.setTimezone(zone),
37351
37874
  // Right-clicking activates the cell first (capture-phase pointerdown), so the
@@ -37719,6 +38242,20 @@ var ChartCell = class {
37719
38242
  this.inner.setVisibleRangePreset(preset.preset);
37720
38243
  }
37721
38244
  }
38245
+ /** Reset this cell's view: re-enable auto scale and frame the full history —
38246
+ * the same action the chart context menu offers. */
38247
+ resetView() {
38248
+ this.inner?.renderer.set("autoScale", true);
38249
+ this.inner?.setVisibleRangePreset("ALL");
38250
+ }
38251
+ /** Rebuild the view-controls cluster (the maximize gate or state changed). */
38252
+ refreshControls() {
38253
+ this.cellControls.refresh();
38254
+ }
38255
+ /** Mobile flips the per-cell cluster off (the shell's mobile bar replaces it). */
38256
+ setControlsSuspended(on) {
38257
+ this.cellControls.setSuspended(on);
38258
+ }
37722
38259
  /** Make this cell the active one and put keyboard focus on its chart surface. */
37723
38260
  focus() {
37724
38261
  this.deps.activate(this.id);
@@ -38032,6 +38569,7 @@ var ChartCell = class {
38032
38569
  destroy() {
38033
38570
  this.destroyed = true;
38034
38571
  this.offMarket();
38572
+ this.cellControls.destroy();
38035
38573
  this.contextMenu.destroy();
38036
38574
  this.history.destroy();
38037
38575
  this.marketStatus?.stop();
@@ -38330,10 +38868,10 @@ var SplitterLayer = class {
38330
38868
  var DEFAULT_TIMEFRAMES = ["1", "5", "15", "60", "240", "D", "W"];
38331
38869
  var GAP_PX = 2;
38332
38870
  var POOL_CAP = 16;
38333
- var TIME_AXIS_H3 = 22;
38871
+ var TIME_AXIS_H4 = 22;
38334
38872
  var ALERT_CAP = 50;
38335
- var STYLE_ID26 = "vela-workspace";
38336
- var CSS23 = `
38873
+ var STYLE_ID27 = "vela-workspace";
38874
+ var CSS24 = `
38337
38875
  .vela-workspace { position: relative; width: 100%; height: 100%; display: flex; flex-direction: column; background: var(--vela-bg); }
38338
38876
  .vela-ws-main { position: relative; display: flex; flex-direction: row; flex: 1 1 auto; min-height: 0; }
38339
38877
  .vela-ws-toolbar { position: relative; flex: none; }
@@ -38360,6 +38898,21 @@ var CSS23 = `
38360
38898
  /* Mobile: the docked drawing-toolbar column would eat a phone-width grid \u2014 the shell's
38361
38899
  drawings drawer + on-chart pill replace it (same policy as the widget's in-chart bar). */
38362
38900
  [data-layout='mobile'] .vela-ws-toolbar { display: none; }
38901
+ /* A maximized cell owns the whole grid: the splitter strips have no seams to grab and
38902
+ the active ring would just outline the only visible chart \u2014 both are noise here. */
38903
+ .vela-ws-grid[data-maximized='1'] .vela-ws-splitter { display: none; }
38904
+ .vela-ws-grid[data-maximized='1'] .vela-cell[data-active='1']::after { display: none; }
38905
+ /* Drop-target preview while a cell's drag handle is held: a dashed ring + the same
38906
+ soft wash the splitter hover uses, over the chart, inert to the pointer. */
38907
+ .vela-cell[data-drop-target='1']::before {
38908
+ content: '';
38909
+ position: absolute;
38910
+ inset: 0;
38911
+ border: 2px dashed var(--vela-fg-bright);
38912
+ background: var(--vela-separator-hover-band);
38913
+ pointer-events: none;
38914
+ z-index: 11;
38915
+ }
38363
38916
  `;
38364
38917
  registerIcon("layout", svg16('<rect x="1.5" y="1.5" width="13" height="13" rx="1.5"/><path d="M8 1.5v13M1.5 8h13"/>'));
38365
38918
  function declaredOrder(cells) {
@@ -38389,6 +38942,9 @@ var VelaWorkspace = class {
38389
38942
  * slots beyond the list get auto identities. Grows, never reorders. */
38390
38943
  this.order = [];
38391
38944
  this.activeId = null;
38945
+ /** The cell maximized over the whole grid (null = normal grid). TRANSIENT view
38946
+ * state — never persisted; any structural change (layout, applyState) restores. */
38947
+ this.maximizedId = null;
38392
38948
  this.cellBackend = "auto";
38393
38949
  this.destroyed = false;
38394
38950
  this.shortcutsHelp = null;
@@ -38493,7 +39049,7 @@ var VelaWorkspace = class {
38493
39049
  this.order = boot?.charts ? boot.charts.map((c) => c.id) : declaredOrder(opts.cells);
38494
39050
  const bootActive = boot?.activeCellId ?? null;
38495
39051
  const doc = hostEl.ownerDocument;
38496
- injectStyles(STYLE_ID26, CSS23, doc);
39052
+ injectStyles(STYLE_ID27, CSS24, doc);
38497
39053
  this.root = doc.createElement("div");
38498
39054
  this.root.className = "vela-workspace";
38499
39055
  ensureUIHost(this.root, resolveTheme(opts.theme));
@@ -38603,7 +39159,11 @@ var VelaWorkspace = class {
38603
39159
  if (attribution !== false) {
38604
39160
  const background = resolveTheme(opts.theme).background;
38605
39161
  const mark = typeof attribution === "string" && attribution.trim() ? createCustomMark(doc, attribution, background) : createAttributionMark(doc, background);
38606
- Object.assign(mark.style, { left: "12px", bottom: `${TIME_AXIS_H3 + 10}px`, zIndex: "11" });
39162
+ Object.assign(mark.style, {
39163
+ left: "calc(var(--vela-toolbar-gutter, 0px) + 12px)",
39164
+ bottom: `calc(var(--vela-bottom-gutter, ${TIME_AXIS_H4}px) + 10px)`,
39165
+ zIndex: "11"
39166
+ });
38607
39167
  this.gridEl.appendChild(mark);
38608
39168
  this.attributionMark = mark;
38609
39169
  }
@@ -38669,6 +39229,9 @@ var VelaWorkspace = class {
38669
39229
  ...topbarHas(this.topbarComp, "indicators") && (picker || this.indicatorsOverride) ? { onIndicatorsClick: this.indicatorsOverride ? () => this.runOverride(this.indicatorsOverride) : () => picker.open() } : {},
38670
39230
  getContext: () => this.context(),
38671
39231
  ...this.drawingsEnabled ? { onDrawingsClick: () => this.openDrawingsDrawer() } : {},
39232
+ // Multi-chart only: the stop that isolates the ACTIVE chart (the
39233
+ // per-cell hover cluster has no cursor to reveal it on mobile).
39234
+ ...this.monoLayout ? {} : { onMaximizeClick: () => this.toggleMobileMaximize() },
38672
39235
  onMoreClick: () => this.openMoreDrawer(),
38673
39236
  onSettingsClick: () => this.active.chart.renderer.openSettings()
38674
39237
  }) : null;
@@ -38875,7 +39438,9 @@ var VelaWorkspace = class {
38875
39438
  this.pool.clear();
38876
39439
  for (const { id, ...cs } of st.charts.slice(liveCount)) this.pool.set(id, cs);
38877
39440
  this.order = st.charts.map((c) => c.id);
39441
+ this.clearMaximized();
38878
39442
  this.applyGrid();
39443
+ this.refreshCellControls();
38879
39444
  const nextActive2 = st.activeCellId && this.cellsById.has(st.activeCellId) ? st.activeCellId : this.order[0] ?? null;
38880
39445
  if (nextActive2 === this.activeId) this.projectActiveCell();
38881
39446
  else this.setActiveCell(nextActive2);
@@ -38901,6 +39466,7 @@ var VelaWorkspace = class {
38901
39466
  const def = this.monoLayout ? null : ensureLayout(st.layout);
38902
39467
  if (def) this.def = def;
38903
39468
  this.cellBackend = this.backendFor(this.def);
39469
+ this.clearMaximized();
38904
39470
  this.applyGrid();
38905
39471
  this.buildCells();
38906
39472
  this.syncCellPresentation();
@@ -38964,6 +39530,7 @@ var VelaWorkspace = class {
38964
39530
  setLayout(layout) {
38965
39531
  if (this.destroyed) return;
38966
39532
  if (this.monoLayout) return;
39533
+ this.clearMaximized();
38967
39534
  const next = this.resolveLayout(layout);
38968
39535
  const nextBackend = this.backendFor(next);
38969
39536
  const rebuildAll = nextBackend !== this.cellBackend;
@@ -38984,6 +39551,7 @@ var VelaWorkspace = class {
38984
39551
  this.buildCells();
38985
39552
  this.alignNewCellStyles(preexisting);
38986
39553
  this.syncCellPresentation();
39554
+ this.refreshCellControls();
38987
39555
  this.topbar.setLayout(next.id);
38988
39556
  const nextActive = activeAfterLayout(this.activeId, this.order.slice(0, next.cells.length));
38989
39557
  if (nextActive === this.activeId) this.projectActiveCell();
@@ -38992,6 +39560,67 @@ var VelaWorkspace = class {
38992
39560
  this.events.emit("layout:changed", { layout: next.id });
38993
39561
  this.markStateDirty();
38994
39562
  }
39563
+ /** The identity of the cell maximized over the whole grid, or null. */
39564
+ get maximizedCell() {
39565
+ return this.maximizedId;
39566
+ }
39567
+ /**
39568
+ * Maximize one cell over the whole grid, or restore the layout with `null`. Pure
39569
+ * presentation: the other cells stay alive underneath — charts, subscriptions and
39570
+ * state untouched — so restoring is instant. The maximized cell becomes the active
39571
+ * one. Transient view state (also reachable from each cell's bottom-center view
39572
+ * cluster): switching layouts or applying a state document restores the grid.
39573
+ */
39574
+ maximizeCell(id) {
39575
+ if (this.destroyed) return;
39576
+ if (id != null && (!this.cellsById.has(id) || this.def.cells.length <= 1)) return;
39577
+ if (id === this.maximizedId) return;
39578
+ this.maximizedId = id;
39579
+ if (id) this.setActiveCell(id);
39580
+ this.applyGrid();
39581
+ this.refreshCellControls();
39582
+ this.syncMobileMaximize();
39583
+ this.events.emit("cell:maximized", { id });
39584
+ }
39585
+ /** The mobile bar's maximize stop: one press isolates the ACTIVE chart over the
39586
+ * grid; while something is already isolated — the chart, or a pane inside it
39587
+ * (mobile's double-tap) — the press restores that instead. Every branch re-syncs
39588
+ * the stop on its own (`maximizeCell` directly, `panes.maximize` via its
39589
+ * synchronous `pane:changed`). */
39590
+ toggleMobileMaximize() {
39591
+ const cell = this.activeId ? this.cellsById.get(this.activeId) : void 0;
39592
+ if (!cell) return;
39593
+ if (this.maximizedId) this.maximizeCell(null);
39594
+ else if (cell.chart.panes.list().some((p) => p.maximized)) cell.chart.panes.maximize(null);
39595
+ else this.maximizeCell(cell.id);
39596
+ }
39597
+ /** Keep the mobile bar's maximize stop truthful: lit (inverse chip, restore
39598
+ * glyph) while the active chart covers the grid OR one of its panes is
39599
+ * maximized — the state a double-tap toggles is otherwise invisible on mobile. */
39600
+ syncMobileMaximize() {
39601
+ if (!this.mobileBar) return;
39602
+ const cell = this.activeId ? this.cellsById.get(this.activeId) : void 0;
39603
+ const paneMax = cell ? cell.chart.panes.list().some((p) => p.maximized) : false;
39604
+ this.mobileBar.setMaximizeActive(this.maximizedId != null || paneMax);
39605
+ }
39606
+ /**
39607
+ * Trade the SLOTS of two live cells — the grid arrangement changes, the cells
39608
+ * themselves (charts, indicators, drawings, the active flag) stay untouched.
39609
+ * What each cell's drag handle commits; also callable directly by hosts.
39610
+ */
39611
+ swapCells(a, b) {
39612
+ if (this.destroyed || a === b) return;
39613
+ const i = this.order.indexOf(a);
39614
+ const j = this.order.indexOf(b);
39615
+ if (i < 0 || j < 0 || !this.cellsById.has(a) || !this.cellsById.has(b)) return;
39616
+ [this.order[i], this.order[j]] = [this.order[j], this.order[i]];
39617
+ for (const [k] of this.def.cells.entries()) {
39618
+ const host = this.cellsById.get(this.order[k] ?? "")?.host;
39619
+ if (host) this.gridEl.appendChild(host);
39620
+ }
39621
+ this.applyGrid();
39622
+ this.markStateDirty();
39623
+ }
38995
39624
  resize() {
38996
39625
  this.splitters.layout();
38997
39626
  }
@@ -39061,6 +39690,7 @@ var VelaWorkspace = class {
39061
39690
  this.mobileBar?.renderActions();
39062
39691
  this.mobileBar?.setSymbol(cell.symbol);
39063
39692
  this.mobileBar?.setTimeframe(cell.timeframe);
39693
+ this.syncMobileMaximize();
39064
39694
  this.drawingPill?.onChart(cell.chart);
39065
39695
  const pushHistory = () => this.topbar.setHistoryState(cell.history.canUndo, cell.history.canRedo);
39066
39696
  this.historyUnsub?.();
@@ -39150,8 +39780,78 @@ var VelaWorkspace = class {
39150
39780
  const host = this.cellsById.get(this.order[i] ?? "")?.host;
39151
39781
  if (host) host.style.gridArea = perCell[slot.id]?.gridArea ?? "";
39152
39782
  }
39783
+ this.applyMaximizePresentation();
39784
+ this.mountAttributionMark();
39153
39785
  this.splitters.layout();
39154
39786
  }
39787
+ /** Overlay the maximize presentation on the freshly applied grid: EVERY cell spans
39788
+ * the full track grid — the maximized one on top, the siblings invisible beneath
39789
+ * it (their charts stay alive — restoring is instant). The siblings must span too:
39790
+ * left in their slots they would auto-flow into implicit zero-height rows, whose
39791
+ * gaps steal height from the maximized cell and collapse their renderers to 0.
39792
+ * The splitter strips and the active ring hide via the `data-maximized` rules. */
39793
+ applyMaximizePresentation() {
39794
+ const maxId = this.maximizedId;
39795
+ if (maxId) this.gridEl.dataset.maximized = "1";
39796
+ else delete this.gridEl.dataset.maximized;
39797
+ for (const [id, cell] of this.cellsById) {
39798
+ const style = cell.host.style;
39799
+ if (maxId) style.gridArea = "1 / 1 / -1 / -1";
39800
+ style.zIndex = maxId && id === maxId ? "5" : "";
39801
+ style.visibility = maxId && id !== maxId ? "hidden" : "";
39802
+ }
39803
+ }
39804
+ /** Rebuild every cell's view cluster (the maximize gate or state changed). */
39805
+ refreshCellControls() {
39806
+ for (const cell of this.cellsById.values()) cell.refreshControls();
39807
+ }
39808
+ /** Drop the transient maximize on a structural change (layout switch, state
39809
+ * document) — WITH the event, so hosts tracking `cell:maximized` never drift
39810
+ * from `maximizedCell`. The caller's own grid re-apply paints the restore. */
39811
+ clearMaximized() {
39812
+ if (this.maximizedId == null) return;
39813
+ this.maximizedId = null;
39814
+ this.events.emit("cell:maximized", { id: null });
39815
+ }
39816
+ /** The live cell under a viewport point, excluding `excludeId` and any host a
39817
+ * maximize has hidden — the drag handle's hit-test. */
39818
+ cellAtPoint(x, y, excludeId) {
39819
+ for (const [id, cell] of this.cellsById) {
39820
+ if (id === excludeId || cell.host.style.visibility === "hidden") continue;
39821
+ const r = cell.host.getBoundingClientRect();
39822
+ if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return id;
39823
+ }
39824
+ return null;
39825
+ }
39826
+ /** Mark one cell as the live drop target of a grip drag (null clears all) —
39827
+ * the `data-drop-target` stylesheet rule paints the dashed preview ring. */
39828
+ setDropTarget(id) {
39829
+ for (const [cid, cell] of this.cellsById) {
39830
+ if (cid === id) cell.host.dataset.dropTarget = "1";
39831
+ else delete cell.host.dataset.dropTarget;
39832
+ }
39833
+ }
39834
+ /** The cell whose bottom-left corner the grid's attribution mark floats in — the
39835
+ * maximized cell while one covers the grid, else the bottom-left slot's cell. */
39836
+ bottomLeftCell() {
39837
+ if (this.maximizedId) return this.cellsById.get(this.maximizedId);
39838
+ const grid = occupancyGrid(this.def);
39839
+ const slot = grid[grid.length - 1]?.[0];
39840
+ const idx = this.def.cells.findIndex((c) => (c.area ?? c.id) === slot);
39841
+ return this.cellsById.get(this.order[idx >= 0 ? idx : 0] ?? "");
39842
+ }
39843
+ /** Keep the shared attribution mark inside the BOTTOM-LEFT visible cell: its
39844
+ * offsets ride that cell's renderer-published `--vela-bottom-gutter` /
39845
+ * `--vela-toolbar-gutter`, so collapsed pane strips push the mark up without any
39846
+ * bookkeeping here. Re-run after anything that changes which host that is
39847
+ * (layout switch, maximize, cell rebuild); a destroyed host drops the mark from
39848
+ * the DOM, and this re-mount brings it back. */
39849
+ mountAttributionMark() {
39850
+ const mark = this.attributionMark;
39851
+ if (!mark) return;
39852
+ const host = this.bottomLeftCell()?.host ?? this.gridEl;
39853
+ if (mark.parentElement !== host) host.appendChild(mark);
39854
+ }
39155
39855
  /** Create the cells the current layout wants but don't exist yet (pool-first).
39156
39856
  * A slot's CELL IDENTITY is `order[i]` (declaration order — never the slot's own
39157
39857
  * positional id); slots past the declared list mint an auto identity once. */
@@ -39183,6 +39883,12 @@ var VelaWorkspace = class {
39183
39883
  setTimezone: (zone) => this.setTimezone(zone),
39184
39884
  context: () => this.context(),
39185
39885
  activate: (id2) => this.setActiveCell(id2),
39886
+ multiCell: () => !this.monoLayout && this.def.cells.length > 1,
39887
+ isMaximized: (id2) => this.maximizedId === id2,
39888
+ toggleMaximize: (id2) => this.maximizeCell(this.maximizedId === id2 ? null : id2),
39889
+ cellDragTarget: (id2, x, y) => this.cellAtPoint(x, y, id2),
39890
+ previewDropTarget: (target) => this.setDropTarget(target),
39891
+ dropCell: (id2, target) => this.swapCells(id2, target),
39186
39892
  onMarketChanged: (id2) => this.onCellMarketChanged(id2),
39187
39893
  onPriceStyleChanged: (id2) => this.onCellPriceStyleChanged(id2),
39188
39894
  onIndicatorsChanged: (id2) => this.onCellIndicatorsChanged(id2),
@@ -39196,6 +39902,7 @@ var VelaWorkspace = class {
39196
39902
  if (id === this.activeId) cell.host.dataset.active = "1";
39197
39903
  this.wireCell(cell);
39198
39904
  cell.chart.renderer.setLayoutMode(this.layoutCtl.current);
39905
+ cell.setControlsSuspended(this.layoutCtl.current === "mobile");
39199
39906
  if (this.favs.length > 0) cell.chart.drawings.setFavorites(this.favs);
39200
39907
  cell.setManifest(this.manifest, pooled?.indicators == null);
39201
39908
  cell.restorePersistedExt();
@@ -39205,6 +39912,7 @@ var VelaWorkspace = class {
39205
39912
  const host = this.cellsById.get(this.order[i] ?? "")?.host;
39206
39913
  if (host) this.gridEl.appendChild(host);
39207
39914
  }
39915
+ this.mountAttributionMark();
39208
39916
  }
39209
39917
  /** Per-cell chart subscriptions (trigger ② — the chart instance is stable for the
39210
39918
  * cell's whole life, so these live and die with the cell). */
@@ -39264,6 +39972,9 @@ var VelaWorkspace = class {
39264
39972
  chart.on("viewport:changed", (range) => this.propagateViewport(cell.id, range));
39265
39973
  chart.renderer.onConfigChanged(() => this.propagateStylePrefs(cell.id));
39266
39974
  chart.on("theme:changed", (t) => this.setTheme(t));
39975
+ chart.on("pane:changed", () => {
39976
+ if (cell.id === this.activeId) this.syncMobileMaximize();
39977
+ });
39267
39978
  chart.renderer.onAxisLongPress((e) => {
39268
39979
  if (this.layoutCtl.current !== "mobile") return;
39269
39980
  if (e.axis === "time") this.openTimezoneDrawer();
@@ -39584,6 +40295,7 @@ var VelaWorkspace = class {
39584
40295
  for (const cell of this.cellsById.values()) {
39585
40296
  cell.chart.renderer.closeDialogs();
39586
40297
  cell.chart.renderer.setLayoutMode(mode);
40298
+ cell.setControlsSuspended(mode === "mobile");
39587
40299
  }
39588
40300
  this.syncCellPresentation();
39589
40301
  }