@luxalgo/vela 0.6.9 → 0.6.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/{DataProvider-8Z95Q-RJ.d.cts → DataProvider-BBf-jc6W.d.ts} +49 -1
  2. package/dist/{DataProvider-DExJrfut.d.ts → DataProvider-p0TEyhlX.d.cts} +49 -1
  3. package/dist/{chunk-6WDDVMBJ.js → chunk-73PEA4MU.js} +707 -353
  4. package/dist/{chunk-62SVGONC.js → chunk-G77Y7LK2.js} +2 -2
  5. package/dist/{chunk-STHSKXOR.js → chunk-IO3NYSQV.js} +650 -157
  6. package/dist/{chunk-WFSZBX3R.js → chunk-KG4YT3TI.js} +3 -1
  7. package/dist/{chunk-FCVIG7JG.js → chunk-MHY7MVXH.js} +5 -1
  8. package/dist/{chunk-EQCHJZOT.js → chunk-MTLJKZDZ.js} +10 -2
  9. package/dist/{contributions-D7PVZO2i.d.ts → contributions-Bbe2R-mQ.d.ts} +20 -6
  10. package/dist/{contributions-C1U2Krwg.d.cts → contributions-CO01zWve.d.cts} +20 -6
  11. package/dist/index.cjs +719 -350
  12. package/dist/index.d.cts +37 -10
  13. package/dist/index.d.ts +37 -10
  14. package/dist/index.js +5 -5
  15. package/dist/{options-FM0peknS.d.ts → options-yp7sA96q.d.cts} +14 -7
  16. package/dist/{options-FM0peknS.d.cts → options-yp7sA96q.d.ts} +14 -7
  17. package/dist/{plugin-DfVqBz9p.d.cts → plugin-CkkH8QnX.d.cts} +3 -3
  18. package/dist/{plugin-7bkF32Rk.d.ts → plugin-DwxjM3Ni.d.ts} +3 -3
  19. package/dist/plugin.cjs +12 -1
  20. package/dist/plugin.d.cts +4 -4
  21. package/dist/plugin.d.ts +4 -4
  22. package/dist/plugin.js +3 -3
  23. package/dist/providers/binance.d.cts +2 -2
  24. package/dist/providers/binance.d.ts +2 -2
  25. package/dist/providers/coinbase.d.cts +2 -2
  26. package/dist/providers/coinbase.d.ts +2 -2
  27. package/dist/providers/hyperliquid.d.cts +2 -2
  28. package/dist/providers/hyperliquid.d.ts +2 -2
  29. package/dist/{statusline-DOPiT6I6.d.cts → statusline-CHPDuKNp.d.ts} +13 -5
  30. package/dist/{statusline-zdF4eZLr.d.ts → statusline-DTHZUFqK.d.cts} +13 -5
  31. package/dist/ui.cjs +7 -1
  32. package/dist/ui.d.cts +3 -1
  33. package/dist/ui.d.ts +3 -1
  34. package/dist/ui.js +3 -3
  35. package/dist/vela.global.js +719 -350
  36. package/dist/vela.global.min.js +51 -49
  37. package/dist/widget.cjs +1249 -388
  38. package/dist/widget.d.cts +17 -6
  39. package/dist/widget.d.ts +17 -6
  40. package/dist/widget.js +7 -7
  41. package/dist/workspace.cjs +1249 -388
  42. package/dist/workspace.d.cts +103 -10
  43. package/dist/workspace.d.ts +103 -10
  44. package/dist/workspace.js +6 -6
  45. package/package.json +1 -1
  46. /package/dist/{chunk-AOGZBKUE.js → chunk-H26BEHF4.js} +0 -0
@@ -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
  }
@@ -16621,11 +16798,12 @@ async function resolveIndicators(config, fetchImpl = fetch) {
16621
16798
  }
16622
16799
  return out;
16623
16800
  }
16801
+ var ledgerEntryName = (e) => typeof e === "string" ? e : e.name;
16624
16802
  function indicatorLedger(i) {
16625
16803
  const natives = [...i.present];
16626
16804
  if (i.volumePending && !natives.includes("volume")) natives.push("volume");
16627
16805
  return {
16628
- manifest: i.manifestSettled ? [...i.instanceNames] : [...i.pendingManifest ?? i.instanceNames],
16806
+ manifest: i.manifestSettled ? [...i.instanceEntries] : [...i.pendingManifest ?? i.instanceEntries],
16629
16807
  natives
16630
16808
  };
16631
16809
  }
@@ -16836,7 +17014,7 @@ var DrawingToolbar = class {
16836
17014
  this.root.replaceChildren();
16837
17015
  this.groupCells.clear();
16838
17016
  this.groupIcons.clear();
16839
- this.cursorBtn = this.makeButton(CURSOR_ICON, "Cursor", () => this.onArm(null));
17017
+ this.cursorBtn = this.makeButton(CURSOR_ICON, "Cursor", () => this.onCursorClick());
16840
17018
  this.root.appendChild(this.cursorBtn);
16841
17019
  if (this.def.groups.length > 0) this.root.appendChild(this.divider());
16842
17020
  for (const g of this.def.groups) {
@@ -16940,6 +17118,14 @@ var DrawingToolbar = class {
16940
17118
  this.magnetIcon = icon2;
16941
17119
  return cell;
16942
17120
  }
17121
+ /** Cursor returns to select/idle: an active measure/eraser mode exits through its own
17122
+ * toggle callback (disarming a tool via `onArm(null)` alone can't — the host treats a
17123
+ * null arm as a no-op side effect of entering those modes), then the tool disarms. */
17124
+ onCursorClick() {
17125
+ if (this.measureActive) this.onMeasure();
17126
+ if (this.eraserActive) this.onEraser();
17127
+ this.onArm(null);
17128
+ }
16943
17129
  /** Clicking the icon arms the group's last-used tool (it does NOT open the flyout). */
16944
17130
  onGroupIconClick(group) {
16945
17131
  const type = this.lastUsed.get(group.id) ?? group.tools[0]?.type;
@@ -17551,7 +17737,17 @@ function sanitizeCell(raw) {
17551
17737
  if (c.drawings != null && typeof c.drawings === "object") out.drawings = c.drawings;
17552
17738
  const ind = c.indicators;
17553
17739
  if (ind != null && typeof ind === "object") {
17554
- const manifest = Array.isArray(ind.manifest) ? ind.manifest.filter((n) => typeof n === "string") : [];
17740
+ const manifest = Array.isArray(ind.manifest) ? ind.manifest.flatMap((n) => {
17741
+ if (typeof n === "string") return [n];
17742
+ if (n != null && typeof n === "object" && typeof n.name === "string") {
17743
+ const e = n;
17744
+ const bag = (v) => v != null && typeof v === "object" && !Array.isArray(v) ? v : void 0;
17745
+ const inputs = bag(e.inputs);
17746
+ const props = bag(e.props);
17747
+ return [inputs || props ? { name: e.name, ...inputs ? { inputs } : {}, ...props ? { props } : {} } : e.name];
17748
+ }
17749
+ return [];
17750
+ }) : [];
17555
17751
  const natives = Array.isArray(ind.natives) ? ind.natives.filter((n) => typeof n === "string") : [];
17556
17752
  out.indicators = { manifest, natives };
17557
17753
  }
@@ -17750,6 +17946,12 @@ var IndicatorHandleImpl = class {
17750
17946
  get visible() {
17751
17947
  return this.visibleState;
17752
17948
  }
17949
+ inputValues() {
17950
+ return this.controller.inputValuesOf(this.id);
17951
+ }
17952
+ propValues() {
17953
+ return this.controller.propValuesOf(this.id);
17954
+ }
17753
17955
  setInput(key, value) {
17754
17956
  this.controller.applyInputs(this.id, { [key]: value });
17755
17957
  }
@@ -17889,6 +18091,7 @@ var RUN_EMIT_THROTTLE_MS = 1e3;
17889
18091
  var PREVIEW_BARS = 300;
17890
18092
  var SINGLE_LOAD_BARS = 5e3;
17891
18093
  var CHUNK_BARS = 1e4;
18094
+ var FIRST_PAINT_BARS = 100;
17892
18095
  var GAP_FACTOR = 1.5;
17893
18096
  var HEAL_COOLDOWN_MS = 5e3;
17894
18097
  var EngineOrchestrator = class _EngineOrchestrator {
@@ -17952,6 +18155,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
17952
18155
  /** Invalidates detached async work (backfill loops, in-flight loads, gap heals):
17953
18156
  * bumped by init(), setMarket() and destroy(). */
17954
18157
  this.generation = 0;
18158
+ /** Aborts the in-flight PROGRESSIVE load's source polling on supersession — an
18159
+ * abandoned stream left polling to its own budget starves the browser's per-host
18160
+ * connection pool, and the NEXT symbol's very first fetch with it (measured). */
18161
+ this.progressiveAbort = null;
17955
18162
  /** Awaiters racing a superseded load (setMarket callers) — released on every bump so they never hang. */
17956
18163
  this.supersedeWaiters = [];
17957
18164
  /** `history:complete` fired for the CURRENT load. Each market load re-arms the cycle
@@ -18094,6 +18301,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
18094
18301
  * superseded setMarket awaiters so their promises resolve instead of hanging. */
18095
18302
  bumpGeneration() {
18096
18303
  const gen = ++this.generation;
18304
+ this.progressiveAbort?.abort();
18305
+ this.progressiveAbort = null;
18097
18306
  for (const w of this.supersedeWaiters.splice(0)) w();
18098
18307
  return gen;
18099
18308
  }
@@ -18147,7 +18356,48 @@ var EngineOrchestrator = class _EngineOrchestrator {
18147
18356
  const requested = market.bars ?? 500;
18148
18357
  const initialRange = market.visibleRange;
18149
18358
  const deep = !market.data?.length && initialRange == null && requested > SINGLE_LOAD_BARS;
18150
- if (deep && this.feed.loadRange) {
18359
+ let progressiveServed = false;
18360
+ if (!market.data?.length && initialRange == null && this.feed.loadProgressive) {
18361
+ let painted = false;
18362
+ const paint = (bars, final) => {
18363
+ if (this.generation !== gen || !final && bars.length === 0) return;
18364
+ if (!painted && !final && bars.length < Math.min(requested, FIRST_PAINT_BARS)) return;
18365
+ this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
18366
+ if (!painted && bars.length > 0) {
18367
+ painted = true;
18368
+ if (opts.firstLoad) this.activateBarLayers();
18369
+ if (!final) this.historyState = "backfill";
18370
+ }
18371
+ };
18372
+ const abort = new AbortController();
18373
+ this.progressiveAbort = abort;
18374
+ progressiveServed = await new Promise((firstPaint) => {
18375
+ let signaled = false;
18376
+ const signal = (served) => {
18377
+ if (!signaled) {
18378
+ signaled = true;
18379
+ firstPaint(served);
18380
+ }
18381
+ };
18382
+ abort.signal.addEventListener("abort", () => signal(true), { once: true });
18383
+ this.feed.loadProgressive(market, (bars) => {
18384
+ paint(bars, false);
18385
+ if (painted) signal(true);
18386
+ }, { signal: abort.signal }).then((full) => {
18387
+ if (this.progressiveAbort === abort) this.progressiveAbort = null;
18388
+ if (full == null) return signal(false);
18389
+ if (this.generation !== gen) return signal(true);
18390
+ paint(full, true);
18391
+ this.completeHistory(full.length >= requested ? "depth" : "genesis");
18392
+ signal(true);
18393
+ }).catch(() => {
18394
+ if (this.progressiveAbort === abort) this.progressiveAbort = null;
18395
+ if (this.generation === gen) this.completeHistory("aborted");
18396
+ signal(true);
18397
+ });
18398
+ });
18399
+ }
18400
+ if (progressiveServed) ; else if (deep && this.feed.loadRange) {
18151
18401
  const head = await this.feed.load({ ...market, bars: Math.min(requested, CHUNK_BARS) });
18152
18402
  if (this.generation !== gen) return;
18153
18403
  this.setBarSeries(head);
@@ -18742,6 +18992,15 @@ var EngineOrchestrator = class _EngineOrchestrator {
18742
18992
  record.pendingCause = "inputs";
18743
18993
  if (record.session) record.session.update(record.inputValues);
18744
18994
  else if (record.native && !record.hidden) record.native.instance.setInputs(record.inputValues);
18995
+ this.events.emit("indicator:inputs", { id });
18996
+ }
18997
+ /** IndicatorController: the CURRENT stored input values (defaults merged with edits). */
18998
+ inputValuesOf(id) {
18999
+ return { ...this.registry.get(id)?.inputValues };
19000
+ }
19001
+ /** IndicatorController: the CURRENT declaration-prop overrides. */
19002
+ propValuesOf(id) {
19003
+ return { ...this.registry.get(id)?.propValues };
18745
19004
  }
18746
19005
  /** IndicatorController: re-run an indicator with merged declaration-prop overrides.
18747
19006
  * Same lifecycle as {@link applyInputs} — a prop change replays the whole script.
@@ -18755,6 +19014,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
18755
19014
  if (!record.hidden) this.setLoading(record, true);
18756
19015
  record.pendingCause = "inputs";
18757
19016
  record.session.update(record.inputValues, record.propValues);
19017
+ this.events.emit("indicator:inputs", { id });
18758
19018
  }
18759
19019
  /** IndicatorController: tear down an indicator and (if now empty) its pane. */
18760
19020
  /** Live handles of every indicator on the chart (script + native), insertion order. */
@@ -19976,6 +20236,14 @@ function inputVisible(when, values) {
19976
20236
  const conds = Array.isArray(when) ? when : [when];
19977
20237
  return conds.every((c) => c.anyOf ? c.anyOf.some((x) => x === values[c.key]) : values[c.key] === c.equals);
19978
20238
  }
20239
+ function inputDeltas(schema, values) {
20240
+ const out = {};
20241
+ for (const s of schema) {
20242
+ const v = values[s.key];
20243
+ if (v !== void 0 && JSON.stringify(v) !== JSON.stringify(s.defval)) out[s.key] = v;
20244
+ }
20245
+ return Object.keys(out).length > 0 ? out : void 0;
20246
+ }
19979
20247
 
19980
20248
  // src/renderers/shared/IndicatorInputsDialog.ts
19981
20249
  var PROPS_TAB = "Properties";
@@ -20890,6 +21158,9 @@ var CLOSE_SVG = iconAt("close", LEGEND_ICON_PX2);
20890
21158
  var FOLD_SVG = iconAt("chevron-up", LEGEND_ICON_PX2);
20891
21159
  var UNFOLD_SVG = iconAt("chevron-down", LEGEND_ICON_PX2);
20892
21160
  var OVERVIEW_SVG = iconAt("objects", LEGEND_ICON_PX2);
21161
+ function legendCalloutsDisplay(open2, hasCallouts) {
21162
+ return !open2 && hasCallouts ? "inline-flex" : "none";
21163
+ }
20893
21164
  var InputsUI = class {
20894
21165
  constructor(container, theme, paneBoundsOf) {
20895
21166
  this.container = container;
@@ -21093,7 +21364,7 @@ var InputsUI = class {
21093
21364
  row.callouts = [];
21094
21365
  row.calloutsEl.replaceChildren();
21095
21366
  const views = this.legendCallouts?.(row.id) ?? [];
21096
- row.calloutsEl.style.display = views.length > 0 ? "inline-flex" : "none";
21367
+ row.calloutsEl.style.display = legendCalloutsDisplay(row.highlighted, views.length > 0);
21097
21368
  for (const view of views) {
21098
21369
  const bubble = new CalloutBubble({
21099
21370
  icon: view.icon,
@@ -21577,11 +21848,11 @@ var InputsUI = class {
21577
21848
  row.controlsEl.style.display = open2 || row.hidden ? "inline-flex" : "none";
21578
21849
  if (open2) {
21579
21850
  row.el.appendChild(row.statusEl);
21580
- row.el.appendChild(row.calloutsEl);
21851
+ for (const bubble of row.callouts) bubble.hidePanel();
21581
21852
  } else {
21582
21853
  row.el.insertBefore(row.statusEl, row.valuesEl);
21583
- row.el.insertBefore(row.calloutsEl, row.statusEl);
21584
21854
  }
21855
+ row.calloutsEl.style.display = legendCalloutsDisplay(open2, row.callouts.length > 0);
21585
21856
  for (const child of Array.from(row.controlsEl.children)) {
21586
21857
  if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
21587
21858
  if (child === row.extrasEl) {
@@ -21671,7 +21942,7 @@ var ICONS = {
21671
21942
  };
21672
21943
  var STYLE_ID21 = "vela-pane-controls";
21673
21944
  var ICON_PX = 12;
21674
- var CLUSTER_PILL = "rgba(0,0,0,0.28)";
21945
+ var CLUSTER_PILL = "rgba(0,0,0,0.65)";
21675
21946
  function ensureStyles3() {
21676
21947
  if (typeof document === "undefined" || document.getElementById(STYLE_ID21)) return;
21677
21948
  const st = document.createElement("style");
@@ -21691,8 +21962,12 @@ var PaneControls = class {
21691
21962
  this.deps = deps;
21692
21963
  this.clusters = /* @__PURE__ */ new Map();
21693
21964
  this.hoverPaneId = null;
21965
+ /** Mobile: hover clusters are meaningless without a cursor — suppressed; a
21966
+ * collapsed pane's standalone expand chip stays (the only way back up). */
21967
+ this.suspended = false;
21694
21968
  /** Reveal the cluster for the pane under the cursor, resolved from the pointer's y in the plot. */
21695
21969
  this.onPlotMove = (e) => {
21970
+ if (this.suspended) return;
21696
21971
  const rect = this.plot.getBoundingClientRect();
21697
21972
  const y = e.clientY - rect.top;
21698
21973
  let hit = null;
@@ -21764,7 +22039,12 @@ var PaneControls = class {
21764
22039
  }
21765
22040
  if (p.count > 1) {
21766
22041
  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" })
22042
+ this.button(p.maximized ? ICONS.restore : ICONS.maximize, p.maximized ? "Restore pane" : "Maximize pane", false, () => this.deps.onToggleMaximize(p.id), {
22043
+ role: "maximize",
22044
+ // Same inverse-chip treatment as the collapsed pane's expand toggle: the
22045
+ // maximized state must read as an active state, not just a swapped glyph.
22046
+ selected: p.maximized
22047
+ })
21768
22048
  );
21769
22049
  }
21770
22050
  }
@@ -21801,17 +22081,18 @@ var PaneControls = class {
21801
22081
  }
21802
22082
  const hovered = id === this.hoverPaneId;
21803
22083
  const hasButtons = cluster.children.length > 0;
21804
- const visible = hasButtons && (hovered || p.collapsed) && p.height > 8;
22084
+ const stateChipRole = p.collapsed ? "collapse" : !this.suspended && p.maximized ? "maximize" : null;
22085
+ const visible = hasButtons && (hovered || stateChipRole != null) && p.height > 8;
21805
22086
  cluster.style.right = `${rightPx}px`;
21806
22087
  cluster.style.top = p.collapsed ? `${p.top + Math.max(1, Math.round((p.height - 24) / 2))}px` : `${p.top + 4}px`;
21807
22088
  cluster.style.display = visible ? "flex" : "none";
21808
22089
  if (!visible) continue;
21809
- const soloExpand = p.collapsed && !hovered;
21810
- cluster.style.background = soloExpand ? "transparent" : CLUSTER_PILL;
22090
+ const soloChip = stateChipRole != null && !hovered;
22091
+ cluster.style.background = soloChip ? "transparent" : CLUSTER_PILL;
21811
22092
  for (const child of cluster.children) {
21812
22093
  const btn2 = child;
21813
22094
  btn2.style.display = "inline-flex";
21814
- btn2.style.visibility = soloExpand && btn2.dataset.role !== "collapse" ? "hidden" : "visible";
22095
+ btn2.style.visibility = soloChip && btn2.dataset.role !== stateChipRole ? "hidden" : "visible";
21815
22096
  }
21816
22097
  }
21817
22098
  }
@@ -21821,6 +22102,14 @@ var PaneControls = class {
21821
22102
  this.hoverPaneId = paneId;
21822
22103
  this.reposition();
21823
22104
  }
22105
+ /** Mobile suppression: no hover clusters (touch has no cursor; the shell's own
22106
+ * chrome covers maximize), while collapsed panes keep their expand chips. */
22107
+ setSuspended(on) {
22108
+ if (on === this.suspended) return;
22109
+ this.suspended = on;
22110
+ if (on) this.hoverPaneId = null;
22111
+ this.reposition();
22112
+ }
21824
22113
  destroy() {
21825
22114
  this.plot.removeEventListener("pointermove", this.onPlotMove);
21826
22115
  this.plot.removeEventListener("pointerleave", this.onPlotLeave);
@@ -21971,160 +22260,6 @@ var AxisScaleButtons = class {
21971
22260
  }
21972
22261
  };
21973
22262
 
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
22263
  // src/renderers/native/capabilities.ts
22129
22264
  var NATIVE_CAPABILITIES = {
22130
22265
  panes: true,
@@ -22145,7 +22280,7 @@ var NATIVE_CAPABILITIES = {
22145
22280
  drawingDepth: true,
22146
22281
  // drawings share the series' z space (backend-composited interleave layers)
22147
22282
  tables: true,
22148
- // reuses the DOM TableOverlay
22283
+ // canvas-painted into the owning indicator's interleave slice
22149
22284
  trades: true,
22150
22285
  // strategy order-fill markers (arrows + labels + fill-price ticks)
22151
22286
  inputsUI: true
@@ -22176,6 +22311,9 @@ function candleTier(spacing) {
22176
22311
  if (spacing < CANDLE_BODY_MIN_SPACING) return "wick";
22177
22312
  return "full";
22178
22313
  }
22314
+ function snapY(yCss, dpr) {
22315
+ return Math.round(yCss * dpr) / dpr;
22316
+ }
22179
22317
  function candleGeometry(xCss, spacing, dpr, bodyScale = 1) {
22180
22318
  const wickDev = Math.max(1, Math.round(wickWidth(spacing) * dpr));
22181
22319
  const wickLeftDev = Math.round(xCss * dpr - wickDev / 2);
@@ -22699,11 +22837,9 @@ var WebGL2Backend = class {
22699
22837
  };
22700
22838
  b.alpha = this.modelAlpha;
22701
22839
  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
22840
  if (isPrice) {
22704
22841
  for (const m of scene.indicators.values()) {
22705
22842
  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
22843
  }
22708
22844
  }
22709
22845
  const drawCandles = isPrice && !scene.candlesHidden;
@@ -22719,6 +22855,7 @@ var WebGL2Backend = class {
22719
22855
  b.alpha = this.modelAlpha;
22720
22856
  const off = scene.offsetOf(m.id);
22721
22857
  const mp = effPane(m);
22858
+ for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, mp, coords, i0, i1, off);
22722
22859
  for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
22723
22860
  }
22724
22861
  if (drawCandles && !candleDrawn) {
@@ -22726,14 +22863,18 @@ var WebGL2Backend = class {
22726
22863
  b.alpha = this.candleStructureAlpha;
22727
22864
  this.emitPriceSeries(b, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
22728
22865
  }
22729
- drawSlicesUpTo(Infinity);
22730
22866
  b.alpha = this.modelAlpha;
22731
22867
  if (isPrice) {
22868
+ for (const m of scene.indicators.values()) {
22869
+ const off = scene.offsetOf(m.id);
22870
+ for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, i1, off);
22871
+ }
22732
22872
  for (const m of scene.indicators.values()) {
22733
22873
  const off = scene.offsetOf(m.id);
22734
22874
  for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0, i1, theme, off);
22735
22875
  }
22736
22876
  }
22877
+ drawSlicesUpTo(Infinity);
22737
22878
  for (const m of models) {
22738
22879
  const mp = effPane(m);
22739
22880
  for (const pl of m.priceLines) this.emitHline(b, pl, mp, coords, dataW, theme);
@@ -23128,12 +23269,12 @@ var WebGL2Backend = class {
23128
23269
  if (drawBody) {
23129
23270
  const oY = coords.priceToY(bar.open, pane.scale, pane.bounds);
23130
23271
  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));
23272
+ bodyTop = snapY(Math.min(oY, cY), coords.dpr);
23273
+ bodyH = Math.max(1 / coords.dpr, snapY(Math.max(oY, cY), coords.dpr) - bodyTop);
23133
23274
  }
23134
23275
  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);
23276
+ const hY = snapY(coords.priceToY(bar.high, pane.scale, pane.bounds), coords.dpr);
23277
+ const lY = snapY(coords.priceToY(bar.low, pane.scale, pane.bounds), coords.dpr);
23137
23278
  b.alpha = this.candleStructureAlpha;
23138
23279
  const wCol = parseColor((isUp ? cs.wickUpColor : cs.wickDownColor) ?? (drawBody ? dir : bodyColorStr));
23139
23280
  if (drawBody) {
@@ -23690,6 +23831,7 @@ var DOUBLE_TAP_MS = 350;
23690
23831
  var DOUBLE_TAP_SLOP = 30;
23691
23832
  var TIME_SCALE_K = 4e-3;
23692
23833
  var WHEEL_ZOOM_K = 4e-3;
23834
+ var WHEEL_PRICE_DRAG_PX = 0.25;
23693
23835
  function wheelZoomAnchor(coords, cursorX, rightEdge) {
23694
23836
  if (rightEdge) return { logical: coords.rightEdgeLogical, x: coords.width };
23695
23837
  return { logical: coords.xToLogical(cursorX), x: cursorX };
@@ -23820,7 +23962,7 @@ var InputController = class {
23820
23962
  this.capture(e.pointerId);
23821
23963
  return;
23822
23964
  }
23823
- if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y)) {
23965
+ if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y, this.snapMode(e))) {
23824
23966
  this.region = "drawing";
23825
23967
  this.capture(e.pointerId);
23826
23968
  return;
@@ -23934,7 +24076,7 @@ var InputController = class {
23934
24076
  const wasTouch = e.pointerType === "touch";
23935
24077
  const tapRelease = this.dragging && !this.moved && (!wasTouch || Math.hypot(x - this.startX, y - this.startY) <= TOUCH_TAP_SLOP);
23936
24078
  if (this.dragging && this.region === "drawing") {
23937
- this.deps.drawingsPointerUp?.(x, y);
24079
+ this.deps.drawingsPointerUp?.(x, y, this.snapMode(e));
23938
24080
  } else if (tapRelease && this.region === "data") {
23939
24081
  this.deps.onClick(x, y);
23940
24082
  } else if (this.dragging && this.region === "data") {
@@ -23955,7 +24097,7 @@ var InputController = class {
23955
24097
  if (e.pointerType === "touch") this.touches.delete(e.pointerId);
23956
24098
  this.cancelLongPress();
23957
24099
  if (!this.dragging) return;
23958
- if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY);
24100
+ if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY, this.snapMode(e));
23959
24101
  if (this.region === "crosshair" || e.pointerType === "touch") this.deps.onPointerMove(null, null);
23960
24102
  this.endGesture(e);
23961
24103
  };
@@ -23971,6 +24113,12 @@ var InputController = class {
23971
24113
  this.onWheel = (e) => {
23972
24114
  e.preventDefault();
23973
24115
  this.deps.drawingsClearTransient?.();
24116
+ const { x, y } = this.local(e);
24117
+ if (this.regionAt(x, y) === "price" && e.deltaY !== 0) {
24118
+ this.deps.beginPriceScale(x, y);
24119
+ this.deps.priceScaleBy(e.deltaY * WHEEL_PRICE_DRAG_PX);
24120
+ return;
24121
+ }
23974
24122
  const coords = this.deps.getCoords();
23975
24123
  const vp = coords.getViewport();
23976
24124
  const pan = wheelPanDelta(e.deltaX, e.deltaY, e.shiftKey);
@@ -23978,9 +24126,8 @@ var InputController = class {
23978
24126
  this.deps.apply({ barSpacing: vp.barSpacing, rightOffset: wheelPanRightOffset(vp.rightOffset, pan, coords.pxPerBar()) });
23979
24127
  return;
23980
24128
  }
23981
- const cursorX = this.local(e).x;
23982
24129
  const rightEdge = this.rightEdgeZoom && !(e.ctrlKey || e.metaKey);
23983
- const anchor = wheelZoomAnchor(coords, cursorX, rightEdge);
24130
+ const anchor = wheelZoomAnchor(coords, x, rightEdge);
23984
24131
  const target = clampBarSpacing(vp.barSpacing * Math.exp(-e.deltaY * WHEEL_ZOOM_K));
23985
24132
  this.deps.zoomTo(target, anchor.logical, anchor.x);
23986
24133
  };
@@ -24466,9 +24613,10 @@ var SceneGraph = class {
24466
24613
  * so each indicator arrives behind the candles (and behind older indicators);
24467
24614
  * `setIndicatorZ`/`bringToFront`/`sendToBack` change it. */
24468
24615
  this.seriesZ = /* @__PURE__ */ new Map();
24469
- /** Per-pane raster layers of user drawings interleaved into the series stack each is a
24616
+ /** Per-pane raster layers of drawings interleaved into the series stack (each
24617
+ * indicator's Pine drawings at its model's z, plus in-stack user drawings) — each a
24470
24618
  * 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. */
24619
+ * Rebuilt by the renderer per data frame. */
24472
24620
  this.drawingSlices = /* @__PURE__ */ new Map();
24473
24621
  /** Per-model index offset: the chart bar index of the model's `anchorTime` — its
24474
24622
  * index-aligned payloads (dense series arrays, `bar_index` drawings) count from that
@@ -24703,11 +24851,9 @@ var Canvas2dBackend = class {
24703
24851
  };
24704
24852
  ctx.globalAlpha = this.modelAlpha;
24705
24853
  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
24854
  if (isPrice) {
24708
24855
  for (const m of scene.indicators.values()) {
24709
24856
  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
24857
  }
24712
24858
  }
24713
24859
  const slices = scene.drawingSlices.get(pane.id) ?? [];
@@ -24731,6 +24877,7 @@ var Canvas2dBackend = class {
24731
24877
  ctx.globalAlpha = this.modelAlpha;
24732
24878
  const off = scene.offsetOf(m.id);
24733
24879
  const mp = effPane(m);
24880
+ for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, mp, coords, i0, i1, off);
24734
24881
  for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
24735
24882
  }
24736
24883
  if (drawCandles && !candleDrawn) {
@@ -24738,14 +24885,18 @@ var Canvas2dBackend = class {
24738
24885
  ctx.globalAlpha = this.candleStructureAlpha;
24739
24886
  this.drawPriceSeries(ctx, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
24740
24887
  }
24741
- drawSlicesUpTo(Infinity);
24742
24888
  if (isPrice) {
24743
24889
  ctx.globalAlpha = this.modelAlpha;
24890
+ for (const m of scene.indicators.values()) {
24891
+ const off = scene.offsetOf(m.id);
24892
+ for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, off);
24893
+ }
24744
24894
  for (const m of scene.indicators.values()) {
24745
24895
  const off = scene.offsetOf(m.id);
24746
24896
  for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0, i1, theme, off);
24747
24897
  }
24748
24898
  }
24899
+ drawSlicesUpTo(Infinity);
24749
24900
  ctx.globalAlpha = this.modelAlpha;
24750
24901
  for (const m of models) {
24751
24902
  const mp = effPane(m);
@@ -24989,13 +25140,13 @@ var Canvas2dBackend = class {
24989
25140
  if (drawBody) {
24990
25141
  const oY = coords.priceToY(b.open, pane.scale, pane.bounds);
24991
25142
  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));
25143
+ top = snapY(Math.min(oY, cY), coords.dpr);
25144
+ bodyH = Math.max(1 / coords.dpr, snapY(Math.max(oY, cY), coords.dpr) - top);
24994
25145
  }
24995
25146
  if (cs.wickVisible) {
24996
25147
  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);
25148
+ const hY = snapY(coords.priceToY(b.high, pane.scale, pane.bounds), coords.dpr);
25149
+ const lY = snapY(coords.priceToY(b.low, pane.scale, pane.bounds), coords.dpr);
24999
25150
  ctx.globalAlpha = this.candleStructureAlpha;
25000
25151
  ctx.strokeStyle = wick;
25001
25152
  ctx.lineWidth = g.wickW;
@@ -25442,9 +25593,31 @@ function autoFontSize(lines, boxW, boxH, bold) {
25442
25593
 
25443
25594
  // src/renderers/shared/DrawingSceneRenderer.ts
25444
25595
  var EMPTY_DRAWING_SET = { lines: [], boxes: [], labels: [], polylines: [], linefills: [] };
25596
+ function modelDrawingSet(m, overlay) {
25597
+ const want = (d) => Boolean(d.overlay) === overlay;
25598
+ return {
25599
+ lines: (m.lines ?? []).filter(want),
25600
+ boxes: (m.boxes ?? []).filter(want),
25601
+ labels: (m.labels ?? []).filter(want),
25602
+ polylines: (m.polylines ?? []).filter(want),
25603
+ linefills: (m.linefills ?? []).filter(want)
25604
+ };
25605
+ }
25606
+ function drawingSetEmpty(s) {
25607
+ return !s.lines.length && !s.boxes.length && !s.labels.length && !s.polylines.length && !s.linefills.length;
25608
+ }
25445
25609
  function fontSizePx(size) {
25446
25610
  return size === "auto" ? 12 : namedFontSize(size);
25447
25611
  }
25612
+ function lineCoversWindow(a, b, extend, lo, hi) {
25613
+ const minX = Math.min(a, b);
25614
+ const maxX = Math.max(a, b);
25615
+ if (a === b) return a >= lo && a <= hi;
25616
+ if (extend === "both") return true;
25617
+ if (extend === "left") return maxX >= lo;
25618
+ if (extend === "right") return minX <= hi;
25619
+ return maxX >= lo && minX <= hi;
25620
+ }
25448
25621
  var DrawingSceneRenderer = class {
25449
25622
  constructor(deps, set = EMPTY_DRAWING_SET) {
25450
25623
  this.deps = deps;
@@ -25509,7 +25682,7 @@ var DrawingSceneRenderer = class {
25509
25682
  };
25510
25683
  for (const ln of this.set.lines) {
25511
25684
  if (ln.invisible) continue;
25512
- if (!visible(this.logicalOf(ln.xloc, ln.x1), this.logicalOf(ln.xloc, ln.x2), ln.extend)) continue;
25685
+ if (!lineCoversWindow(this.logicalOf(ln.xloc, ln.x1), this.logicalOf(ln.xloc, ln.x2), ln.extend, lo, hi)) continue;
25513
25686
  fold(ln.y1);
25514
25687
  fold(ln.y2);
25515
25688
  }
@@ -26294,10 +26467,8 @@ var ChromeRenderer = class {
26294
26467
  this.ctx = null;
26295
26468
  // The color for axis tick labels — the host-passed surface text, set each frame in render().
26296
26469
  this.axisTextColor = DARK_THEME.textColor;
26297
- // Shared Pine-drawing renderer (line/box/label/polyline/linefill); widthCache persists.
26470
+ // Shared Pine-drawing renderer, used here for autoscale geometry only; widthCache persists.
26298
26471
  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
26472
  }
26302
26473
  mount(canvas) {
26303
26474
  this.canvas = canvas;
@@ -26321,8 +26492,8 @@ var ChromeRenderer = class {
26321
26492
  */
26322
26493
  paneDrawingsRange(ownModels, scene, isPricePane, vr) {
26323
26494
  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)));
26495
+ for (const m of ownModels) dr = unionRange(dr, this.drawingsRange(modelDrawingSet(m, false), vr, scene.offsetOf(m.id)));
26496
+ if (isPricePane) for (const m of scene.indicators.values()) dr = unionRange(dr, this.drawingsRange(modelDrawingSet(m, true), vr, scene.offsetOf(m.id)));
26326
26497
  return dr;
26327
26498
  }
26328
26499
  /** Clear the chrome canvas and draw drawings + axes + current-price line.
@@ -26339,7 +26510,6 @@ var ChromeRenderer = class {
26339
26510
  const dataW = coords.width;
26340
26511
  const dataH = coords.height;
26341
26512
  this.axisTextColor = surface?.textColor ?? theme.textColor;
26342
- this.labelTips = [];
26343
26513
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
26344
26514
  ctx.clearRect(0, 0, fullW, fullH);
26345
26515
  if (surface && (fullW > dataW || fullH > dataH)) {
@@ -26355,17 +26525,6 @@ var ChromeRenderer = class {
26355
26525
  return;
26356
26526
  }
26357
26527
  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
26528
  if (pricePane && !pricePane.collapsed && scene.tradeMarkers.visible) {
26370
26529
  for (const m of scene.indicators.values()) {
26371
26530
  if (m.trades?.length) this.renderTrades(ctx, coords, scene, theme, m.trades, pricePane, dataW);
@@ -26381,25 +26540,6 @@ var ChromeRenderer = class {
26381
26540
  this.canvas = null;
26382
26541
  this.ctx = null;
26383
26542
  }
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
26543
  drawingsRange(set, vr, indexOffset = 0) {
26404
26544
  this.drawScene.setSet(set, indexOffset);
26405
26545
  if (this.drawScene.isEmpty()) return null;
@@ -26433,34 +26573,6 @@ var ChromeRenderer = class {
26433
26573
  );
26434
26574
  ctx.restore();
26435
26575
  }
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
26576
  // ── axes ──
26465
26577
  drawPriceAxes(ctx, scene, coords, theme, dataW, panes) {
26466
26578
  ctx.strokeStyle = scene.style.borderColor ?? theme.borderColor;
@@ -29420,6 +29532,22 @@ var DrawingInteraction = class {
29420
29532
  this.snapAt = changed ? { point: snapped, paneId } : null;
29421
29533
  return snapped;
29422
29534
  }
29535
+ /**
29536
+ * Resolve a cursor pixel through the magnet and return the snapped pixel — the same
29537
+ * conversion drawing placement uses. Updates the snap-ring marker. The measure
29538
+ * ruler goes through this so its endpoints follow weak/strong/Ctrl magnet too.
29539
+ */
29540
+ snapCursor(x, y, mode) {
29541
+ const proj = this.deps.projector();
29542
+ const paneId = proj.paneIdAtY(y) ?? "price";
29543
+ const point = this.resolve(x, y, paneId, mode);
29544
+ const sy = proj.yOf(point.price, paneId);
29545
+ return { x: proj.xOf(point.time), y: sy ?? y };
29546
+ }
29547
+ /** Drop the snap-ring marker (a transient mode ended without going through `up`). */
29548
+ clearSnapMarker() {
29549
+ this.snapAt = null;
29550
+ }
29423
29551
  /** Resolve a pixel to a data point with the segment angle locked to 45° steps around
29424
29552
  * `pivot` (Shift held on a line tool). Works in PIXEL space — the user reasons about
29425
29553
  * the angle they see, not about time/price units. The magnet is bypassed: snapping
@@ -30779,17 +30907,17 @@ function glyphIcon(glyph) {
30779
30907
  return textGlyph(String(glyph), 15);
30780
30908
  }
30781
30909
  function stampSizeIcon(size) {
30782
- return textGlyph("\u25CF", (SIZE_PX4[String(size)] ?? 13) + 4);
30910
+ return textGlyph("\u25CF", (SIZE_PX3[String(size)] ?? 13) + 4);
30783
30911
  }
30784
30912
  function sizeIcon(size) {
30785
30913
  return textGlyph(String(size).charAt(0).toUpperCase(), 15);
30786
30914
  }
30787
- var SIZE_PX4 = { small: 10, normal: 13, large: 16, huge: 20 };
30915
+ var SIZE_PX3 = { small: 10, normal: 13, large: 16, huge: 20 };
30788
30916
  function numbersSizeIcon(size) {
30789
- return textGlyph("12", (SIZE_PX4[String(size)] ?? 13) - 1, 16.5, 'font-weight="600"');
30917
+ return textGlyph("12", (SIZE_PX3[String(size)] ?? 13) - 1, 16.5, 'font-weight="600"');
30790
30918
  }
30791
30919
  function labelSizeIcon(size) {
30792
- return textGlyph("T", (SIZE_PX4[String(size)] ?? 13) + 2);
30920
+ return textGlyph("T", (SIZE_PX3[String(size)] ?? 13) + 2);
30793
30921
  }
30794
30922
  function capitalize(s) {
30795
30923
  return s.charAt(0).toUpperCase() + s.slice(1);
@@ -30825,30 +30953,39 @@ var MeasureOverlay = class {
30825
30953
  isFinished() {
30826
30954
  return this.state === "finished";
30827
30955
  }
30828
- /** A press: begin the measurement, or finish it on the second click. */
30829
- down(x, y) {
30956
+ /** A press: begin the measurement, or finish it on the second click.
30957
+ * `x,y` are the raw cursor (drag-slop vs click-move-click). `gx,gy` are the
30958
+ * graphic endpoints — magnet-snapped when the magnet is on, else the same as `x,y`. */
30959
+ down(x, y, gx = x, gy = y) {
30830
30960
  if (this.state === "measuring") {
30831
- this.end = { x, y };
30961
+ this.end = { x: gx, y: gy };
30832
30962
  this.state = "finished";
30833
30963
  return;
30834
30964
  }
30835
- this.start = { x, y };
30836
- this.end = { x, y };
30965
+ this.start = { x: gx, y: gy };
30966
+ this.end = { x: gx, y: gy };
30837
30967
  this.pressX = x;
30838
30968
  this.pressY = y;
30839
30969
  this.state = "measuring";
30840
30970
  }
30971
+ /** Size the in-progress ruler. `x,y` are the graphic (magnet-snapped) cursor. */
30841
30972
  move(x, y) {
30842
30973
  if (this.state === "measuring") this.end = { x, y };
30843
30974
  }
30844
30975
  /** A release: finish if the press was actually dragged (press-drag-release), else wait
30845
- * for the second click (click-move-click). */
30846
- up(x, y) {
30976
+ * for the second click (click-move-click). `x,y` are the raw cursor (slop); `gx,gy`
30977
+ * are the graphic end (magnet-snapped when the magnet is on). */
30978
+ up(x, y, gx = x, gy = y) {
30847
30979
  if (this.state === "measuring" && Math.hypot(x - this.pressX, y - this.pressY) > DRAG_SLOP4) {
30848
- this.end = { x, y };
30980
+ this.end = { x: gx, y: gy };
30849
30981
  this.state = "finished";
30850
30982
  }
30851
30983
  }
30984
+ /** Current graphic endpoints in media pixels, or null when idle. */
30985
+ points() {
30986
+ if (!this.start || !this.end) return null;
30987
+ return { start: this.start, end: this.end };
30988
+ }
30852
30989
  clear() {
30853
30990
  this.state = "idle";
30854
30991
  this.start = null;
@@ -31225,11 +31362,13 @@ var UserDrawingController = class {
31225
31362
  }
31226
31363
  /** Shift+press on the empty plot: arm the measure ruler AND start it at (x, y) in one
31227
31364
  * gesture — the equivalent of clicking the toolbar's Measure button, then pressing.
31228
- * Returns false when a mode/tool is already active (the normal press path owns it). */
31229
- beginMeasureAt(x, y) {
31365
+ * `snap` is the effective magnet (sticky mode, or Ctrl/Cmd-forced strong). Returns
31366
+ * false when a mode/tool is already active (the normal press path owns it). */
31367
+ beginMeasureAt(x, y, snap = "off") {
31230
31368
  if (this.measureMode || this.eraserMode || this.activeTool != null) return false;
31231
31369
  this.withModeIntent(() => this.toggleMeasure());
31232
- this.measure.down(x, y);
31370
+ const g = this.interaction.snapCursor(x, y, snap);
31371
+ this.measure.down(x, y, g.x, g.y);
31233
31372
  this.render();
31234
31373
  return true;
31235
31374
  }
@@ -31251,7 +31390,8 @@ var UserDrawingController = class {
31251
31390
  return;
31252
31391
  }
31253
31392
  if (this.measureMode) {
31254
- this.measure.down(x, y);
31393
+ const g = this.interaction.snapCursor(x, y, snap);
31394
+ this.measure.down(x, y, g.x, g.y);
31255
31395
  if (this.measure.isFinished()) this.withModeIntent(() => this.exitMeasure(false));
31256
31396
  this.render();
31257
31397
  return;
@@ -31264,7 +31404,8 @@ var UserDrawingController = class {
31264
31404
  return;
31265
31405
  }
31266
31406
  if (this.measureMode) {
31267
- this.measure.move(x, y);
31407
+ const g = this.interaction.snapCursor(x, y, snap);
31408
+ this.measure.move(g.x, g.y);
31268
31409
  this.render();
31269
31410
  return;
31270
31411
  }
@@ -31282,13 +31423,14 @@ var UserDrawingController = class {
31282
31423
  this.render();
31283
31424
  }
31284
31425
  }
31285
- pointerUp(x, y) {
31426
+ pointerUp(x, y, snap = "off") {
31286
31427
  if (this.eraserMode) {
31287
31428
  this.erasing = false;
31288
31429
  return;
31289
31430
  }
31290
31431
  if (this.measureMode) {
31291
- this.measure.up(x, y);
31432
+ const g = this.interaction.snapCursor(x, y, snap);
31433
+ this.measure.up(x, y, g.x, g.y);
31292
31434
  if (this.measure.isFinished()) this.withModeIntent(() => this.exitMeasure(false));
31293
31435
  this.render();
31294
31436
  return;
@@ -31333,6 +31475,7 @@ var UserDrawingController = class {
31333
31475
  /** Leave ruler mode. `clearGraphic` keeps a just-finished measurement on screen (false). */
31334
31476
  exitMeasure(clearGraphic = true) {
31335
31477
  this.measureMode = false;
31478
+ this.interaction.clearSnapMarker();
31336
31479
  if (clearGraphic) this.measure.clear();
31337
31480
  this.toolbar.setMeasureActive(false);
31338
31481
  this.render();
@@ -31473,15 +31616,28 @@ var UserDrawingController = class {
31473
31616
  if (this.eraserMode) return "pointer";
31474
31617
  return this.interaction.cursorAt(x, y);
31475
31618
  }
31476
- /** Right-click while placing: cancel the in-progress drawing and revert to the
31477
- * pointer the gesture is an explicit escape, so it disarms even in
31478
- * stay-in-drawing-mode (where Escape would leave the tool armed). Returns whether
31479
- * the press was consumed; false lets the host's context menu open normally. */
31619
+ /** Right-click: an explicit escape back to the pointer. Cancels an in-progress
31620
+ * placement or measurement, and also plain-disarms an armed-but-idle drawing
31621
+ * tool or the eraser — so a right-click ALWAYS reverts to the pointer, even in
31622
+ * stay-in-drawing-mode (where Escape would leave a drawing tool armed).
31623
+ * Persistent toggles (magnet, stay-mode, favorites) are untouched. Returns
31624
+ * whether the press was consumed; false lets the host's context menu open
31625
+ * normally. */
31480
31626
  cancelPlacement() {
31481
- if (!this.interaction.isPlacing()) return false;
31482
- this.interaction.cancel();
31483
- if (this.activeTool != null) this.emit({ kind: "arm", type: null });
31484
- return true;
31627
+ if (this.measureMode || this.eraserMode) {
31628
+ this.withModeIntent(() => this.measureMode ? this.exitMeasure() : this.exitEraser());
31629
+ return true;
31630
+ }
31631
+ if (this.interaction.isPlacing()) {
31632
+ this.interaction.cancel();
31633
+ if (this.activeTool != null) this.emit({ kind: "arm", type: null });
31634
+ return true;
31635
+ }
31636
+ if (this.activeTool != null) {
31637
+ this.emit({ kind: "arm", type: null });
31638
+ return true;
31639
+ }
31640
+ return false;
31485
31641
  }
31486
31642
  /** Double-click over a drawing → suppress the chart's view reset (single-click already
31487
31643
  * opens settings). Returns true only when a drawing is under the cursor. */
@@ -31507,6 +31663,10 @@ var UserDrawingController = class {
31507
31663
  return true;
31508
31664
  }
31509
31665
  if (this.interaction.cancel()) return true;
31666
+ if (this.measureMode) {
31667
+ this.withModeIntent(() => this.exitMeasure());
31668
+ return true;
31669
+ }
31510
31670
  if (this.selectedIds.size) {
31511
31671
  this.clearSelection();
31512
31672
  return true;
@@ -31764,6 +31924,315 @@ var UserDrawingController = class {
31764
31924
  }
31765
31925
  };
31766
31926
 
31927
+ // src/renderers/shared/TableOverlay.ts
31928
+ var SIZE_PX4 = {
31929
+ auto: 13,
31930
+ tiny: 10,
31931
+ small: 11,
31932
+ normal: 13,
31933
+ large: 16,
31934
+ huge: 20
31935
+ };
31936
+ function fontPxOf(size) {
31937
+ if (typeof size === "number") return size > 0 ? size : SIZE_PX4.auto;
31938
+ return SIZE_PX4[size] ?? SIZE_PX4.auto;
31939
+ }
31940
+ function tableHasContent(t) {
31941
+ return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
31942
+ }
31943
+ function mergeRenderPlan(t) {
31944
+ const span = /* @__PURE__ */ new Map();
31945
+ const omit = /* @__PURE__ */ new Set();
31946
+ for (const m of t.merges) {
31947
+ span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
31948
+ for (let r = m.startRow; r <= m.endRow; r += 1) {
31949
+ for (let c = m.startCol; c <= m.endCol; c += 1) {
31950
+ if (r !== m.startRow || c !== m.startCol) omit.add(`${r}:${c}`);
31951
+ }
31952
+ }
31953
+ }
31954
+ for (let r = 0; r < t.rows; r += 1) {
31955
+ for (let c = 0; c < t.columns; c += 1) {
31956
+ if (t.cells[r]?.[c]?.merged && !span.has(`${r}:${c}`)) omit.add(`${r}:${c}`);
31957
+ }
31958
+ }
31959
+ for (const key of span.keys()) omit.delete(key);
31960
+ return { span, omit };
31961
+ }
31962
+
31963
+ // src/renderers/shared/TableCanvasRenderer.ts
31964
+ var PAD_X = 6;
31965
+ var PAD_Y = 2;
31966
+ var MARGIN = 6;
31967
+ var LINE_HEIGHT = 1.2;
31968
+ function paintTable(ctx, t, args, tips) {
31969
+ if (!tableHasContent(t)) return;
31970
+ const layout = layoutTable(ctx, t, args);
31971
+ if (!layout || layout.w <= 0 || layout.h <= 0) return;
31972
+ const fw = t.frameColor && t.frameWidth > 0 ? t.frameWidth : 0;
31973
+ const { x, y } = anchorOrigin(t.position, layout.w + 2 * fw, layout.h + 2 * fw, args);
31974
+ const x0 = x + fw;
31975
+ const y0 = y + fw;
31976
+ if (t.bgColor) {
31977
+ ctx.fillStyle = t.bgColor;
31978
+ ctx.fillRect(x0, y0, layout.w, layout.h);
31979
+ }
31980
+ if (fw > 0 && t.frameColor) {
31981
+ ctx.strokeStyle = t.frameColor;
31982
+ ctx.lineWidth = fw;
31983
+ ctx.strokeRect(x + fw / 2, y + fw / 2, layout.w + fw, layout.h + fw);
31984
+ }
31985
+ const colX = [0];
31986
+ for (const w of layout.colW) colX.push(colX[colX.length - 1] + w);
31987
+ const rowY = [0];
31988
+ for (const h of layout.rowH) rowY.push(rowY[rowY.length - 1] + h);
31989
+ const prevBaseline = ctx.textBaseline;
31990
+ const prevAlign = ctx.textAlign;
31991
+ ctx.textBaseline = "middle";
31992
+ for (const box of layout.boxes) {
31993
+ const rx = x0 + colX[box.c];
31994
+ const ry = y0 + rowY[box.r];
31995
+ const rw = colX[box.c + box.cs] - colX[box.c];
31996
+ const rh = rowY[box.r + box.rs] - rowY[box.r];
31997
+ const cell = box.cell;
31998
+ if (cell.bgColor) {
31999
+ ctx.fillStyle = cell.bgColor;
32000
+ ctx.fillRect(rx, ry, rw, rh);
32001
+ }
32002
+ const text = cell.text ?? "";
32003
+ if (text.length > 0) {
32004
+ const px = fontPxOf(cell.textSize);
32005
+ ctx.font = cellFont(cell, px, args.theme);
32006
+ ctx.fillStyle = cell.textColor ?? args.theme.textColor;
32007
+ const lines = text.split("\n");
32008
+ const blockH = lines.length * px * LINE_HEIGHT;
32009
+ const blockTop = cell.vAlign === "top" ? ry + PAD_Y : cell.vAlign === "bottom" ? ry + rh - PAD_Y - blockH : ry + (rh - blockH) / 2;
32010
+ const tx = cell.hAlign === "left" ? rx + PAD_X : cell.hAlign === "right" ? rx + rw - PAD_X : rx + rw / 2;
32011
+ ctx.textAlign = cell.hAlign;
32012
+ lines.forEach((line, i) => ctx.fillText(line, tx, blockTop + (i + 0.5) * px * LINE_HEIGHT));
32013
+ }
32014
+ if (cell.tooltip) tips.push({ left: rx, top: ry, right: rx + rw, bottom: ry + rh, text: cell.tooltip });
32015
+ }
32016
+ ctx.textBaseline = prevBaseline;
32017
+ ctx.textAlign = prevAlign;
32018
+ if (t.borderColor && t.borderWidth > 0) {
32019
+ ctx.strokeStyle = t.borderColor;
32020
+ ctx.lineWidth = t.borderWidth;
32021
+ const seen = /* @__PURE__ */ new Set();
32022
+ ctx.beginPath();
32023
+ const edge = (ax, ay, bx, by) => {
32024
+ const key = `${ax},${ay},${bx},${by}`;
32025
+ if (seen.has(key)) return;
32026
+ seen.add(key);
32027
+ ctx.moveTo(ax, ay);
32028
+ ctx.lineTo(bx, by);
32029
+ };
32030
+ for (const box of layout.boxes) {
32031
+ const l = Math.round(x0 + colX[box.c]);
32032
+ const r = Math.round(x0 + colX[box.c + box.cs]);
32033
+ const tp = Math.round(y0 + rowY[box.r]);
32034
+ const bt = Math.round(y0 + rowY[box.r + box.rs]);
32035
+ edge(l, tp, r, tp);
32036
+ edge(l, bt, r, bt);
32037
+ edge(l, tp, l, bt);
32038
+ edge(r, tp, r, bt);
32039
+ }
32040
+ ctx.stroke();
32041
+ }
32042
+ }
32043
+ function layoutTable(ctx, t, args) {
32044
+ const { span, omit } = mergeRenderPlan(t);
32045
+ const colW = new Array(t.columns).fill(0);
32046
+ const rowH = new Array(t.rows).fill(0);
32047
+ const boxes = [];
32048
+ for (let r = 0; r < t.rows; r += 1) {
32049
+ for (let c = 0; c < t.columns; c += 1) {
32050
+ if (omit.has(`${r}:${c}`)) continue;
32051
+ const cell = t.cells[r]?.[c];
32052
+ if (cell == null) continue;
32053
+ const sp = span.get(`${r}:${c}`);
32054
+ boxes.push({ cell, r, c, cs: Math.min(sp?.cs ?? 1, t.columns - c), rs: Math.min(sp?.rs ?? 1, t.rows - r) });
32055
+ }
32056
+ }
32057
+ if (boxes.length === 0) return null;
32058
+ const sizeOf = (cell) => {
32059
+ const px = fontPxOf(cell.textSize);
32060
+ ctx.font = cellFont(cell, px, args.theme);
32061
+ const lines = (cell.text ?? "").split("\n");
32062
+ let maxW = 0;
32063
+ for (const line of lines) maxW = Math.max(maxW, ctx.measureText(line).width);
32064
+ let w2 = Math.ceil(maxW) + 2 * PAD_X;
32065
+ let h2 = Math.ceil(lines.length * px * LINE_HEIGHT) + 2 * PAD_Y;
32066
+ if (cell.width) w2 = Math.max(w2, cell.width / 100 * args.plotWidth);
32067
+ if (cell.height) h2 = Math.max(h2, cell.height / 100 * args.paneHeight);
32068
+ return { w: w2, h: h2 };
32069
+ };
32070
+ const spanning = [];
32071
+ for (const box of boxes) {
32072
+ const { w: w2, h: h2 } = sizeOf(box.cell);
32073
+ if (box.cs === 1) colW[box.c] = Math.max(colW[box.c], w2);
32074
+ if (box.rs === 1) rowH[box.r] = Math.max(rowH[box.r], h2);
32075
+ if (box.cs > 1 || box.rs > 1) spanning.push({ box, w: w2, h: h2 });
32076
+ }
32077
+ for (const { box, w: w2, h: h2 } of spanning) {
32078
+ if (box.cs > 1) {
32079
+ let sum = 0;
32080
+ for (let c = box.c; c < box.c + box.cs; c += 1) sum += colW[c];
32081
+ if (w2 > sum) for (let c = box.c; c < box.c + box.cs; c += 1) colW[c] += (w2 - sum) / box.cs;
32082
+ }
32083
+ if (box.rs > 1) {
32084
+ let sum = 0;
32085
+ for (let r = box.r; r < box.r + box.rs; r += 1) sum += rowH[r];
32086
+ if (h2 > sum) for (let r = box.r; r < box.r + box.rs; r += 1) rowH[r] += (h2 - sum) / box.rs;
32087
+ }
32088
+ }
32089
+ let w = 0;
32090
+ for (const cw of colW) w += cw;
32091
+ let h = 0;
32092
+ for (const rh of rowH) h += rh;
32093
+ return { colW, rowH, w, h, boxes };
32094
+ }
32095
+ function anchorOrigin(position, totalW, totalH, args) {
32096
+ let y;
32097
+ if (position.startsWith("top")) y = MARGIN;
32098
+ else if (position.startsWith("bottom")) y = args.paneHeight - MARGIN - totalH;
32099
+ else y = args.paneHeight / 2 - totalH / 2;
32100
+ let x;
32101
+ if (position.endsWith("left")) x = MARGIN;
32102
+ else if (position.endsWith("right")) x = args.plotWidth - MARGIN - totalW;
32103
+ else x = args.plotWidth / 2 - totalW / 2;
32104
+ return { x, y };
32105
+ }
32106
+ function cellFont(cell, px, theme) {
32107
+ const family = cell.fontFamily === "monospace" ? "monospace" : theme.fontFamily || "sans-serif";
32108
+ return `${cell.italic ? "italic " : ""}${cell.bold ? "bold " : ""}${px}px ${family}`;
32109
+ }
32110
+
32111
+ // src/renderers/native/drawings/IndicatorDrawingSlices.ts
32112
+ function indicatorSliceKey(z, boundaries) {
32113
+ return boundaries.find((b) => b > z) ?? Infinity;
32114
+ }
32115
+ var IndicatorDrawingSlices = class {
32116
+ constructor() {
32117
+ this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
32118
+ /** Slice canvas cache, keyed `paneId|beforeZ` — same lifecycle as the user-drawing cache. */
32119
+ this.sliceCache = /* @__PURE__ */ new Map();
32120
+ /** Tooltip hit-rects of every label drawn this frame, in plot coords (rebuilt per prepare). */
32121
+ this.tips = [];
32122
+ }
32123
+ /**
32124
+ * Rebuild the per-indicator drawing slices for this data frame. `ref` is the data
32125
+ * canvas the slices must match pixel-for-pixel (the backend composites them 1:1).
32126
+ * Runs from the renderer's data paint, just before the backend composites the scene.
32127
+ */
32128
+ prepare(scene, coords, theme, ref) {
32129
+ this.tips = [];
32130
+ const out = /* @__PURE__ */ new Map();
32131
+ if (ref.width === 0 || ref.height === 0) {
32132
+ this.sliceCache.clear();
32133
+ return out;
32134
+ }
32135
+ this.drawScene.setDeps({
32136
+ timeToLogical: (ms) => coords.timeToLogical(ms),
32137
+ barAt: (logical) => {
32138
+ const b = scene.bars[Math.round(logical)];
32139
+ return b ? { high: b.high, low: b.low } : null;
32140
+ },
32141
+ theme
32142
+ });
32143
+ const dpr = coords.dpr;
32144
+ const dataW = coords.width;
32145
+ const buckets = /* @__PURE__ */ new Map();
32146
+ const add = (paneId, beforeZ, entry) => {
32147
+ const key = `${paneId}|${beforeZ}`;
32148
+ const bucket = buckets.get(key);
32149
+ if (bucket) bucket.entries.push(entry);
32150
+ else buckets.set(key, { paneId, beforeZ, entries: [entry] });
32151
+ };
32152
+ for (const pane of scene.orderedPanes()) {
32153
+ if (pane.collapsed) continue;
32154
+ const boundaries = scene.seriesBoundaries(pane.id);
32155
+ for (const m of scene.orderedIndicatorsForPane(pane.id)) {
32156
+ const set = modelDrawingSet(m, false);
32157
+ const tables = (m.tables ?? []).filter((t) => !t.overlay);
32158
+ if (drawingSetEmpty(set) && tables.length === 0) continue;
32159
+ const sc = scene.scaleFor(m, pane);
32160
+ const mp = sc === pane.scale ? pane : { ...pane, scale: sc };
32161
+ const beforeZ = indicatorSliceKey(scene.zOf(m.id), boundaries);
32162
+ add(pane.id, beforeZ, { set, tables, pane: mp, indexOffset: scene.offsetOf(m.id) });
32163
+ }
32164
+ if (pane.kind === "price") {
32165
+ for (const m of scene.indicators.values()) {
32166
+ const set = modelDrawingSet(m, true);
32167
+ const tables = (m.tables ?? []).filter((t) => t.overlay === true);
32168
+ if (drawingSetEmpty(set) && tables.length === 0) continue;
32169
+ add(pane.id, Infinity, { set, tables, pane, indexOffset: scene.offsetOf(m.id) });
32170
+ }
32171
+ }
32172
+ }
32173
+ for (const [key, { paneId, beforeZ, entries }] of buckets) {
32174
+ let canvas = this.sliceCache.get(key);
32175
+ if (!canvas) {
32176
+ canvas = document.createElement("canvas");
32177
+ this.sliceCache.set(key, canvas);
32178
+ }
32179
+ if (canvas.width !== ref.width || canvas.height !== ref.height) {
32180
+ canvas.width = ref.width;
32181
+ canvas.height = ref.height;
32182
+ }
32183
+ const ctx = canvas.getContext("2d");
32184
+ if (!ctx) continue;
32185
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
32186
+ ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
32187
+ for (const e of entries) this.paintEntry(ctx, e, coords, dataW, theme);
32188
+ const slices = out.get(paneId) ?? [];
32189
+ slices.push({ beforeZ, canvas });
32190
+ out.set(paneId, slices);
32191
+ }
32192
+ for (const key of [...this.sliceCache.keys()]) if (!buckets.has(key)) this.sliceCache.delete(key);
32193
+ for (const slices of out.values()) slices.sort((a, b) => a.beforeZ - b.beforeZ);
32194
+ return out;
32195
+ }
32196
+ paintEntry(ctx, e, coords, dataW, theme) {
32197
+ const { pane } = e;
32198
+ const paneTips = [];
32199
+ ctx.save();
32200
+ ctx.translate(0, pane.bounds.top);
32201
+ ctx.beginPath();
32202
+ ctx.rect(0, 0, dataW, pane.bounds.height);
32203
+ ctx.clip();
32204
+ this.drawScene.setSet(e.set, e.indexOffset);
32205
+ this.drawScene.render(
32206
+ ctx,
32207
+ dataW,
32208
+ pane.bounds.height,
32209
+ (l) => coords.logicalToX(l),
32210
+ (price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
32211
+ );
32212
+ paneTips.push(...this.drawScene.labelTipRegions());
32213
+ for (const t of e.tables) paintTable(ctx, t, { paneHeight: pane.bounds.height, plotWidth: dataW, theme }, paneTips);
32214
+ ctx.restore();
32215
+ for (const r of paneTips) {
32216
+ this.tips.push({ ...r, top: r.top + pane.bounds.top, bottom: r.bottom + pane.bounds.top });
32217
+ }
32218
+ }
32219
+ /** Tooltip of the topmost label or table cell under a plot-space point, or null. Fed by the last prepare. */
32220
+ labelTooltipAt(x, y) {
32221
+ for (let i = this.tips.length - 1; i >= 0; i -= 1) {
32222
+ const r = this.tips[i];
32223
+ if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return r.text;
32224
+ }
32225
+ return null;
32226
+ }
32227
+ };
32228
+ function mergeSlices(indicator, user) {
32229
+ const out = /* @__PURE__ */ new Map();
32230
+ for (const [paneId, slices] of indicator) out.set(paneId, [...slices]);
32231
+ for (const [paneId, slices] of user) out.set(paneId, [...out.get(paneId) ?? [], ...slices]);
32232
+ for (const slices of out.values()) slices.sort((a, b) => a.beforeZ - b.beforeZ);
32233
+ return out;
32234
+ }
32235
+
31767
32236
  // src/renderers/native/drawings/Projector.ts
31768
32237
  function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
31769
32238
  return {
@@ -32509,6 +32978,11 @@ var NativeRenderer = class {
32509
32978
  this.vpvrRenderer = new VpvrRenderer();
32510
32979
  this.resizeObserver = null;
32511
32980
  this.dprMedia = null;
32981
+ /** Plot size in INTEGER device px, as last reported by the resize observer's
32982
+ * device-pixel-content-box — the browser's own statement of how many device pixels
32983
+ * it paints the plot into. `null` until the first report or where the box type is
32984
+ * unsupported (WebKit); syncSize then falls back to rounding the client rect. */
32985
+ this.plotDeviceSize = null;
32512
32986
  this.coords = new CoordinateSystem();
32513
32987
  this.scene = new SceneGraph();
32514
32988
  // chosen at mount (WebGL2 if available, else canvas2d)
@@ -32516,6 +32990,8 @@ var NativeRenderer = class {
32516
32990
  this.glowAmount = 0;
32517
32991
  // WebGL2 neon-glow intensity (canvas2d ignores it)
32518
32992
  this.chrome = new ChromeRenderer();
32993
+ /** Prepaints each indicator's Pine drawings into interleave slices at the model's z. */
32994
+ this.indicatorSlices = new IndicatorDrawingSlices();
32519
32995
  /** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
32520
32996
  this.labelTooltip = null;
32521
32997
  this.crosshairLayer = new CrosshairRenderer();
@@ -32664,7 +33140,6 @@ var NativeRenderer = class {
32664
33140
  this.toggleVisibleCbs = /* @__PURE__ */ new Set();
32665
33141
  this.moveIndicatorCbs = /* @__PURE__ */ new Set();
32666
33142
  this.priceStyleCbs = /* @__PURE__ */ new Set();
32667
- this.tableOverlays = /* @__PURE__ */ new Map();
32668
33143
  this.name = "native";
32669
33144
  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
33145
  /** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
@@ -32978,7 +33453,6 @@ var NativeRenderer = class {
32978
33453
  * hidden) on clear so a re-show picks up the current theme.
32979
33454
  */
32980
33455
  setLoading(loading) {
32981
- for (const overlay of this.tableOverlays.values()) overlay.setVisible(!loading);
32982
33456
  if (!loading || !this.wrapper) {
32983
33457
  this.loadingEl?.remove();
32984
33458
  this.loadingEl = null;
@@ -33617,13 +34091,13 @@ var NativeRenderer = class {
33617
34091
  resetView: () => this.resetView(),
33618
34092
  // User drawings claim a gesture before pan when armed / over a drawing.
33619
34093
  drawingsClaim: (x, y) => this.userDrawings?.claim(x, y) ?? false,
33620
- drawingsMeasureStart: (x, y) => this.userDrawings?.beginMeasureAt(x, y) ?? false,
34094
+ drawingsMeasureStart: (x, y, snap) => this.userDrawings?.beginMeasureAt(x, y, snap) ?? false,
33621
34095
  drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y) ?? false,
33622
34096
  drawingsCancelPlacement: () => this.userDrawings?.cancelPlacement() ?? false,
33623
34097
  drawingsSnapMode: () => this.snapMode,
33624
34098
  drawingsPointerDown: (x, y, snap, shift) => this.userDrawings?.pointerDown(x, y, snap, shift),
33625
34099
  drawingsPointerMove: (x, y, snap, shift) => this.userDrawings?.pointerMove(x, y, snap, shift),
33626
- drawingsPointerUp: (x, y) => this.userDrawings?.pointerUp(x, y),
34100
+ drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
33627
34101
  drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
33628
34102
  drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
33629
34103
  drawingsClearTransient: () => this.userDrawings?.clearTransient()
@@ -33640,7 +34114,7 @@ var NativeRenderer = class {
33640
34114
  this.plot.addEventListener("pointerleave", this.onScrollProximityLeave);
33641
34115
  this.labelTooltip = new LabelTooltip(this.plot, {
33642
34116
  theme: () => this.chromeTheme(),
33643
- lookup: (x, y) => this.chrome.labelTooltipAt(x, y)
34117
+ lookup: (x, y) => this.indicatorSlices.labelTooltipAt(x, y)
33644
34118
  });
33645
34119
  this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
33646
34120
  projector: () => this.drawingProjector(),
@@ -33725,6 +34199,7 @@ var NativeRenderer = class {
33725
34199
  this.emitPaneAction({ type: "maximize", paneId, maximized });
33726
34200
  }
33727
34201
  });
34202
+ this.paneControls.setSuspended(this.layoutMode === "mobile");
33728
34203
  this.axisScaleButtons = new AxisScaleButtons(this.plot, theme, {
33729
34204
  panes: () => this.axisScaleViews(),
33730
34205
  rightAxis: () => this.rightAxisW,
@@ -33734,8 +34209,19 @@ var NativeRenderer = class {
33734
34209
  if (pane) this.setPaneLog(paneId, !paneLogScale(this.scene, pane));
33735
34210
  }
33736
34211
  });
33737
- this.resizeObserver = new ResizeObserver(() => this.resize());
34212
+ this.resizeObserver = new ResizeObserver((entries) => {
34213
+ for (const e of entries) {
34214
+ if (e.target !== this.plot) continue;
34215
+ const s = e.devicePixelContentBoxSize?.[0];
34216
+ if (s) this.plotDeviceSize = { width: s.inlineSize, height: s.blockSize };
34217
+ }
34218
+ this.resize();
34219
+ });
33738
34220
  this.resizeObserver.observe(this.wrapper);
34221
+ try {
34222
+ this.resizeObserver.observe(this.plot, { box: "device-pixel-content-box" });
34223
+ } catch {
34224
+ }
33739
34225
  this.watchDpr();
33740
34226
  this.syncSize();
33741
34227
  }
@@ -33860,8 +34346,6 @@ var NativeRenderer = class {
33860
34346
  this.inputsUI?.destroy();
33861
34347
  this.paneControls?.destroy();
33862
34348
  this.axisScaleButtons?.destroy();
33863
- for (const overlay of this.tableOverlays.values()) overlay.destroy();
33864
- this.tableOverlays.clear();
33865
34349
  this.resizeObserver?.disconnect();
33866
34350
  this.resizeObserver = null;
33867
34351
  this.dprMedia?.removeEventListener("change", this.onDprChange);
@@ -33890,6 +34374,7 @@ var NativeRenderer = class {
33890
34374
  this.attributionEl = null;
33891
34375
  this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
33892
34376
  this.mountContainer?.style.removeProperty("--vela-scale-gutter");
34377
+ this.mountContainer?.style.removeProperty("--vela-bottom-gutter");
33893
34378
  this.mountContainer?.style.removeProperty("--vela-price-pane-top");
33894
34379
  this.mountContainer?.style.removeProperty("--vela-price-pane-bottom");
33895
34380
  this.mountContainer = null;
@@ -33927,7 +34412,8 @@ var NativeRenderer = class {
33927
34412
  }
33928
34413
  const skipFit = opts?.preserveView === true && this.didInitialFit;
33929
34414
  if (this.coords.width > 0 && !skipFit) {
33930
- this.fitContent();
34415
+ if (this.didInitialFit) this.reframeKeepZoom();
34416
+ else this.fitContent();
33931
34417
  this.didInitialFit = true;
33932
34418
  }
33933
34419
  if (!this.introPlayed && this.bars.length > 0) {
@@ -33982,7 +34468,6 @@ var NativeRenderer = class {
33982
34468
  ensurePane(pane) {
33983
34469
  this.scene.ensurePane(pane.id, pane.kind, pane.order, pane.heightWeight ?? (pane.kind === "price" ? 3 : 1));
33984
34470
  this.layoutPanes();
33985
- this.repositionTables();
33986
34471
  this.paneControls?.refresh();
33987
34472
  this.scheduler.invalidate(4 /* Full */);
33988
34473
  }
@@ -34004,17 +34489,14 @@ var NativeRenderer = class {
34004
34489
  if (!model.ownScale) this.scene.dropIndicatorScale(handle.id);
34005
34490
  this.inputsUI.setPane(handle.id, paneId);
34006
34491
  this.refreshAnchorOffset(model);
34007
- this.syncTables(model);
34008
34492
  this.refreshAxisWidth();
34009
34493
  this.layoutPanes();
34010
- this.repositionTables();
34011
34494
  this.paneControls?.refresh();
34012
34495
  this.scheduler.invalidate(4 /* Full */);
34013
34496
  }
34014
34497
  orderPanes(orderedIds) {
34015
34498
  this.scene.orderPanes(orderedIds);
34016
34499
  this.layoutPanes();
34017
- this.repositionTables();
34018
34500
  this.paneControls?.refresh();
34019
34501
  this.scheduler.invalidate(4 /* Full */);
34020
34502
  }
@@ -34023,7 +34505,6 @@ var NativeRenderer = class {
34023
34505
  if (!pane || pane.collapsed === collapsed) return;
34024
34506
  pane.collapsed = collapsed;
34025
34507
  this.layoutPanes();
34026
- this.repositionTables();
34027
34508
  this.paneControls?.refresh();
34028
34509
  this.scheduler.invalidate(4 /* Full */);
34029
34510
  }
@@ -34031,7 +34512,6 @@ var NativeRenderer = class {
34031
34512
  if (paneId !== null && !this.scene.panes.has(paneId)) paneId = null;
34032
34513
  this.maximizedPaneId = paneId;
34033
34514
  this.layoutPanes();
34034
- this.repositionTables();
34035
34515
  this.paneControls?.refresh();
34036
34516
  this.scheduler.invalidate(4 /* Full */);
34037
34517
  }
@@ -34118,7 +34598,6 @@ var NativeRenderer = class {
34118
34598
  native: !!model.native,
34119
34599
  ...model.props ? { props: model.props, propValues: model.propValues ?? {} } : {}
34120
34600
  });
34121
- this.syncTables(model);
34122
34601
  if (model.native?.type === "volume") {
34123
34602
  this.volumeActive = true;
34124
34603
  this.volumeHidden = false;
@@ -34142,7 +34621,6 @@ var NativeRenderer = class {
34142
34621
  }
34143
34622
  }
34144
34623
  applyPatch(model, patch);
34145
- this.syncTables(model);
34146
34624
  this.scheduler.invalidate(3 /* Light */);
34147
34625
  }
34148
34626
  removeIndicator(handle) {
@@ -34161,8 +34639,6 @@ var NativeRenderer = class {
34161
34639
  this.scene.forgetAnchorOffset(handle.id);
34162
34640
  this.scene.dropIndicatorScale(handle.id);
34163
34641
  this.inputsUI.remove(handle.id);
34164
- this.tableOverlays.get(handle.id)?.destroy();
34165
- this.tableOverlays.delete(handle.id);
34166
34642
  this.refreshAxisWidth();
34167
34643
  this.paneControls?.refresh();
34168
34644
  this.scheduler.invalidate(4 /* Full */);
@@ -34206,8 +34682,6 @@ var NativeRenderer = class {
34206
34682
  }
34207
34683
  if (!visible) {
34208
34684
  this.scene.indicators.delete(handle.id);
34209
- this.tableOverlays.get(handle.id)?.destroy();
34210
- this.tableOverlays.delete(handle.id);
34211
34685
  }
34212
34686
  this.inputsUI.setVisible(handle.id, visible);
34213
34687
  this.scheduler.invalidate(4 /* Full */);
@@ -34253,6 +34727,7 @@ var NativeRenderer = class {
34253
34727
  this.userDrawings?.setLayoutMode(mode);
34254
34728
  this.settingsDialog?.setLayoutMode(mode);
34255
34729
  this.inputsUI?.setLayoutMode(mode);
34730
+ this.paneControls?.setSuspended(mode === "mobile");
34256
34731
  if (this.scrollButton) {
34257
34732
  const px = mode === "mobile" ? SCROLL_BTN_SIZE_TOUCH : SCROLL_BTN_SIZE;
34258
34733
  this.scrollButton.style.width = `${px}px`;
@@ -34586,6 +35061,7 @@ var NativeRenderer = class {
34586
35061
  this.scaleDragHeight = res.height;
34587
35062
  this.scaleDragStart = { ...res.holder.scale };
34588
35063
  res.holder.manualScale = { ...res.holder.scale };
35064
+ this.axisScaleButtons?.reposition();
34589
35065
  this.scheduler.invalidate(4 /* Full */);
34590
35066
  }
34591
35067
  /** Rescale the grabbed scale around its center by the total drag (down ⇒ zoom out). */
@@ -34617,6 +35093,7 @@ var NativeRenderer = class {
34617
35093
  const res = this.resolveScaleHolder(x, y);
34618
35094
  if (!res) return;
34619
35095
  res.holder.manualScale = null;
35096
+ this.axisScaleButtons?.reposition();
34620
35097
  this.scheduler.invalidate(4 /* Full */);
34621
35098
  }
34622
35099
  /**
@@ -34648,7 +35125,6 @@ var NativeRenderer = class {
34648
35125
  /** Relayout + repaint + refresh the hover buttons after a collapse/maximize/order change. */
34649
35126
  afterPaneLayoutChange() {
34650
35127
  this.layoutPanes();
34651
- this.repositionTables();
34652
35128
  this.paneControls?.refresh();
34653
35129
  this.scheduler.invalidate(4 /* Full */);
34654
35130
  }
@@ -34729,7 +35205,6 @@ var NativeRenderer = class {
34729
35205
  above.heightWeight = next.above;
34730
35206
  below.heightWeight = next.below;
34731
35207
  this.layoutPanes();
34732
- this.repositionTables();
34733
35208
  this.scheduler.invalidate(4 /* Full */);
34734
35209
  }
34735
35210
  /** Double-click a separator → split the two adjacent panes evenly (each gets half of
@@ -34744,7 +35219,6 @@ var NativeRenderer = class {
34744
35219
  above.heightWeight = half;
34745
35220
  below.heightWeight = half;
34746
35221
  this.layoutPanes();
34747
- this.repositionTables();
34748
35222
  this.scheduler.invalidate(4 /* Full */);
34749
35223
  }
34750
35224
  // ── keyboard navigation / accessibility (item 11) ──
@@ -35023,7 +35497,10 @@ var NativeRenderer = class {
35023
35497
  const liveActual = li >= 0 ? this.bars[li] : void 0;
35024
35498
  const easeLive = !!liveActual && this.liveEaseTime === liveActual.time && (liveActual.high !== this.liveEaseHigh || liveActual.low !== this.liveEaseLow || liveActual.close !== this.liveEaseClose);
35025
35499
  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();
35500
+ this.scene.drawingSlices = mergeSlices(
35501
+ this.indicatorSlices.prepare(this.scene, this.coords, this.theme, this.dataCanvas),
35502
+ this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map()
35503
+ );
35027
35504
  this.backdropRenderer.render(this.scene, this.coords, this.theme, gridAlpha);
35028
35505
  this.backend.render(this.scene, this.coords, this.theme);
35029
35506
  this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
@@ -35254,6 +35731,20 @@ var NativeRenderer = class {
35254
35731
  this.coords.setViewport(v);
35255
35732
  this.targetBarSpacing = v.barSpacing;
35256
35733
  }
35734
+ /** Re-frame after a series replacement (a symbol/timeframe switch): keep the user's
35735
+ * zoom (bar spacing), re-anchor the newest bars at the default right offset.
35736
+ * `clampViewport`'s fit-all-bars floor deliberately does NOT apply — a progressive
35737
+ * head may still be backfilling toward the previous depth, and raising the spacing
35738
+ * to its temporary bar count would lose the zoom this exists to keep. */
35739
+ reframeKeepZoom() {
35740
+ this.animator?.stop();
35741
+ this.panVelocity = 0;
35742
+ for (const pane of this.scene.panes.values()) pane.manualScale = null;
35743
+ for (const sl of this.scene.indicatorScales.values()) sl.manualScale = null;
35744
+ const v = { barSpacing: clampBarSpacing(this.coords.getViewport().barSpacing), rightOffset: defaultViewport().rightOffset };
35745
+ this.coords.setViewport(v);
35746
+ this.targetBarSpacing = v.barSpacing;
35747
+ }
35257
35748
  paneBoundsFor(paneId) {
35258
35749
  const p = this.scene.panes.get(paneId);
35259
35750
  return { top: p?.bounds.top ?? 0, height: p?.bounds.height ?? 0, rightAxis: this.rightAxisW };
@@ -35461,27 +35952,6 @@ var NativeRenderer = class {
35461
35952
  }
35462
35953
  return maxVol;
35463
35954
  }
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
35955
  layoutPanes() {
35486
35956
  const panes = this.scene.orderedPanes();
35487
35957
  const dataHeight = this.coords.height;
@@ -35532,6 +36002,7 @@ var NativeRenderer = class {
35532
36002
  const visible = maxPane ? [maxPane] : this.scene.orderedPanes().filter((p) => !p.collapsed);
35533
36003
  const paneBottom = visible.length ? Math.max(...visible.map((p) => p.bounds.top + p.bounds.height)) : dataHeight;
35534
36004
  this.scrollBtnBottomPx = SCROLL_BTN_BOTTOM + Math.max(0, dataHeight - paneBottom);
36005
+ this.mountContainer?.style.setProperty("--vela-bottom-gutter", `${TIME_AXIS_H + Math.max(0, dataHeight - paneBottom)}px`);
35535
36006
  this.scrollBtnRightPx = this.rightAxisW + SCROLL_BTN_RIGHT_INSET;
35536
36007
  if (this.scrollButton) {
35537
36008
  this.scrollButton.style.bottom = `${this.scrollBtnBottomPx}px`;
@@ -35624,30 +36095,33 @@ var NativeRenderer = class {
35624
36095
  if (w <= 0 || h <= 0) return;
35625
36096
  const dpr = window.devicePixelRatio || 1;
35626
36097
  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;
36098
+ const rect = this.plot.getBoundingClientRect();
36099
+ let bw = Math.max(1, Math.round(rect.width * dpr));
36100
+ let bh = Math.max(1, Math.round(rect.height * dpr));
36101
+ const dev = this.plotDeviceSize;
36102
+ if (dev && Math.abs(dev.width - rect.width * dpr) <= 1 && Math.abs(dev.height - rect.height * dpr) <= 1) {
36103
+ bw = Math.max(1, dev.width);
36104
+ bh = Math.max(1, dev.height);
36105
+ }
36106
+ const pw = bw / dpr;
36107
+ const ph = bh / dpr;
36108
+ const size = (canvas) => {
36109
+ canvas.width = bw;
36110
+ canvas.height = bh;
36111
+ canvas.style.width = `${pw}px`;
36112
+ canvas.style.height = `${ph}px`;
36113
+ };
36114
+ size(this.dataCanvas);
36115
+ size(this.backdropCanvas);
36116
+ size(this.volumeCanvas);
36117
+ for (const l of this.extLayers) size(l.canvas);
36118
+ size(this.vpvrCanvas);
36119
+ size(this.chromeCanvas);
36120
+ size(this.drawingsCanvas);
36121
+ size(this.cursorCanvas);
35647
36122
  this.coords.setSize(Math.max(1, pw - this.rightAxisW), Math.max(1, ph - TIME_AXIS_H), dpr);
35648
36123
  this.scene.crosshair = null;
35649
36124
  this.layoutPanes();
35650
- this.repositionTables();
35651
36125
  this.userDrawings?.onResize();
35652
36126
  if (!this.didInitialFit && this.coords.barCount > 0) {
35653
36127
  this.fitContent();
@@ -36966,9 +37440,182 @@ var Watermark = class {
36966
37440
  }
36967
37441
  };
36968
37442
 
37443
+ // src/widget/cell-controls.ts
37444
+ var CELL_CONTROLS_PROXIMITY_PX = 120;
37445
+ var TIME_AXIS_H2 = 22;
37446
+ var CONTROLS_BOTTOM_PX = TIME_AXIS_H2 + 12;
37447
+ var CLUSTER_H2 = 24;
37448
+ var CLUSTER_PILL2 = "rgba(0,0,0,0.65)";
37449
+ var CLUSTER_LEFT_CSS = "calc((100% + var(--vela-toolbar-gutter, 0px) - var(--vela-scale-gutter, 0px)) / 2)";
37450
+ var STYLE_ID26 = "vela-cell-controls";
37451
+ var CSS23 = `
37452
+ .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;}
37453
+ .vela-cc-btn svg{display:block;}
37454
+ .vela-cc-btn:hover{background:var(--vela-active);color:var(--vela-fg-bright);}
37455
+ .vela-cc-on,.vela-cc-on:hover{background:var(--vela-selected-bg);color:var(--vela-selected-fg);}
37456
+ .vela-cc-grip{cursor:grab;touch-action:none;}
37457
+ .vela-cc-grip:active{cursor:grabbing;}
37458
+ `;
37459
+ function plotCenterX(width, toolbarGutter = 0, scaleGutter = 0) {
37460
+ return (width + toolbarGutter - scaleGutter) / 2;
37461
+ }
37462
+ function nearBottomCenter(x, y, width, height, proximityPx = CELL_CONTROLS_PROXIMITY_PX, gutters = {}) {
37463
+ const cx = plotCenterX(width, gutters.toolbar ?? 0, gutters.scale ?? 0);
37464
+ const cy = height - CONTROLS_BOTTOM_PX - CLUSTER_H2 / 2;
37465
+ return Math.hypot(x - cx, y - cy) <= proximityPx;
37466
+ }
37467
+ var CellControls = class {
37468
+ constructor(host, deps) {
37469
+ this.host = host;
37470
+ this.deps = deps;
37471
+ this.near = false;
37472
+ /** A grip drag is underway — the proximity reveal must not hide the cluster
37473
+ * while captured pointer moves sweep across the whole grid. */
37474
+ this.dragging = false;
37475
+ /** Mobile: the proximity reveal is meaningless without a cursor — the mobile
37476
+ * bar's maximize stop replaces the cluster. */
37477
+ this.suspended = false;
37478
+ this.onHostMove = (e) => {
37479
+ if (this.suspended) return;
37480
+ if (this.dragging) return;
37481
+ const rect = this.host.getBoundingClientRect();
37482
+ this.setNear(nearBottomCenter(e.clientX - rect.left, e.clientY - rect.top, rect.width, rect.height, CELL_CONTROLS_PROXIMITY_PX, this.hostGutters()));
37483
+ };
37484
+ this.onHostLeave = () => {
37485
+ if (this.dragging) return;
37486
+ this.setNear(false);
37487
+ };
37488
+ injectStyles(STYLE_ID26, CSS23, host.ownerDocument);
37489
+ this.glider = new Glider(deps.chart);
37490
+ this.root = host.ownerDocument.createElement("div");
37491
+ Object.assign(this.root.style, {
37492
+ position: "absolute",
37493
+ left: CLUSTER_LEFT_CSS,
37494
+ bottom: `${CONTROLS_BOTTOM_PX}px`,
37495
+ transform: "translateX(-50%)",
37496
+ zIndex: "6",
37497
+ display: "none",
37498
+ // revealed by cursor proximity (onHostMove)
37499
+ gap: "2px",
37500
+ padding: "2px",
37501
+ borderRadius: "var(--vela-radius-md)",
37502
+ background: CLUSTER_PILL2,
37503
+ pointerEvents: "auto"
37504
+ });
37505
+ this.host.addEventListener("pointermove", this.onHostMove);
37506
+ this.host.addEventListener("pointerleave", this.onHostLeave);
37507
+ this.host.appendChild(this.root);
37508
+ this.refresh();
37509
+ }
37510
+ /** Rebuild the buttons (the multi-cell gate or the maximized state changed). */
37511
+ refresh() {
37512
+ this.root.textContent = "";
37513
+ const multi = this.deps.multiCell();
37514
+ const maximized = multi && this.deps.isMaximized();
37515
+ if (multi && !maximized) this.root.appendChild(this.makeGrip());
37516
+ this.root.appendChild(this.button("minus", "Zoom out", () => this.glider.zoom(ZOOM_OUT)));
37517
+ this.root.appendChild(this.button("plus", "Zoom in", () => this.glider.zoom(ZOOM_IN)));
37518
+ if (multi) {
37519
+ this.root.appendChild(
37520
+ this.button(maximized ? "restore" : "maximize", maximized ? "Restore layout" : "Maximize chart", () => this.deps.toggleMaximize(), {
37521
+ // The maximized state reads as an inverse chip (white-on-dark, dark-on-light),
37522
+ // the same active-state affordance as a collapsed pane's expand button.
37523
+ selected: maximized
37524
+ })
37525
+ );
37526
+ }
37527
+ this.root.appendChild(
37528
+ this.button("reset", "Reset chart", () => {
37529
+ this.glider.stop();
37530
+ this.deps.reset();
37531
+ })
37532
+ );
37533
+ }
37534
+ button(iconId, title, onClick, opts = {}) {
37535
+ const b = this.host.ownerDocument.createElement("button");
37536
+ b.type = "button";
37537
+ b.title = title;
37538
+ b.setAttribute("aria-label", title);
37539
+ b.className = opts.selected === true ? "vela-cc-btn vela-cc-on" : "vela-cc-btn";
37540
+ b.innerHTML = icon(iconId);
37541
+ b.addEventListener("click", (e) => {
37542
+ e.stopPropagation();
37543
+ onClick();
37544
+ });
37545
+ return b;
37546
+ }
37547
+ /** The drag handle (2×3 dot grip): press and drag onto another cell to trade
37548
+ * slots with it. The preview highlight follows the pointer; releasing outside
37549
+ * any other cell cancels. */
37550
+ makeGrip() {
37551
+ const b = this.host.ownerDocument.createElement("button");
37552
+ b.type = "button";
37553
+ b.title = "Drag to move chart";
37554
+ b.setAttribute("aria-label", "Drag to move chart");
37555
+ b.className = "vela-cc-btn vela-cc-grip";
37556
+ b.innerHTML = icon("grip");
37557
+ b.addEventListener("pointerdown", (e) => this.onGripDown(b, e));
37558
+ return b;
37559
+ }
37560
+ onGripDown(btn2, e) {
37561
+ if (e.button !== 0 && e.pointerType === "mouse") return;
37562
+ e.preventDefault();
37563
+ e.stopPropagation();
37564
+ try {
37565
+ btn2.setPointerCapture(e.pointerId);
37566
+ } catch {
37567
+ }
37568
+ this.dragging = true;
37569
+ let target = null;
37570
+ const move = (ev) => {
37571
+ target = this.deps.dragTargetAt(ev.clientX, ev.clientY);
37572
+ this.deps.previewDrop(target);
37573
+ };
37574
+ const finish = (commit) => () => {
37575
+ this.dragging = false;
37576
+ this.deps.previewDrop(null);
37577
+ btn2.removeEventListener("pointermove", move);
37578
+ btn2.removeEventListener("pointerup", onUp);
37579
+ btn2.removeEventListener("pointercancel", onCancel);
37580
+ if (commit && target != null) this.deps.dropOn(target);
37581
+ };
37582
+ const onUp = finish(true);
37583
+ const onCancel = finish(false);
37584
+ btn2.addEventListener("pointermove", move);
37585
+ btn2.addEventListener("pointerup", onUp);
37586
+ btn2.addEventListener("pointercancel", onCancel);
37587
+ }
37588
+ /** Mobile flips the cluster off entirely (and hides it if currently revealed). */
37589
+ setSuspended(on) {
37590
+ this.suspended = on;
37591
+ if (on) this.setNear(false);
37592
+ }
37593
+ /** Live renderer gutters on the cell host (0 when unpublished — a test stub). */
37594
+ hostGutters() {
37595
+ const view = this.host.ownerDocument.defaultView;
37596
+ if (!view) return { toolbar: 0, scale: 0 };
37597
+ const cs = view.getComputedStyle(this.host);
37598
+ return {
37599
+ toolbar: Number.parseFloat(cs.getPropertyValue("--vela-toolbar-gutter")) || 0,
37600
+ scale: Number.parseFloat(cs.getPropertyValue("--vela-scale-gutter")) || 0
37601
+ };
37602
+ }
37603
+ setNear(near) {
37604
+ if (near === this.near) return;
37605
+ this.near = near;
37606
+ this.root.style.display = near ? "flex" : "none";
37607
+ }
37608
+ destroy() {
37609
+ this.glider.stop();
37610
+ this.host.removeEventListener("pointermove", this.onHostMove);
37611
+ this.host.removeEventListener("pointerleave", this.onHostLeave);
37612
+ this.root.remove();
37613
+ }
37614
+ };
37615
+
36969
37616
  // src/widget/context-menu.ts
36970
37617
  var PRICE_AXIS_W = 60;
36971
- var TIME_AXIS_H2 = 26;
37618
+ var TIME_AXIS_H3 = 26;
36972
37619
  var ChartContextMenu = class {
36973
37620
  constructor(host, cbs) {
36974
37621
  this.cbs = cbs;
@@ -37007,7 +37654,7 @@ var ChartContextMenu = class {
37007
37654
  zoneOf(e) {
37008
37655
  const rect = this.host.getBoundingClientRect();
37009
37656
  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";
37657
+ if (e.clientY - rect.top > rect.height - TIME_AXIS_H3) return "time-axis";
37011
37658
  return "body";
37012
37659
  }
37013
37660
  /** The pane under the pointer, so every pane's price scale has its own menu. */
@@ -37193,6 +37840,12 @@ function cellDrawings(opt) {
37193
37840
  if (opt === true || opt == null) return { toolbar: false };
37194
37841
  return { ...opt, toolbar: false };
37195
37842
  }
37843
+ function instanceDeltas(handle) {
37844
+ if (!handle) return void 0;
37845
+ const inputs = inputDeltas(handle.inputs, handle.inputValues());
37846
+ const props = inputDeltas(handle.props, handle.propValues());
37847
+ return inputs || props ? { ...inputs ? { inputs } : {}, ...props ? { props } : {} } : void 0;
37848
+ }
37196
37849
  var ChartCell = class {
37197
37850
  constructor(id, gridHost, seed, deps) {
37198
37851
  this.id = id;
@@ -37341,11 +37994,18 @@ var ChartCell = class {
37341
37994
  if (this.inner && this.state.symbol) this.marketStatus?.track(this.inner.data, this.state.symbol);
37342
37995
  });
37343
37996
  this.syncStatuslineColors();
37997
+ this.cellControls = new CellControls(this.host, {
37998
+ chart: () => this.inner,
37999
+ reset: () => this.resetView(),
38000
+ multiCell: () => deps.multiCell(),
38001
+ isMaximized: () => deps.isMaximized(id),
38002
+ toggleMaximize: () => deps.toggleMaximize(id),
38003
+ dragTargetAt: (x, y) => deps.cellDragTarget(id, x, y),
38004
+ previewDrop: (target) => deps.previewDropTarget(target),
38005
+ dropOn: (target) => deps.dropCell(id, target)
38006
+ });
37344
38007
  this.contextMenu = new ChartContextMenu(this.host, {
37345
- resetView: () => {
37346
- this.inner?.renderer.set("autoScale", true);
37347
- this.inner?.setVisibleRangePreset("ALL");
37348
- },
38008
+ resetView: () => this.resetView(),
37349
38009
  timezone: () => this.deps.timezone(),
37350
38010
  setTimezone: (zone) => this.deps.setTimezone(zone),
37351
38011
  // Right-clicking activates the cell first (capture-phase pointerdown), so the
@@ -37361,6 +38021,7 @@ var ChartCell = class {
37361
38021
  this.syncPresentNatives();
37362
38022
  this.refreshNativeCatalog();
37363
38023
  });
38024
+ this.inner.on("indicator:inputs", () => this.deps.onStateDirty());
37364
38025
  this.inner.on("indicator:removed", ({ id: id2 }) => {
37365
38026
  if (this.destroyed) return;
37366
38027
  const idx = this.instances.findIndex((it) => it.handle?.id === id2);
@@ -37369,7 +38030,7 @@ var ChartCell = class {
37369
38030
  this.instances.splice(idx, 1);
37370
38031
  this.history.push({
37371
38032
  undo: () => {
37372
- snapshot.handle = this.addToChart(snapshot.entry);
38033
+ snapshot.handle = this.addToChart(snapshot.entry, snapshot.values);
37373
38034
  this.instances.push(snapshot);
37374
38035
  this.deps.onIndicatorsChanged(this.id);
37375
38036
  },
@@ -37719,6 +38380,20 @@ var ChartCell = class {
37719
38380
  this.inner.setVisibleRangePreset(preset.preset);
37720
38381
  }
37721
38382
  }
38383
+ /** Reset this cell's view: re-enable auto scale and frame the full history —
38384
+ * the same action the chart context menu offers. */
38385
+ resetView() {
38386
+ this.inner?.renderer.set("autoScale", true);
38387
+ this.inner?.setVisibleRangePreset("ALL");
38388
+ }
38389
+ /** Rebuild the view-controls cluster (the maximize gate or state changed). */
38390
+ refreshControls() {
38391
+ this.cellControls.refresh();
38392
+ }
38393
+ /** Mobile flips the per-cell cluster off (the shell's mobile bar replaces it). */
38394
+ setControlsSuspended(on) {
38395
+ this.cellControls.setSuspended(on);
38396
+ }
37722
38397
  /** Make this cell the active one and put keyboard focus on its chart surface. */
37723
38398
  focus() {
37724
38399
  this.deps.activate(this.id);
@@ -37744,9 +38419,9 @@ var ChartCell = class {
37744
38419
  this.manifest = list;
37745
38420
  if (this.pendingManifestNames) {
37746
38421
  if (list.length === 0) return;
37747
- for (const name of this.pendingManifestNames) {
37748
- const entry = list.find((e) => e.name === name);
37749
- if (entry) this.addManifestInstance(entry, { record: false });
38422
+ for (const led of this.pendingManifestNames) {
38423
+ const entry = list.find((e) => e.name === ledgerEntryName(led));
38424
+ if (entry) this.addManifestInstance(entry, { record: false, ...typeof led === "object" ? { inputs: led.inputs, props: led.props } : {} });
37750
38425
  }
37751
38426
  this.pendingManifestNames = null;
37752
38427
  return;
@@ -37776,9 +38451,9 @@ var ChartCell = class {
37776
38451
  }
37777
38452
  for (const it of [...this.instances]) this.dropInstance(it);
37778
38453
  if (this.manifest.length > 0) {
37779
- for (const name of led.manifest) {
37780
- const entry = this.manifest.find((e) => e.name === name);
37781
- if (entry) this.addManifestInstance(entry, { record: false });
38454
+ for (const item of led.manifest) {
38455
+ const entry = this.manifest.find((e) => e.name === ledgerEntryName(item));
38456
+ if (entry) this.addManifestInstance(entry, { record: false, ...typeof item === "object" ? { inputs: item.inputs, props: item.props } : {} });
37782
38457
  }
37783
38458
  this.pendingManifestNames = null;
37784
38459
  } else if (!this.deps.manifestSettled()) {
@@ -37827,12 +38502,13 @@ var ChartCell = class {
37827
38502
  * a persistence handler's `restore` runs silently, a user-driven call records.
37828
38503
  */
37829
38504
  addExternalIndicator(entry) {
37830
- this.addManifestInstance({ ...entry, enabled: true }, { external: true });
38505
+ this.addManifestInstance({ ...entry, enabled: true }, { external: true, ...entry.inputs ? { inputs: entry.inputs } : {}, ...entry.props ? { props: entry.props } : {} });
37831
38506
  }
37832
38507
  /** Add ONE instance of a manifest entry (repeatable — duplicates are legitimate). */
37833
38508
  addManifestInstance(entry, opts = {}) {
37834
38509
  if (this.destroyed) return;
37835
- const it = { entry, handle: this.addToChart(entry), ...opts.external ? { external: true } : {} };
38510
+ const values = opts.inputs || opts.props ? { inputs: opts.inputs, props: opts.props } : void 0;
38511
+ const it = { entry, handle: this.addToChart(entry, values), ...opts.external ? { external: true } : {}, ...values ? { values } : {} };
37836
38512
  this.instances.push(it);
37837
38513
  this.deps.onIndicatorsChanged(this.id);
37838
38514
  if (opts.record === false) return;
@@ -37840,7 +38516,7 @@ var ChartCell = class {
37840
38516
  this.history.push({
37841
38517
  undo: () => this.dropInstance(snapshot),
37842
38518
  redo: () => {
37843
- snapshot.handle = this.addToChart(snapshot.entry);
38519
+ snapshot.handle = this.addToChart(snapshot.entry, snapshot.values);
37844
38520
  this.instances.push(snapshot);
37845
38521
  this.deps.onIndicatorsChanged(this.id);
37846
38522
  }
@@ -37853,7 +38529,7 @@ var ChartCell = class {
37853
38529
  const snapshot = it;
37854
38530
  this.history.push({
37855
38531
  undo: () => {
37856
- snapshot.handle = this.addToChart(snapshot.entry);
38532
+ snapshot.handle = this.addToChart(snapshot.entry, snapshot.values);
37857
38533
  this.instances.push(snapshot);
37858
38534
  this.deps.onIndicatorsChanged(this.id);
37859
38535
  },
@@ -37863,6 +38539,9 @@ var ChartCell = class {
37863
38539
  dropInstance(it) {
37864
38540
  const idx = this.instances.indexOf(it);
37865
38541
  if (idx >= 0) this.instances.splice(idx, 1);
38542
+ const captured = instanceDeltas(it.handle);
38543
+ if (captured) it.values = captured;
38544
+ else delete it.values;
37866
38545
  try {
37867
38546
  it.handle?.remove();
37868
38547
  } catch {
@@ -37905,9 +38584,13 @@ var ChartCell = class {
37905
38584
  this.deps.onIndicatorsChanged(this.id);
37906
38585
  });
37907
38586
  }
37908
- addToChart(entry) {
38587
+ addToChart(entry, values) {
37909
38588
  try {
37910
- return this.inner?.addIndicator(entry.script, entry.language !== void 0 ? { language: entry.language } : void 0) ?? null;
38589
+ return this.inner?.addIndicator(entry.script, {
38590
+ ...entry.language !== void 0 ? { language: entry.language } : {},
38591
+ ...values?.inputs ? { inputs: values.inputs } : {},
38592
+ ...values?.props ? { props: values.props } : {}
38593
+ }) ?? null;
37911
38594
  } catch (err) {
37912
38595
  console.warn(`[vela] indicator "${entry.name}" failed to add:`, err);
37913
38596
  return null;
@@ -38021,7 +38704,10 @@ var ChartCell = class {
38021
38704
  // manifest — their plugin persists them via the `ext` seam instead.
38022
38705
  indicators: indicatorLedger({
38023
38706
  present: this.inner ? this.inner.presentNativeIndicators() : [],
38024
- instanceNames: this.instances.filter((it) => !it.external).map((it) => it.entry.name),
38707
+ instanceEntries: this.instances.filter((it) => !it.external).map((it) => {
38708
+ const d = it.handle ? instanceDeltas(it.handle) : it.values;
38709
+ return d ? { name: it.entry.name, ...d } : it.entry.name;
38710
+ }),
38025
38711
  pendingManifest: this.pendingManifestNames,
38026
38712
  manifestSettled: this.deps.manifestSettled(),
38027
38713
  volumePending: this.volumeMayBePending && this.volumeIntent
@@ -38032,6 +38718,7 @@ var ChartCell = class {
38032
38718
  destroy() {
38033
38719
  this.destroyed = true;
38034
38720
  this.offMarket();
38721
+ this.cellControls.destroy();
38035
38722
  this.contextMenu.destroy();
38036
38723
  this.history.destroy();
38037
38724
  this.marketStatus?.stop();
@@ -38330,10 +39017,10 @@ var SplitterLayer = class {
38330
39017
  var DEFAULT_TIMEFRAMES = ["1", "5", "15", "60", "240", "D", "W"];
38331
39018
  var GAP_PX = 2;
38332
39019
  var POOL_CAP = 16;
38333
- var TIME_AXIS_H3 = 22;
39020
+ var TIME_AXIS_H4 = 22;
38334
39021
  var ALERT_CAP = 50;
38335
- var STYLE_ID26 = "vela-workspace";
38336
- var CSS23 = `
39022
+ var STYLE_ID27 = "vela-workspace";
39023
+ var CSS24 = `
38337
39024
  .vela-workspace { position: relative; width: 100%; height: 100%; display: flex; flex-direction: column; background: var(--vela-bg); }
38338
39025
  .vela-ws-main { position: relative; display: flex; flex-direction: row; flex: 1 1 auto; min-height: 0; }
38339
39026
  .vela-ws-toolbar { position: relative; flex: none; }
@@ -38360,6 +39047,21 @@ var CSS23 = `
38360
39047
  /* Mobile: the docked drawing-toolbar column would eat a phone-width grid \u2014 the shell's
38361
39048
  drawings drawer + on-chart pill replace it (same policy as the widget's in-chart bar). */
38362
39049
  [data-layout='mobile'] .vela-ws-toolbar { display: none; }
39050
+ /* A maximized cell owns the whole grid: the splitter strips have no seams to grab and
39051
+ the active ring would just outline the only visible chart \u2014 both are noise here. */
39052
+ .vela-ws-grid[data-maximized='1'] .vela-ws-splitter { display: none; }
39053
+ .vela-ws-grid[data-maximized='1'] .vela-cell[data-active='1']::after { display: none; }
39054
+ /* Drop-target preview while a cell's drag handle is held: a dashed ring + the same
39055
+ soft wash the splitter hover uses, over the chart, inert to the pointer. */
39056
+ .vela-cell[data-drop-target='1']::before {
39057
+ content: '';
39058
+ position: absolute;
39059
+ inset: 0;
39060
+ border: 2px dashed var(--vela-fg-bright);
39061
+ background: var(--vela-separator-hover-band);
39062
+ pointer-events: none;
39063
+ z-index: 11;
39064
+ }
38363
39065
  `;
38364
39066
  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
39067
  function declaredOrder(cells) {
@@ -38389,6 +39091,9 @@ var VelaWorkspace = class {
38389
39091
  * slots beyond the list get auto identities. Grows, never reorders. */
38390
39092
  this.order = [];
38391
39093
  this.activeId = null;
39094
+ /** The cell maximized over the whole grid (null = normal grid). TRANSIENT view
39095
+ * state — never persisted; any structural change (layout, applyState) restores. */
39096
+ this.maximizedId = null;
38392
39097
  this.cellBackend = "auto";
38393
39098
  this.destroyed = false;
38394
39099
  this.shortcutsHelp = null;
@@ -38493,7 +39198,7 @@ var VelaWorkspace = class {
38493
39198
  this.order = boot?.charts ? boot.charts.map((c) => c.id) : declaredOrder(opts.cells);
38494
39199
  const bootActive = boot?.activeCellId ?? null;
38495
39200
  const doc = hostEl.ownerDocument;
38496
- injectStyles(STYLE_ID26, CSS23, doc);
39201
+ injectStyles(STYLE_ID27, CSS24, doc);
38497
39202
  this.root = doc.createElement("div");
38498
39203
  this.root.className = "vela-workspace";
38499
39204
  ensureUIHost(this.root, resolveTheme(opts.theme));
@@ -38603,7 +39308,11 @@ var VelaWorkspace = class {
38603
39308
  if (attribution !== false) {
38604
39309
  const background = resolveTheme(opts.theme).background;
38605
39310
  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" });
39311
+ Object.assign(mark.style, {
39312
+ left: "calc(var(--vela-toolbar-gutter, 0px) + 12px)",
39313
+ bottom: `calc(var(--vela-bottom-gutter, ${TIME_AXIS_H4}px) + 10px)`,
39314
+ zIndex: "11"
39315
+ });
38607
39316
  this.gridEl.appendChild(mark);
38608
39317
  this.attributionMark = mark;
38609
39318
  }
@@ -38669,6 +39378,9 @@ var VelaWorkspace = class {
38669
39378
  ...topbarHas(this.topbarComp, "indicators") && (picker || this.indicatorsOverride) ? { onIndicatorsClick: this.indicatorsOverride ? () => this.runOverride(this.indicatorsOverride) : () => picker.open() } : {},
38670
39379
  getContext: () => this.context(),
38671
39380
  ...this.drawingsEnabled ? { onDrawingsClick: () => this.openDrawingsDrawer() } : {},
39381
+ // Multi-chart only: the stop that isolates the ACTIVE chart (the
39382
+ // per-cell hover cluster has no cursor to reveal it on mobile).
39383
+ ...this.monoLayout ? {} : { onMaximizeClick: () => this.toggleMobileMaximize() },
38672
39384
  onMoreClick: () => this.openMoreDrawer(),
38673
39385
  onSettingsClick: () => this.active.chart.renderer.openSettings()
38674
39386
  }) : null;
@@ -38875,7 +39587,9 @@ var VelaWorkspace = class {
38875
39587
  this.pool.clear();
38876
39588
  for (const { id, ...cs } of st.charts.slice(liveCount)) this.pool.set(id, cs);
38877
39589
  this.order = st.charts.map((c) => c.id);
39590
+ this.clearMaximized();
38878
39591
  this.applyGrid();
39592
+ this.refreshCellControls();
38879
39593
  const nextActive2 = st.activeCellId && this.cellsById.has(st.activeCellId) ? st.activeCellId : this.order[0] ?? null;
38880
39594
  if (nextActive2 === this.activeId) this.projectActiveCell();
38881
39595
  else this.setActiveCell(nextActive2);
@@ -38901,6 +39615,7 @@ var VelaWorkspace = class {
38901
39615
  const def = this.monoLayout ? null : ensureLayout(st.layout);
38902
39616
  if (def) this.def = def;
38903
39617
  this.cellBackend = this.backendFor(this.def);
39618
+ this.clearMaximized();
38904
39619
  this.applyGrid();
38905
39620
  this.buildCells();
38906
39621
  this.syncCellPresentation();
@@ -38964,6 +39679,7 @@ var VelaWorkspace = class {
38964
39679
  setLayout(layout) {
38965
39680
  if (this.destroyed) return;
38966
39681
  if (this.monoLayout) return;
39682
+ this.clearMaximized();
38967
39683
  const next = this.resolveLayout(layout);
38968
39684
  const nextBackend = this.backendFor(next);
38969
39685
  const rebuildAll = nextBackend !== this.cellBackend;
@@ -38984,6 +39700,7 @@ var VelaWorkspace = class {
38984
39700
  this.buildCells();
38985
39701
  this.alignNewCellStyles(preexisting);
38986
39702
  this.syncCellPresentation();
39703
+ this.refreshCellControls();
38987
39704
  this.topbar.setLayout(next.id);
38988
39705
  const nextActive = activeAfterLayout(this.activeId, this.order.slice(0, next.cells.length));
38989
39706
  if (nextActive === this.activeId) this.projectActiveCell();
@@ -38992,6 +39709,67 @@ var VelaWorkspace = class {
38992
39709
  this.events.emit("layout:changed", { layout: next.id });
38993
39710
  this.markStateDirty();
38994
39711
  }
39712
+ /** The identity of the cell maximized over the whole grid, or null. */
39713
+ get maximizedCell() {
39714
+ return this.maximizedId;
39715
+ }
39716
+ /**
39717
+ * Maximize one cell over the whole grid, or restore the layout with `null`. Pure
39718
+ * presentation: the other cells stay alive underneath — charts, subscriptions and
39719
+ * state untouched — so restoring is instant. The maximized cell becomes the active
39720
+ * one. Transient view state (also reachable from each cell's bottom-center view
39721
+ * cluster): switching layouts or applying a state document restores the grid.
39722
+ */
39723
+ maximizeCell(id) {
39724
+ if (this.destroyed) return;
39725
+ if (id != null && (!this.cellsById.has(id) || this.def.cells.length <= 1)) return;
39726
+ if (id === this.maximizedId) return;
39727
+ this.maximizedId = id;
39728
+ if (id) this.setActiveCell(id);
39729
+ this.applyGrid();
39730
+ this.refreshCellControls();
39731
+ this.syncMobileMaximize();
39732
+ this.events.emit("cell:maximized", { id });
39733
+ }
39734
+ /** The mobile bar's maximize stop: one press isolates the ACTIVE chart over the
39735
+ * grid; while something is already isolated — the chart, or a pane inside it
39736
+ * (mobile's double-tap) — the press restores that instead. Every branch re-syncs
39737
+ * the stop on its own (`maximizeCell` directly, `panes.maximize` via its
39738
+ * synchronous `pane:changed`). */
39739
+ toggleMobileMaximize() {
39740
+ const cell = this.activeId ? this.cellsById.get(this.activeId) : void 0;
39741
+ if (!cell) return;
39742
+ if (this.maximizedId) this.maximizeCell(null);
39743
+ else if (cell.chart.panes.list().some((p) => p.maximized)) cell.chart.panes.maximize(null);
39744
+ else this.maximizeCell(cell.id);
39745
+ }
39746
+ /** Keep the mobile bar's maximize stop truthful: lit (inverse chip, restore
39747
+ * glyph) while the active chart covers the grid OR one of its panes is
39748
+ * maximized — the state a double-tap toggles is otherwise invisible on mobile. */
39749
+ syncMobileMaximize() {
39750
+ if (!this.mobileBar) return;
39751
+ const cell = this.activeId ? this.cellsById.get(this.activeId) : void 0;
39752
+ const paneMax = cell ? cell.chart.panes.list().some((p) => p.maximized) : false;
39753
+ this.mobileBar.setMaximizeActive(this.maximizedId != null || paneMax);
39754
+ }
39755
+ /**
39756
+ * Trade the SLOTS of two live cells — the grid arrangement changes, the cells
39757
+ * themselves (charts, indicators, drawings, the active flag) stay untouched.
39758
+ * What each cell's drag handle commits; also callable directly by hosts.
39759
+ */
39760
+ swapCells(a, b) {
39761
+ if (this.destroyed || a === b) return;
39762
+ const i = this.order.indexOf(a);
39763
+ const j = this.order.indexOf(b);
39764
+ if (i < 0 || j < 0 || !this.cellsById.has(a) || !this.cellsById.has(b)) return;
39765
+ [this.order[i], this.order[j]] = [this.order[j], this.order[i]];
39766
+ for (const [k] of this.def.cells.entries()) {
39767
+ const host = this.cellsById.get(this.order[k] ?? "")?.host;
39768
+ if (host) this.gridEl.appendChild(host);
39769
+ }
39770
+ this.applyGrid();
39771
+ this.markStateDirty();
39772
+ }
38995
39773
  resize() {
38996
39774
  this.splitters.layout();
38997
39775
  }
@@ -39061,6 +39839,7 @@ var VelaWorkspace = class {
39061
39839
  this.mobileBar?.renderActions();
39062
39840
  this.mobileBar?.setSymbol(cell.symbol);
39063
39841
  this.mobileBar?.setTimeframe(cell.timeframe);
39842
+ this.syncMobileMaximize();
39064
39843
  this.drawingPill?.onChart(cell.chart);
39065
39844
  const pushHistory = () => this.topbar.setHistoryState(cell.history.canUndo, cell.history.canRedo);
39066
39845
  this.historyUnsub?.();
@@ -39150,8 +39929,78 @@ var VelaWorkspace = class {
39150
39929
  const host = this.cellsById.get(this.order[i] ?? "")?.host;
39151
39930
  if (host) host.style.gridArea = perCell[slot.id]?.gridArea ?? "";
39152
39931
  }
39932
+ this.applyMaximizePresentation();
39933
+ this.mountAttributionMark();
39153
39934
  this.splitters.layout();
39154
39935
  }
39936
+ /** Overlay the maximize presentation on the freshly applied grid: EVERY cell spans
39937
+ * the full track grid — the maximized one on top, the siblings invisible beneath
39938
+ * it (their charts stay alive — restoring is instant). The siblings must span too:
39939
+ * left in their slots they would auto-flow into implicit zero-height rows, whose
39940
+ * gaps steal height from the maximized cell and collapse their renderers to 0.
39941
+ * The splitter strips and the active ring hide via the `data-maximized` rules. */
39942
+ applyMaximizePresentation() {
39943
+ const maxId = this.maximizedId;
39944
+ if (maxId) this.gridEl.dataset.maximized = "1";
39945
+ else delete this.gridEl.dataset.maximized;
39946
+ for (const [id, cell] of this.cellsById) {
39947
+ const style = cell.host.style;
39948
+ if (maxId) style.gridArea = "1 / 1 / -1 / -1";
39949
+ style.zIndex = maxId && id === maxId ? "5" : "";
39950
+ style.visibility = maxId && id !== maxId ? "hidden" : "";
39951
+ }
39952
+ }
39953
+ /** Rebuild every cell's view cluster (the maximize gate or state changed). */
39954
+ refreshCellControls() {
39955
+ for (const cell of this.cellsById.values()) cell.refreshControls();
39956
+ }
39957
+ /** Drop the transient maximize on a structural change (layout switch, state
39958
+ * document) — WITH the event, so hosts tracking `cell:maximized` never drift
39959
+ * from `maximizedCell`. The caller's own grid re-apply paints the restore. */
39960
+ clearMaximized() {
39961
+ if (this.maximizedId == null) return;
39962
+ this.maximizedId = null;
39963
+ this.events.emit("cell:maximized", { id: null });
39964
+ }
39965
+ /** The live cell under a viewport point, excluding `excludeId` and any host a
39966
+ * maximize has hidden — the drag handle's hit-test. */
39967
+ cellAtPoint(x, y, excludeId) {
39968
+ for (const [id, cell] of this.cellsById) {
39969
+ if (id === excludeId || cell.host.style.visibility === "hidden") continue;
39970
+ const r = cell.host.getBoundingClientRect();
39971
+ if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return id;
39972
+ }
39973
+ return null;
39974
+ }
39975
+ /** Mark one cell as the live drop target of a grip drag (null clears all) —
39976
+ * the `data-drop-target` stylesheet rule paints the dashed preview ring. */
39977
+ setDropTarget(id) {
39978
+ for (const [cid, cell] of this.cellsById) {
39979
+ if (cid === id) cell.host.dataset.dropTarget = "1";
39980
+ else delete cell.host.dataset.dropTarget;
39981
+ }
39982
+ }
39983
+ /** The cell whose bottom-left corner the grid's attribution mark floats in — the
39984
+ * maximized cell while one covers the grid, else the bottom-left slot's cell. */
39985
+ bottomLeftCell() {
39986
+ if (this.maximizedId) return this.cellsById.get(this.maximizedId);
39987
+ const grid = occupancyGrid(this.def);
39988
+ const slot = grid[grid.length - 1]?.[0];
39989
+ const idx = this.def.cells.findIndex((c) => (c.area ?? c.id) === slot);
39990
+ return this.cellsById.get(this.order[idx >= 0 ? idx : 0] ?? "");
39991
+ }
39992
+ /** Keep the shared attribution mark inside the BOTTOM-LEFT visible cell: its
39993
+ * offsets ride that cell's renderer-published `--vela-bottom-gutter` /
39994
+ * `--vela-toolbar-gutter`, so collapsed pane strips push the mark up without any
39995
+ * bookkeeping here. Re-run after anything that changes which host that is
39996
+ * (layout switch, maximize, cell rebuild); a destroyed host drops the mark from
39997
+ * the DOM, and this re-mount brings it back. */
39998
+ mountAttributionMark() {
39999
+ const mark = this.attributionMark;
40000
+ if (!mark) return;
40001
+ const host = this.bottomLeftCell()?.host ?? this.gridEl;
40002
+ if (mark.parentElement !== host) host.appendChild(mark);
40003
+ }
39155
40004
  /** Create the cells the current layout wants but don't exist yet (pool-first).
39156
40005
  * A slot's CELL IDENTITY is `order[i]` (declaration order — never the slot's own
39157
40006
  * positional id); slots past the declared list mint an auto identity once. */
@@ -39183,6 +40032,12 @@ var VelaWorkspace = class {
39183
40032
  setTimezone: (zone) => this.setTimezone(zone),
39184
40033
  context: () => this.context(),
39185
40034
  activate: (id2) => this.setActiveCell(id2),
40035
+ multiCell: () => !this.monoLayout && this.def.cells.length > 1,
40036
+ isMaximized: (id2) => this.maximizedId === id2,
40037
+ toggleMaximize: (id2) => this.maximizeCell(this.maximizedId === id2 ? null : id2),
40038
+ cellDragTarget: (id2, x, y) => this.cellAtPoint(x, y, id2),
40039
+ previewDropTarget: (target) => this.setDropTarget(target),
40040
+ dropCell: (id2, target) => this.swapCells(id2, target),
39186
40041
  onMarketChanged: (id2) => this.onCellMarketChanged(id2),
39187
40042
  onPriceStyleChanged: (id2) => this.onCellPriceStyleChanged(id2),
39188
40043
  onIndicatorsChanged: (id2) => this.onCellIndicatorsChanged(id2),
@@ -39196,6 +40051,7 @@ var VelaWorkspace = class {
39196
40051
  if (id === this.activeId) cell.host.dataset.active = "1";
39197
40052
  this.wireCell(cell);
39198
40053
  cell.chart.renderer.setLayoutMode(this.layoutCtl.current);
40054
+ cell.setControlsSuspended(this.layoutCtl.current === "mobile");
39199
40055
  if (this.favs.length > 0) cell.chart.drawings.setFavorites(this.favs);
39200
40056
  cell.setManifest(this.manifest, pooled?.indicators == null);
39201
40057
  cell.restorePersistedExt();
@@ -39205,6 +40061,7 @@ var VelaWorkspace = class {
39205
40061
  const host = this.cellsById.get(this.order[i] ?? "")?.host;
39206
40062
  if (host) this.gridEl.appendChild(host);
39207
40063
  }
40064
+ this.mountAttributionMark();
39208
40065
  }
39209
40066
  /** Per-cell chart subscriptions (trigger ② — the chart instance is stable for the
39210
40067
  * cell's whole life, so these live and die with the cell). */
@@ -39264,6 +40121,9 @@ var VelaWorkspace = class {
39264
40121
  chart.on("viewport:changed", (range) => this.propagateViewport(cell.id, range));
39265
40122
  chart.renderer.onConfigChanged(() => this.propagateStylePrefs(cell.id));
39266
40123
  chart.on("theme:changed", (t) => this.setTheme(t));
40124
+ chart.on("pane:changed", () => {
40125
+ if (cell.id === this.activeId) this.syncMobileMaximize();
40126
+ });
39267
40127
  chart.renderer.onAxisLongPress((e) => {
39268
40128
  if (this.layoutCtl.current !== "mobile") return;
39269
40129
  if (e.axis === "time") this.openTimezoneDrawer();
@@ -39584,6 +40444,7 @@ var VelaWorkspace = class {
39584
40444
  for (const cell of this.cellsById.values()) {
39585
40445
  cell.chart.renderer.closeDialogs();
39586
40446
  cell.chart.renderer.setLayoutMode(mode);
40447
+ cell.setControlsSuspended(mode === "mobile");
39587
40448
  }
39588
40449
  this.syncCellPresentation();
39589
40450
  }