@luxalgo/vela 0.6.9 → 0.6.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/{DataProvider-8Z95Q-RJ.d.cts → DataProvider-BBf-jc6W.d.ts} +49 -1
  2. package/dist/{DataProvider-DExJrfut.d.ts → DataProvider-p0TEyhlX.d.cts} +49 -1
  3. package/dist/{chunk-6WDDVMBJ.js → chunk-73PEA4MU.js} +707 -353
  4. package/dist/{chunk-62SVGONC.js → chunk-G77Y7LK2.js} +2 -2
  5. package/dist/{chunk-STHSKXOR.js → chunk-IO3NYSQV.js} +650 -157
  6. package/dist/{chunk-WFSZBX3R.js → chunk-KG4YT3TI.js} +3 -1
  7. package/dist/{chunk-FCVIG7JG.js → chunk-MHY7MVXH.js} +5 -1
  8. package/dist/{chunk-EQCHJZOT.js → chunk-MTLJKZDZ.js} +10 -2
  9. package/dist/{contributions-D7PVZO2i.d.ts → contributions-Bbe2R-mQ.d.ts} +20 -6
  10. package/dist/{contributions-C1U2Krwg.d.cts → contributions-CO01zWve.d.cts} +20 -6
  11. package/dist/index.cjs +719 -350
  12. package/dist/index.d.cts +37 -10
  13. package/dist/index.d.ts +37 -10
  14. package/dist/index.js +5 -5
  15. package/dist/{options-FM0peknS.d.ts → options-yp7sA96q.d.cts} +14 -7
  16. package/dist/{options-FM0peknS.d.cts → options-yp7sA96q.d.ts} +14 -7
  17. package/dist/{plugin-DfVqBz9p.d.cts → plugin-CkkH8QnX.d.cts} +3 -3
  18. package/dist/{plugin-7bkF32Rk.d.ts → plugin-DwxjM3Ni.d.ts} +3 -3
  19. package/dist/plugin.cjs +12 -1
  20. package/dist/plugin.d.cts +4 -4
  21. package/dist/plugin.d.ts +4 -4
  22. package/dist/plugin.js +3 -3
  23. package/dist/providers/binance.d.cts +2 -2
  24. package/dist/providers/binance.d.ts +2 -2
  25. package/dist/providers/coinbase.d.cts +2 -2
  26. package/dist/providers/coinbase.d.ts +2 -2
  27. package/dist/providers/hyperliquid.d.cts +2 -2
  28. package/dist/providers/hyperliquid.d.ts +2 -2
  29. package/dist/{statusline-DOPiT6I6.d.cts → statusline-CHPDuKNp.d.ts} +13 -5
  30. package/dist/{statusline-zdF4eZLr.d.ts → statusline-DTHZUFqK.d.cts} +13 -5
  31. package/dist/ui.cjs +7 -1
  32. package/dist/ui.d.cts +3 -1
  33. package/dist/ui.d.ts +3 -1
  34. package/dist/ui.js +3 -3
  35. package/dist/vela.global.js +719 -350
  36. package/dist/vela.global.min.js +51 -49
  37. package/dist/widget.cjs +1249 -388
  38. package/dist/widget.d.cts +17 -6
  39. package/dist/widget.d.ts +17 -6
  40. package/dist/widget.js +7 -7
  41. package/dist/workspace.cjs +1249 -388
  42. package/dist/workspace.d.cts +103 -10
  43. package/dist/workspace.d.ts +103 -10
  44. package/dist/workspace.js +6 -6
  45. package/package.json +1 -1
  46. /package/dist/{chunk-AOGZBKUE.js → chunk-H26BEHF4.js} +0 -0
package/dist/widget.cjs CHANGED
@@ -125,6 +125,23 @@ var TypedEventBus = class {
125
125
  }
126
126
  };
127
127
 
128
+ // src/data/symbol-groups.ts
129
+ function isGroupRow(d) {
130
+ return d.group != null && d.ticker === d.group;
131
+ }
132
+ function groupKeyOf(d) {
133
+ return `${(d.prefix ?? d.provider ?? "").toLowerCase()}:${(d.group ?? "").toUpperCase()}`;
134
+ }
135
+ function groupMembers(pool, groupRow) {
136
+ const key = groupKeyOf(groupRow);
137
+ return pool.filter((s) => s.group != null && !isGroupRow(s) && groupKeyOf(s) === key);
138
+ }
139
+ function defaultMemberOf(pool, groupRow) {
140
+ const members = groupMembers(pool, groupRow);
141
+ const defaults2 = members.filter((m) => m.default);
142
+ return (defaults2.length === 1 ? defaults2[0] : members[0]) ?? null;
143
+ }
144
+
128
145
  // src/data/ProviderRegistry.ts
129
146
  var normName = (n) => n.trim().toLowerCase();
130
147
  var normTicker = (t) => t.trim().toUpperCase();
@@ -214,17 +231,24 @@ var ProviderRegistry = class {
214
231
  resolve(raw, opts = {}) {
215
232
  const { provider, ticker } = this.parse(raw);
216
233
  if (provider) {
217
- if (this.entries.has(provider)) return { provider, ticker };
234
+ if (this.entries.has(provider)) {
235
+ const d = this.entries.get(provider).byTicker?.get(normTicker(ticker));
236
+ return { provider, ticker: d != null && isGroupRow(d) ? this.loadableTicker(provider, d) : ticker };
237
+ }
218
238
  const key = `${provider}:${normTicker(ticker)}`;
219
239
  for (const name of this.candidateOrder(opts.default)) {
220
240
  const d = this.entries.get(name).prefixIndex?.get(key);
221
- if (d) return { provider: name, ticker: d.ticker };
241
+ if (d) return { provider: name, ticker: this.loadableTicker(name, d) };
222
242
  }
223
243
  return null;
224
244
  }
225
245
  const norm = normTicker(ticker);
226
246
  for (const name of this.candidateOrder(opts.default)) {
227
- if (this.entries.get(name).index?.has(norm)) return { provider: name, ticker };
247
+ const e = this.entries.get(name);
248
+ if (e.index?.has(norm)) {
249
+ const d = e.byTicker?.get(norm);
250
+ return { provider: name, ticker: d != null && isGroupRow(d) ? this.loadableTicker(name, d) : ticker };
251
+ }
228
252
  }
229
253
  if (opts.lenient && this.entries.size === 1) {
230
254
  const only = this.names()[0];
@@ -233,6 +257,13 @@ var ProviderRegistry = class {
233
257
  }
234
258
  return null;
235
259
  }
260
+ /** What a resolved descriptor LOADS: itself — unless it is a GROUP row (listed, never
261
+ * served), which loads its default member (single `default`, else first listed). A
262
+ * memberless group keeps its own ticker and fails downstream like any unknown symbol. */
263
+ loadableTicker(name, d) {
264
+ if (!isGroupRow(d)) return d.ticker;
265
+ return defaultMemberOf(this.entries.get(name)?.descriptors ?? [], d)?.ticker ?? d.ticker;
266
+ }
236
267
  /**
237
268
  * Resolve now if possible, else resolve later — re-attempting after each provider
238
269
  * settles. The promise stays pending until some registered provider can serve the
@@ -486,6 +517,28 @@ var CachingDataFeed = class {
486
517
  this.store.merge(key, dropForming(bars));
487
518
  return bars;
488
519
  }
520
+ /**
521
+ * Progressive twin of {@link load}: a COLD load streams through the inner feed's
522
+ * progressive path — batches forwarded verbatim, the FINAL answer cached exactly as
523
+ * `load` caches — while a cache-covered load answers once through `load` itself (a
524
+ * warm chart has nothing to stream). Falls back to `load` wholesale when the inner
525
+ * feed lacks the capability, so callers may prefer this method unconditionally.
526
+ */
527
+ async loadProgressive(cfg, onBatch, opts) {
528
+ if (cfg.data && cfg.data.length > 0) return this.inner.load(cfg);
529
+ if (!this.inner.loadProgressive) return null;
530
+ const symbol = cfg.symbol ?? "TEST";
531
+ const key = cacheKey(symbol, cfg.timeframe ?? "60", cfg.session);
532
+ const cached = this.store.get(key);
533
+ const n = cfg.bars ?? 500;
534
+ const lastCached = cached?.[cached.length - 1];
535
+ if (cached && lastCached && cached.length >= n - 1 && this.inner.loadRange) return this.load(cfg);
536
+ this.store.retainSymbol(symbol);
537
+ const bars = await this.inner.loadProgressive(cfg, onBatch, opts);
538
+ if (bars == null) return null;
539
+ this.store.merge(key, dropForming(bars));
540
+ return bars;
541
+ }
489
542
  /**
490
543
  * Cache-backed ranged fetch — the gateway engines use for secondary series
491
544
  * (`request.security` HTF/LTF/cross-symbol) and the orchestrator's backward
@@ -731,6 +784,17 @@ var MultiProviderFeed = class {
731
784
  this.prefetchSymbolInfo(resolved);
732
785
  return this.cache.load(canonical(cfg, resolved));
733
786
  }
787
+ /** Progressive twin of {@link load} — same resolution, the cache streams the batches. */
788
+ async loadProgressive(cfg, onBatch, opts) {
789
+ if (cfg.data && cfg.data.length > 0) {
790
+ this.liveBars = cfg.data.map((b) => ({ ...b }));
791
+ return cfg.data;
792
+ }
793
+ const resolved = await this.registry.whenResolvable(rawSymbol(cfg), { default: this.primaryProvider });
794
+ this.primaryProvider = resolved.provider;
795
+ this.prefetchSymbolInfo(resolved);
796
+ return this.cache.loadProgressive(canonical(cfg, resolved), onBatch, opts);
797
+ }
734
798
  /**
735
799
  * Synchronous per-symbol metadata for engines (Pine `syminfo.*`), served from the cache
736
800
  * warmed by load(). Undefined until the prefetch lands (the engine then synthesizes a
@@ -818,6 +882,18 @@ var RegistryFetchFeed = class {
818
882
  if (!provider) return Promise.resolve([]);
819
883
  return safeBars(provider, ticker, cfg.timeframe ?? "60", { limit: cfg.bars ?? 500, session: cfg.session });
820
884
  }
885
+ async loadProgressive(cfg, onBatch, opts) {
886
+ const { provider: name, ticker } = parseSymbol(cfg.symbol ?? "");
887
+ const provider = this.registry.get(name ?? "");
888
+ if (!provider) return [];
889
+ if (!provider.getBarsProgressive) return null;
890
+ try {
891
+ return await provider.getBarsProgressive(ticker, cfg.timeframe ?? "60", { limit: cfg.bars ?? 500, session: cfg.session }, onBatch, opts);
892
+ } catch (e) {
893
+ console.warn(`[vela] progressive fetch failed for ${ticker} ${cfg.timeframe ?? "60"} \u2014 ${e instanceof Error ? e.message : String(e)}`);
894
+ return [];
895
+ }
896
+ }
821
897
  loadRange(cfg, range) {
822
898
  const { provider: name, ticker } = parseSymbol(cfg.symbol ?? "");
823
899
  const provider = this.registry.get(name ?? "");
@@ -1134,6 +1210,8 @@ registerIcon("folder-plus", S('<path d="M1.6 4.4a1 1 0 0 1 1-1h2.7l1.3 1.7h6.8a1
1134
1210
  registerIcon("folder-minus", S('<path d="M1.6 4.4a1 1 0 0 1 1-1h2.7l1.3 1.7h6.8a1 1 0 0 1 1 1v6.5a1 1 0 0 1-1 1h-10.8a1 1 0 0 1-1-1z"/><path d="M6.2 9.4h3.6"/>'));
1135
1211
  registerIcon("collapse", S('<path d="M3 8h10"/>'));
1136
1212
  registerIcon("expand", S('<rect x="2.2" y="2.2" width="11.6" height="11.6" rx="1.4"/><path d="M8 5.4v5.2M5.4 8h5.2"/>'));
1213
+ registerIcon("plus", S('<path d="M8 2.8v10.4M2.8 8h10.4"/>'));
1214
+ registerIcon("minus", S('<path d="M2.8 8h10.4"/>'));
1137
1215
  registerIcon("maximize", S('<path d="M2.5 6V3a.5.5 0 0 1 .5-.5h3M10 2.5h3a.5.5 0 0 1 .5.5v3M13.5 10v3a.5.5 0 0 1-.5.5h-3M6 13.5H3a.5.5 0 0 1-.5-.5v-3"/>'));
1138
1216
  registerIcon("restore", S('<path d="M6.2 2.5v3.7H2.5M9.8 13.5V9.8h3.7M13.5 6.2H9.8V2.5M2.5 9.8h3.7v3.7"/>'));
1139
1217
  registerIcon("star", S('<path d="M8 2.2l1.75 3.55 3.9.55-2.8 2.75.65 3.9L8 11.1l-3.5 1.85.65-3.9-2.8-2.75 3.9-.55z"/>'));
@@ -1141,7 +1219,7 @@ registerIcon("star-filled", S('<path d="M8 2.2l1.75 3.55 3.9.55-2.8 2.75.65 3.9L
1141
1219
  registerIcon("grip", S('<circle cx="6" cy="3.5" r="1"/><circle cx="10" cy="3.5" r="1"/><circle cx="6" cy="8" r="1"/><circle cx="10" cy="8" r="1"/><circle cx="6" cy="12.5" r="1"/><circle cx="10" cy="12.5" r="1"/>', 'fill="currentColor" stroke="none"'));
1142
1220
  registerIcon("kebab", S('<circle cx="8" cy="3.2" r="1.2"/><circle cx="8" cy="8" r="1.2"/><circle cx="8" cy="12.8" r="1.2"/>', 'fill="currentColor" stroke="none"'));
1143
1221
  registerIcon("burger", S('<path d="M2.5 4.5h11M2.5 8h11M2.5 11.5h11"/>'));
1144
- registerIcon("reset", S('<rect x="6" y="6" width="4" height="4" rx="0.8"/><path d="M2.2 8a5.8 5.8 0 1 0 1.9-4.3L2.2 5.3"/><path d="M2.2 2.2v3.1h3.1"/>'));
1222
+ registerIcon("reset", S('<path d="M2 8a6 6 0 1 0 6-6 6.5 6.5 0 0 0-4.5 1.83L2 5.33"/><path d="M2 2v3.33h3.33"/>'));
1145
1223
  registerIcon("pane-collapse", S('<path d="M2.6 9.4h4v4M13.4 6.6h-4v-4"/><path d="m9.4 6.6 4.2-4.2M2.4 13.6l4.2-4.2"/>'));
1146
1224
  registerIcon("pane-expand", S('<path d="M9.8 2.4h3.8v3.8M6.2 13.6H2.4V9.8"/><path d="m13.6 2.4-4.4 4.4M2.4 13.6l4.4-4.4"/>'));
1147
1225
  registerIcon("cursor", svg24('<path d="M5 3l6 16 2-6 6-2z"/>', 'fill="currentColor" stroke="none"'));
@@ -4307,6 +4385,10 @@ var CalloutBubble = class {
4307
4385
  get open() {
4308
4386
  return this.pop?.open ?? false;
4309
4387
  }
4388
+ /** Close the deployed panel, if any. The bubble itself stays. */
4389
+ hidePanel() {
4390
+ this.pop?.hide();
4391
+ }
4310
4392
  destroy() {
4311
4393
  this.pop?.destroy();
4312
4394
  this.pop = null;
@@ -13740,6 +13822,17 @@ function dedupeSymbols(list) {
13740
13822
  }
13741
13823
  return out;
13742
13824
  }
13825
+ function foldGroups(list) {
13826
+ const groupsAbove = /* @__PURE__ */ new Set();
13827
+ const out = [];
13828
+ for (const s of list) {
13829
+ if (isGroupRow(s)) {
13830
+ groupsAbove.add(groupKeyOf(s));
13831
+ out.push(s);
13832
+ } else if (s.group == null || !groupsAbove.has(groupKeyOf(s))) out.push(s);
13833
+ }
13834
+ return out;
13835
+ }
13743
13836
  var TOP_TICKERS = ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT", "DOGEUSDT", "ADAUSDT", "LINKUSDT"];
13744
13837
  function parseQuery(raw, venues) {
13745
13838
  const m = raw.match(/^\s*([^\s:]+)\s*[:\s]\s*(.*)$/);
@@ -13871,6 +13964,21 @@ var CSS8 = `
13871
13964
  .vela-sp-badge[data-p='binance'] { color: #f0b90b; } /* palette-exempt: venue brand mark */
13872
13965
  .vela-sp-badge[data-p='hyperliquid'] { color: #50d2c1; } /* palette-exempt: venue brand mark */
13873
13966
  .vela-sp-empty { padding: var(--vela-space-3); color: var(--vela-fg-muted); text-align: center; }
13967
+ /* Grouped listings (futures roots): the chevron unfolds members inline, indented. */
13968
+ .vela-sp-expander {
13969
+ all: unset;
13970
+ flex: none;
13971
+ display: inline-flex;
13972
+ align-items: center;
13973
+ justify-content: center;
13974
+ width: 22px;
13975
+ height: 22px;
13976
+ border-radius: 5px;
13977
+ cursor: pointer;
13978
+ color: var(--vela-fg-muted);
13979
+ }
13980
+ .vela-sp-expander:hover { background: var(--vela-surface-elev); color: var(--vela-fg); }
13981
+ .vela-sp-row[data-member] { padding-left: 34px; }
13874
13982
  `;
13875
13983
  var PAGE = 100;
13876
13984
  var SymbolPicker = class {
@@ -13882,6 +13990,11 @@ var SymbolPicker = class {
13882
13990
  this.seed = "";
13883
13991
  this.activeTab = "All";
13884
13992
  this.visible = PAGE;
13993
+ /** The last filter pass returned fewer raw rows than asked — the pool is drained
13994
+ * (checked BEFORE folding: folding shortens pages without meaning exhaustion). */
13995
+ this.exhausted = false;
13996
+ /** Group rows currently expanded (venue-scoped keys) — members shown inline. */
13997
+ this.expanded = /* @__PURE__ */ new Set();
13885
13998
  /** The ranked pool cache — `key` fingerprints the raw pool the ranking ran on. */
13886
13999
  this.ranked = null;
13887
14000
  this.ranking = false;
@@ -13896,7 +14009,7 @@ var SymbolPicker = class {
13896
14009
  searchRow.append(iconEl("search", doc), this.input);
13897
14010
  this.tabs = doc.createElement("div");
13898
14011
  this.tabs.className = "vela-sp-tabs";
13899
- for (const t of ["All", "Stocks", "ETFs", "Crypto", "Forex", "Commodities"]) {
14012
+ for (const t of ["All", "Stocks", "ETFs", "Crypto", "Futures", "Forex", "Commodities"]) {
13900
14013
  const b = doc.createElement("button");
13901
14014
  b.className = "vela-sp-tab";
13902
14015
  b.textContent = t;
@@ -13912,7 +14025,7 @@ var SymbolPicker = class {
13912
14025
  this.list = doc.createElement("div");
13913
14026
  this.list.className = "vela-sp-list";
13914
14027
  this.list.addEventListener("scroll", () => {
13915
- if (this.rows.length < this.visible) return;
14028
+ if (this.exhausted) return;
13916
14029
  if (this.list.scrollTop + this.list.clientHeight < this.list.scrollHeight - 200) return;
13917
14030
  this.visible += PAGE;
13918
14031
  this.grow();
@@ -13941,14 +14054,19 @@ var SymbolPicker = class {
13941
14054
  else if (e.key === "ArrowUp") this.moveHighlight(-1);
13942
14055
  else if (e.key === "Enter") {
13943
14056
  const pick = this.rows[this.highlighted];
13944
- if (pick) this.select(pick.ticker, pick.prefix ?? pick.provider, opts.onSelect);
14057
+ if (pick) this.pick(pick);
13945
14058
  return;
13946
14059
  } else return;
13947
14060
  e.preventDefault();
13948
14061
  });
13949
14062
  this.list.addEventListener("click", (e) => {
13950
- const row = e.target.closest(".vela-sp-row");
13951
- if (row?.dataset.ticker) this.select(row.dataset.ticker, row.dataset.venue, opts.onSelect);
14063
+ const target = e.target;
14064
+ const row = target.closest(".vela-sp-row");
14065
+ if (!row) return;
14066
+ const s = this.rows[Number(row.dataset.i)];
14067
+ if (!s) return;
14068
+ if (target.closest(".vela-sp-expander")) this.toggleExpand(s);
14069
+ else this.pick(s);
13952
14070
  });
13953
14071
  }
13954
14072
  /** Wire where symbols come from (re-called on every widget rebuild). */
@@ -13965,6 +14083,27 @@ var SymbolPicker = class {
13965
14083
  destroy() {
13966
14084
  this.dialog.destroy();
13967
14085
  }
14086
+ /** Route a row activation: a GROUP row loads its default member (the root itself is
14087
+ * listed, never loadable), any other row loads itself. */
14088
+ pick(s) {
14089
+ const target = isGroupRow(s) ? defaultMemberOf(this.pool(), s) ?? s : s;
14090
+ this.select(target.ticker, target.prefix ?? target.provider, this.opts.onSelect);
14091
+ }
14092
+ /** Expand/collapse a group row IN PLACE — same query, same page, same scroll; only
14093
+ * the member rows under the group appear or go. */
14094
+ toggleExpand(s) {
14095
+ const key = groupKeyOf(s);
14096
+ if (!this.expanded.delete(key)) this.expanded.add(key);
14097
+ const scrollTop = this.list.scrollTop;
14098
+ const focus = this.rows[this.highlighted];
14099
+ this.rows = this.computeRows();
14100
+ this.list.replaceChildren();
14101
+ this.rows.forEach((r, i) => this.list.appendChild(this.rowEl(r, i)));
14102
+ const at = focus ? this.rows.indexOf(focus) : -1;
14103
+ this.highlighted = at >= 0 ? at : Math.min(this.highlighted, Math.max(0, this.rows.length - 1));
14104
+ this.renderHighlight();
14105
+ this.list.scrollTop = scrollTop;
14106
+ }
13968
14107
  select(ticker, venue, onSelect) {
13969
14108
  this.close();
13970
14109
  onSelect(venue ? `${venue}:${ticker}` : ticker);
@@ -14006,10 +14145,26 @@ var SymbolPicker = class {
14006
14145
  return this.ranked?.result ?? raw;
14007
14146
  }
14008
14147
  computeRows() {
14009
- const TAB_TYPES = { Crypto: ["crypto"], Stocks: ["stock"], ETFs: ["etf"], Forex: ["forex"], Commodities: ["commodity"] };
14148
+ const TAB_TYPES = { Crypto: ["crypto"], Stocks: ["stock"], ETFs: ["etf"], Futures: ["futures", "root"], Forex: ["forex"], Commodities: ["commodity"] };
14010
14149
  const all = this.pool();
14011
14150
  const pool = this.activeTab === "All" ? all : all.filter((s) => TAB_TYPES[this.activeTab]?.includes((s.type ?? "").toLowerCase()) || this.activeTab === "Crypto" && (s.type ?? "").toLowerCase() === "futures");
14012
- return filterSymbols(pool, this.input.value, this.visible, symbolRanking() ? false : TOP_TICKERS);
14151
+ const filtered = filterSymbols(pool, this.input.value, this.visible, symbolRanking() ? false : TOP_TICKERS);
14152
+ this.exhausted = filtered.length < this.visible;
14153
+ const folded = foldGroups(filtered);
14154
+ if (!this.expanded.size) return folded;
14155
+ const keyOf = (s) => `${(s.prefix ?? s.provider ?? "").toLowerCase()}:${s.ticker.toUpperCase()}`;
14156
+ const present = new Set(folded.map(keyOf));
14157
+ const out = [];
14158
+ for (const s of folded) {
14159
+ out.push(s);
14160
+ if (!isGroupRow(s) || !this.expanded.has(groupKeyOf(s))) continue;
14161
+ for (const m of groupMembers(all, s)) {
14162
+ if (present.has(keyOf(m))) continue;
14163
+ present.add(keyOf(m));
14164
+ out.push(m);
14165
+ }
14166
+ }
14167
+ return out;
14013
14168
  }
14014
14169
  refresh() {
14015
14170
  const doc = this.list.ownerDocument;
@@ -14024,19 +14179,20 @@ var SymbolPicker = class {
14024
14179
  this.list.appendChild(empty);
14025
14180
  return;
14026
14181
  }
14027
- for (const s of this.rows) this.list.appendChild(this.rowEl(s));
14182
+ this.rows.forEach((s, i) => this.list.appendChild(this.rowEl(s, i)));
14028
14183
  this.renderHighlight();
14029
14184
  }
14030
14185
  /** Append the page the grown `visible` just uncovered — rows already on screen stay put. */
14031
14186
  grow() {
14032
14187
  const already = this.rows.length;
14033
14188
  this.rows = this.computeRows();
14034
- for (const s of this.rows.slice(already)) this.list.appendChild(this.rowEl(s));
14189
+ this.rows.slice(already).forEach((s, j) => this.list.appendChild(this.rowEl(s, already + j)));
14035
14190
  }
14036
- rowEl(s) {
14191
+ rowEl(s, i) {
14037
14192
  const doc = this.list.ownerDocument;
14038
14193
  const row = doc.createElement("div");
14039
14194
  row.className = "vela-sp-row";
14195
+ row.dataset.i = String(i);
14040
14196
  row.dataset.ticker = s.ticker;
14041
14197
  const venue = s.prefix ?? s.provider;
14042
14198
  if (venue) row.dataset.venue = venue;
@@ -14051,6 +14207,14 @@ var SymbolPicker = class {
14051
14207
  d.textContent = s.description ?? (s.type ?? "");
14052
14208
  main.append(t, d);
14053
14209
  row.append(av, main);
14210
+ if (isGroupRow(s)) {
14211
+ row.dataset.group = "1";
14212
+ const expander = doc.createElement("button");
14213
+ expander.className = "vela-sp-expander";
14214
+ expander.setAttribute("aria-label", "Show contracts");
14215
+ expander.appendChild(iconEl(this.expanded.has(groupKeyOf(s)) ? "chevron-down" : "chevron-right", doc));
14216
+ row.appendChild(expander);
14217
+ } else if (s.group != null && this.expanded.has(groupKeyOf(s))) row.dataset.member = "1";
14054
14218
  if (venue) {
14055
14219
  const badge = doc.createElement("span");
14056
14220
  badge.className = "vela-sp-badge";
@@ -14667,6 +14831,9 @@ var CSS13 = `
14667
14831
  }
14668
14832
  .vela-mb-item:active { background: var(--vela-hover); }
14669
14833
  .vela-mb-item .vela-icon { font-size: 18px; width: 18px; height: 18px; }
14834
+ /* A lit stop (the maximize toggle while something is isolated): the inverse
14835
+ "selected" chip \u2014 white on the dark theme, dark on the light one. */
14836
+ .vela-mb-item.vela-mb-on, .vela-mb-item.vela-mb-on:active { background: var(--vela-selected-bg); color: var(--vela-selected-fg); }
14670
14837
  /* Left-aligned contributed actions get their own stops (the built-in indicators
14671
14838
  slot) \u2014 the wrapper is layout-transparent so each stop flexes like a sibling. */
14672
14839
  .vela-mb-actions { display: contents; }
@@ -14707,9 +14874,11 @@ var MobileBar = class {
14707
14874
  this.actionsHost.className = "vela-mb-actions";
14708
14875
  const onDrawings = opts.onDrawingsClick;
14709
14876
  const drawings = onDrawings ? item("vela-mb-drawings", "Drawings", onDrawings, "pen") : null;
14877
+ const onMaximize = opts.onMaximizeClick;
14878
+ this.maxEl = onMaximize ? item("vela-mb-maximize", "Maximize chart", onMaximize, "maximize") : null;
14710
14879
  const more = item("vela-mb-more", "More", opts.onMoreClick, "kebab");
14711
14880
  const settings = item("vela-mb-settings", "Chart settings", opts.onSettingsClick, "gear");
14712
- this.el.append(this.symbolEl, this.tfEl, ...indicators ? [indicators] : [], this.actionsHost, ...drawings ? [drawings] : [], more, settings);
14881
+ this.el.append(this.symbolEl, this.tfEl, ...indicators ? [indicators] : [], this.actionsHost, ...drawings ? [drawings] : [], ...this.maxEl ? [this.maxEl] : [], more, settings);
14713
14882
  host.appendChild(this.el);
14714
14883
  this.renderActions();
14715
14884
  }
@@ -14741,6 +14910,14 @@ var MobileBar = class {
14741
14910
  setTimeframe(tf) {
14742
14911
  this.tfEl.textContent = timeframeLabel(tf);
14743
14912
  }
14913
+ /** Light the maximize stop while something is isolated (a chart over the grid,
14914
+ * or a maximized pane inside the active chart) — inverse chip + restore glyph. */
14915
+ setMaximizeActive(on) {
14916
+ if (!this.maxEl) return;
14917
+ this.maxEl.classList.toggle("vela-mb-on", on);
14918
+ this.maxEl.setAttribute("aria-label", on ? "Restore layout" : "Maximize chart");
14919
+ this.maxEl.replaceChildren(iconEl(on ? "restore" : "maximize", this.el.ownerDocument));
14920
+ }
14744
14921
  destroy() {
14745
14922
  this.el.remove();
14746
14923
  }
@@ -16703,11 +16880,12 @@ async function resolveIndicators(config, fetchImpl = fetch) {
16703
16880
  }
16704
16881
  return out;
16705
16882
  }
16883
+ var ledgerEntryName = (e) => typeof e === "string" ? e : e.name;
16706
16884
  function indicatorLedger(i) {
16707
16885
  const natives = [...i.present];
16708
16886
  if (i.volumePending && !natives.includes("volume")) natives.push("volume");
16709
16887
  return {
16710
- manifest: i.manifestSettled ? [...i.instanceNames] : [...i.pendingManifest ?? i.instanceNames],
16888
+ manifest: i.manifestSettled ? [...i.instanceEntries] : [...i.pendingManifest ?? i.instanceEntries],
16711
16889
  natives
16712
16890
  };
16713
16891
  }
@@ -16918,7 +17096,7 @@ var DrawingToolbar = class {
16918
17096
  this.root.replaceChildren();
16919
17097
  this.groupCells.clear();
16920
17098
  this.groupIcons.clear();
16921
- this.cursorBtn = this.makeButton(CURSOR_ICON, "Cursor", () => this.onArm(null));
17099
+ this.cursorBtn = this.makeButton(CURSOR_ICON, "Cursor", () => this.onCursorClick());
16922
17100
  this.root.appendChild(this.cursorBtn);
16923
17101
  if (this.def.groups.length > 0) this.root.appendChild(this.divider());
16924
17102
  for (const g of this.def.groups) {
@@ -17022,6 +17200,14 @@ var DrawingToolbar = class {
17022
17200
  this.magnetIcon = icon2;
17023
17201
  return cell;
17024
17202
  }
17203
+ /** Cursor returns to select/idle: an active measure/eraser mode exits through its own
17204
+ * toggle callback (disarming a tool via `onArm(null)` alone can't — the host treats a
17205
+ * null arm as a no-op side effect of entering those modes), then the tool disarms. */
17206
+ onCursorClick() {
17207
+ if (this.measureActive) this.onMeasure();
17208
+ if (this.eraserActive) this.onEraser();
17209
+ this.onArm(null);
17210
+ }
17025
17211
  /** Clicking the icon arms the group's last-used tool (it does NOT open the flyout). */
17026
17212
  onGroupIconClick(group) {
17027
17213
  const type = this.lastUsed.get(group.id) ?? group.tools[0]?.type;
@@ -17633,7 +17819,17 @@ function sanitizeCell(raw) {
17633
17819
  if (c.drawings != null && typeof c.drawings === "object") out.drawings = c.drawings;
17634
17820
  const ind = c.indicators;
17635
17821
  if (ind != null && typeof ind === "object") {
17636
- const manifest = Array.isArray(ind.manifest) ? ind.manifest.filter((n) => typeof n === "string") : [];
17822
+ const manifest = Array.isArray(ind.manifest) ? ind.manifest.flatMap((n) => {
17823
+ if (typeof n === "string") return [n];
17824
+ if (n != null && typeof n === "object" && typeof n.name === "string") {
17825
+ const e = n;
17826
+ const bag = (v) => v != null && typeof v === "object" && !Array.isArray(v) ? v : void 0;
17827
+ const inputs = bag(e.inputs);
17828
+ const props = bag(e.props);
17829
+ return [inputs || props ? { name: e.name, ...inputs ? { inputs } : {}, ...props ? { props } : {} } : e.name];
17830
+ }
17831
+ return [];
17832
+ }) : [];
17637
17833
  const natives = Array.isArray(ind.natives) ? ind.natives.filter((n) => typeof n === "string") : [];
17638
17834
  out.indicators = { manifest, natives };
17639
17835
  }
@@ -17818,6 +18014,12 @@ var IndicatorHandleImpl = class {
17818
18014
  get visible() {
17819
18015
  return this.visibleState;
17820
18016
  }
18017
+ inputValues() {
18018
+ return this.controller.inputValuesOf(this.id);
18019
+ }
18020
+ propValues() {
18021
+ return this.controller.propValuesOf(this.id);
18022
+ }
17821
18023
  setInput(key, value) {
17822
18024
  this.controller.applyInputs(this.id, { [key]: value });
17823
18025
  }
@@ -17957,6 +18159,7 @@ var RUN_EMIT_THROTTLE_MS = 1e3;
17957
18159
  var PREVIEW_BARS = 300;
17958
18160
  var SINGLE_LOAD_BARS = 5e3;
17959
18161
  var CHUNK_BARS = 1e4;
18162
+ var FIRST_PAINT_BARS = 100;
17960
18163
  var GAP_FACTOR = 1.5;
17961
18164
  var HEAL_COOLDOWN_MS = 5e3;
17962
18165
  var EngineOrchestrator = class _EngineOrchestrator {
@@ -18020,6 +18223,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
18020
18223
  /** Invalidates detached async work (backfill loops, in-flight loads, gap heals):
18021
18224
  * bumped by init(), setMarket() and destroy(). */
18022
18225
  this.generation = 0;
18226
+ /** Aborts the in-flight PROGRESSIVE load's source polling on supersession — an
18227
+ * abandoned stream left polling to its own budget starves the browser's per-host
18228
+ * connection pool, and the NEXT symbol's very first fetch with it (measured). */
18229
+ this.progressiveAbort = null;
18023
18230
  /** Awaiters racing a superseded load (setMarket callers) — released on every bump so they never hang. */
18024
18231
  this.supersedeWaiters = [];
18025
18232
  /** `history:complete` fired for the CURRENT load. Each market load re-arms the cycle
@@ -18162,6 +18369,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
18162
18369
  * superseded setMarket awaiters so their promises resolve instead of hanging. */
18163
18370
  bumpGeneration() {
18164
18371
  const gen = ++this.generation;
18372
+ this.progressiveAbort?.abort();
18373
+ this.progressiveAbort = null;
18165
18374
  for (const w of this.supersedeWaiters.splice(0)) w();
18166
18375
  return gen;
18167
18376
  }
@@ -18215,7 +18424,48 @@ var EngineOrchestrator = class _EngineOrchestrator {
18215
18424
  const requested = market.bars ?? 500;
18216
18425
  const initialRange = market.visibleRange;
18217
18426
  const deep = !market.data?.length && initialRange == null && requested > SINGLE_LOAD_BARS;
18218
- if (deep && this.feed.loadRange) {
18427
+ let progressiveServed = false;
18428
+ if (!market.data?.length && initialRange == null && this.feed.loadProgressive) {
18429
+ let painted = false;
18430
+ const paint = (bars, final) => {
18431
+ if (this.generation !== gen || !final && bars.length === 0) return;
18432
+ if (!painted && !final && bars.length < Math.min(requested, FIRST_PAINT_BARS)) return;
18433
+ this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
18434
+ if (!painted && bars.length > 0) {
18435
+ painted = true;
18436
+ if (opts.firstLoad) this.activateBarLayers();
18437
+ if (!final) this.historyState = "backfill";
18438
+ }
18439
+ };
18440
+ const abort = new AbortController();
18441
+ this.progressiveAbort = abort;
18442
+ progressiveServed = await new Promise((firstPaint) => {
18443
+ let signaled = false;
18444
+ const signal = (served) => {
18445
+ if (!signaled) {
18446
+ signaled = true;
18447
+ firstPaint(served);
18448
+ }
18449
+ };
18450
+ abort.signal.addEventListener("abort", () => signal(true), { once: true });
18451
+ this.feed.loadProgressive(market, (bars) => {
18452
+ paint(bars, false);
18453
+ if (painted) signal(true);
18454
+ }, { signal: abort.signal }).then((full) => {
18455
+ if (this.progressiveAbort === abort) this.progressiveAbort = null;
18456
+ if (full == null) return signal(false);
18457
+ if (this.generation !== gen) return signal(true);
18458
+ paint(full, true);
18459
+ this.completeHistory(full.length >= requested ? "depth" : "genesis");
18460
+ signal(true);
18461
+ }).catch(() => {
18462
+ if (this.progressiveAbort === abort) this.progressiveAbort = null;
18463
+ if (this.generation === gen) this.completeHistory("aborted");
18464
+ signal(true);
18465
+ });
18466
+ });
18467
+ }
18468
+ if (progressiveServed) ; else if (deep && this.feed.loadRange) {
18219
18469
  const head = await this.feed.load({ ...market, bars: Math.min(requested, CHUNK_BARS) });
18220
18470
  if (this.generation !== gen) return;
18221
18471
  this.setBarSeries(head);
@@ -18810,6 +19060,15 @@ var EngineOrchestrator = class _EngineOrchestrator {
18810
19060
  record.pendingCause = "inputs";
18811
19061
  if (record.session) record.session.update(record.inputValues);
18812
19062
  else if (record.native && !record.hidden) record.native.instance.setInputs(record.inputValues);
19063
+ this.events.emit("indicator:inputs", { id });
19064
+ }
19065
+ /** IndicatorController: the CURRENT stored input values (defaults merged with edits). */
19066
+ inputValuesOf(id) {
19067
+ return { ...this.registry.get(id)?.inputValues };
19068
+ }
19069
+ /** IndicatorController: the CURRENT declaration-prop overrides. */
19070
+ propValuesOf(id) {
19071
+ return { ...this.registry.get(id)?.propValues };
18813
19072
  }
18814
19073
  /** IndicatorController: re-run an indicator with merged declaration-prop overrides.
18815
19074
  * Same lifecycle as {@link applyInputs} — a prop change replays the whole script.
@@ -18823,6 +19082,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
18823
19082
  if (!record.hidden) this.setLoading(record, true);
18824
19083
  record.pendingCause = "inputs";
18825
19084
  record.session.update(record.inputValues, record.propValues);
19085
+ this.events.emit("indicator:inputs", { id });
18826
19086
  }
18827
19087
  /** IndicatorController: tear down an indicator and (if now empty) its pane. */
18828
19088
  /** Live handles of every indicator on the chart (script + native), insertion order. */
@@ -20044,6 +20304,14 @@ function inputVisible(when, values) {
20044
20304
  const conds = Array.isArray(when) ? when : [when];
20045
20305
  return conds.every((c) => c.anyOf ? c.anyOf.some((x) => x === values[c.key]) : values[c.key] === c.equals);
20046
20306
  }
20307
+ function inputDeltas(schema, values) {
20308
+ const out = {};
20309
+ for (const s of schema) {
20310
+ const v = values[s.key];
20311
+ if (v !== void 0 && JSON.stringify(v) !== JSON.stringify(s.defval)) out[s.key] = v;
20312
+ }
20313
+ return Object.keys(out).length > 0 ? out : void 0;
20314
+ }
20047
20315
 
20048
20316
  // src/renderers/shared/IndicatorInputsDialog.ts
20049
20317
  var PROPS_TAB = "Properties";
@@ -20958,6 +21226,9 @@ var CLOSE_SVG = iconAt("close", LEGEND_ICON_PX2);
20958
21226
  var FOLD_SVG = iconAt("chevron-up", LEGEND_ICON_PX2);
20959
21227
  var UNFOLD_SVG = iconAt("chevron-down", LEGEND_ICON_PX2);
20960
21228
  var OVERVIEW_SVG = iconAt("objects", LEGEND_ICON_PX2);
21229
+ function legendCalloutsDisplay(open2, hasCallouts) {
21230
+ return !open2 && hasCallouts ? "inline-flex" : "none";
21231
+ }
20961
21232
  var InputsUI = class {
20962
21233
  constructor(container, theme, paneBoundsOf) {
20963
21234
  this.container = container;
@@ -21161,7 +21432,7 @@ var InputsUI = class {
21161
21432
  row.callouts = [];
21162
21433
  row.calloutsEl.replaceChildren();
21163
21434
  const views = this.legendCallouts?.(row.id) ?? [];
21164
- row.calloutsEl.style.display = views.length > 0 ? "inline-flex" : "none";
21435
+ row.calloutsEl.style.display = legendCalloutsDisplay(row.highlighted, views.length > 0);
21165
21436
  for (const view of views) {
21166
21437
  const bubble = new CalloutBubble({
21167
21438
  icon: view.icon,
@@ -21645,11 +21916,11 @@ var InputsUI = class {
21645
21916
  row.controlsEl.style.display = open2 || row.hidden ? "inline-flex" : "none";
21646
21917
  if (open2) {
21647
21918
  row.el.appendChild(row.statusEl);
21648
- row.el.appendChild(row.calloutsEl);
21919
+ for (const bubble of row.callouts) bubble.hidePanel();
21649
21920
  } else {
21650
21921
  row.el.insertBefore(row.statusEl, row.valuesEl);
21651
- row.el.insertBefore(row.calloutsEl, row.statusEl);
21652
21922
  }
21923
+ row.calloutsEl.style.display = legendCalloutsDisplay(open2, row.callouts.length > 0);
21653
21924
  for (const child of Array.from(row.controlsEl.children)) {
21654
21925
  if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
21655
21926
  if (child === row.extrasEl) {
@@ -21739,7 +22010,7 @@ var ICONS = {
21739
22010
  };
21740
22011
  var STYLE_ID21 = "vela-pane-controls";
21741
22012
  var ICON_PX = 12;
21742
- var CLUSTER_PILL = "rgba(0,0,0,0.28)";
22013
+ var CLUSTER_PILL = "rgba(0,0,0,0.65)";
21743
22014
  function ensureStyles3() {
21744
22015
  if (typeof document === "undefined" || document.getElementById(STYLE_ID21)) return;
21745
22016
  const st = document.createElement("style");
@@ -21759,8 +22030,12 @@ var PaneControls = class {
21759
22030
  this.deps = deps;
21760
22031
  this.clusters = /* @__PURE__ */ new Map();
21761
22032
  this.hoverPaneId = null;
22033
+ /** Mobile: hover clusters are meaningless without a cursor — suppressed; a
22034
+ * collapsed pane's standalone expand chip stays (the only way back up). */
22035
+ this.suspended = false;
21762
22036
  /** Reveal the cluster for the pane under the cursor, resolved from the pointer's y in the plot. */
21763
22037
  this.onPlotMove = (e) => {
22038
+ if (this.suspended) return;
21764
22039
  const rect = this.plot.getBoundingClientRect();
21765
22040
  const y = e.clientY - rect.top;
21766
22041
  let hit = null;
@@ -21832,7 +22107,12 @@ var PaneControls = class {
21832
22107
  }
21833
22108
  if (p.count > 1) {
21834
22109
  cluster.appendChild(
21835
- this.button(p.maximized ? ICONS.restore : ICONS.maximize, p.maximized ? "Restore pane" : "Maximize pane", false, () => this.deps.onToggleMaximize(p.id), { role: "maximize" })
22110
+ this.button(p.maximized ? ICONS.restore : ICONS.maximize, p.maximized ? "Restore pane" : "Maximize pane", false, () => this.deps.onToggleMaximize(p.id), {
22111
+ role: "maximize",
22112
+ // Same inverse-chip treatment as the collapsed pane's expand toggle: the
22113
+ // maximized state must read as an active state, not just a swapped glyph.
22114
+ selected: p.maximized
22115
+ })
21836
22116
  );
21837
22117
  }
21838
22118
  }
@@ -21869,17 +22149,18 @@ var PaneControls = class {
21869
22149
  }
21870
22150
  const hovered = id === this.hoverPaneId;
21871
22151
  const hasButtons = cluster.children.length > 0;
21872
- const visible = hasButtons && (hovered || p.collapsed) && p.height > 8;
22152
+ const stateChipRole = p.collapsed ? "collapse" : !this.suspended && p.maximized ? "maximize" : null;
22153
+ const visible = hasButtons && (hovered || stateChipRole != null) && p.height > 8;
21873
22154
  cluster.style.right = `${rightPx}px`;
21874
22155
  cluster.style.top = p.collapsed ? `${p.top + Math.max(1, Math.round((p.height - 24) / 2))}px` : `${p.top + 4}px`;
21875
22156
  cluster.style.display = visible ? "flex" : "none";
21876
22157
  if (!visible) continue;
21877
- const soloExpand = p.collapsed && !hovered;
21878
- cluster.style.background = soloExpand ? "transparent" : CLUSTER_PILL;
22158
+ const soloChip = stateChipRole != null && !hovered;
22159
+ cluster.style.background = soloChip ? "transparent" : CLUSTER_PILL;
21879
22160
  for (const child of cluster.children) {
21880
22161
  const btn2 = child;
21881
22162
  btn2.style.display = "inline-flex";
21882
- btn2.style.visibility = soloExpand && btn2.dataset.role !== "collapse" ? "hidden" : "visible";
22163
+ btn2.style.visibility = soloChip && btn2.dataset.role !== stateChipRole ? "hidden" : "visible";
21883
22164
  }
21884
22165
  }
21885
22166
  }
@@ -21889,6 +22170,14 @@ var PaneControls = class {
21889
22170
  this.hoverPaneId = paneId;
21890
22171
  this.reposition();
21891
22172
  }
22173
+ /** Mobile suppression: no hover clusters (touch has no cursor; the shell's own
22174
+ * chrome covers maximize), while collapsed panes keep their expand chips. */
22175
+ setSuspended(on) {
22176
+ if (on === this.suspended) return;
22177
+ this.suspended = on;
22178
+ if (on) this.hoverPaneId = null;
22179
+ this.reposition();
22180
+ }
21892
22181
  destroy() {
21893
22182
  this.plot.removeEventListener("pointermove", this.onPlotMove);
21894
22183
  this.plot.removeEventListener("pointerleave", this.onPlotLeave);
@@ -22039,160 +22328,6 @@ var AxisScaleButtons = class {
22039
22328
  }
22040
22329
  };
22041
22330
 
22042
- // src/renderers/shared/TableOverlay.ts
22043
- var SIZE_PX3 = {
22044
- auto: 13,
22045
- tiny: 10,
22046
- small: 11,
22047
- normal: 13,
22048
- large: 16,
22049
- huge: 20
22050
- };
22051
- function fontPxOf(size) {
22052
- if (typeof size === "number") return size > 0 ? size : SIZE_PX3.auto;
22053
- return SIZE_PX3[size] ?? SIZE_PX3.auto;
22054
- }
22055
- function tableHasContent(t) {
22056
- return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
22057
- }
22058
- function mergeRenderPlan(t) {
22059
- const span = /* @__PURE__ */ new Map();
22060
- const omit = /* @__PURE__ */ new Set();
22061
- for (const m of t.merges) {
22062
- span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
22063
- for (let r = m.startRow; r <= m.endRow; r += 1) {
22064
- for (let c = m.startCol; c <= m.endCol; c += 1) {
22065
- if (r !== m.startRow || c !== m.startCol) omit.add(`${r}:${c}`);
22066
- }
22067
- }
22068
- }
22069
- for (let r = 0; r < t.rows; r += 1) {
22070
- for (let c = 0; c < t.columns; c += 1) {
22071
- if (t.cells[r]?.[c]?.merged && !span.has(`${r}:${c}`)) omit.add(`${r}:${c}`);
22072
- }
22073
- }
22074
- for (const key of span.keys()) omit.delete(key);
22075
- return { span, omit };
22076
- }
22077
- var TableOverlay = class {
22078
- constructor(container, theme, paneBounds) {
22079
- this.container = container;
22080
- this.theme = theme;
22081
- this.paneBounds = paneBounds;
22082
- this.lastTables = [];
22083
- if (getComputedStyle(container).position === "static") container.style.position = "relative";
22084
- this.root = document.createElement("div");
22085
- Object.assign(this.root.style, {
22086
- position: "absolute",
22087
- inset: "0",
22088
- pointerEvents: "none",
22089
- overflow: "hidden",
22090
- zIndex: "3"
22091
- });
22092
- container.appendChild(this.root);
22093
- }
22094
- update(tables) {
22095
- this.lastTables = tables;
22096
- this.root.replaceChildren();
22097
- for (const t of tables) {
22098
- if (tableHasContent(t)) this.root.appendChild(this.renderTable(t));
22099
- }
22100
- }
22101
- /** Re-render at the current pane geometry — after layout settles or on resize. */
22102
- reposition() {
22103
- if (this.root.isConnected) this.update(this.lastTables);
22104
- }
22105
- /** Show/hide the whole overlay. Tables anchor to pane corners, not to bars, so unlike the
22106
- * series content they DON'T vanish with an emptied chart — the loading state hides them. */
22107
- setVisible(visible) {
22108
- this.root.style.display = visible ? "" : "none";
22109
- }
22110
- destroy() {
22111
- this.root.remove();
22112
- }
22113
- renderTable(t) {
22114
- const b = this.paneBounds(t.paneId);
22115
- const wrap = document.createElement("div");
22116
- wrap.style.position = "absolute";
22117
- if (t.frameColor && t.frameWidth > 0) wrap.style.border = `${t.frameWidth}px solid ${t.frameColor}`;
22118
- this.anchor(wrap, t.position, b);
22119
- const table = document.createElement("table");
22120
- Object.assign(table.style, {
22121
- borderCollapse: "collapse",
22122
- background: t.bgColor ?? "transparent",
22123
- fontFamily: this.theme.fontFamily || "sans-serif",
22124
- border: "none",
22125
- tableLayout: "auto",
22126
- // Re-enable pointer events on the table only (root is none) so cell tooltips work.
22127
- pointerEvents: "auto"
22128
- });
22129
- const { span, omit } = mergeRenderPlan(t);
22130
- const plotW = Math.max(0, (this.root.clientWidth || this.container.clientWidth) - b.rightAxis);
22131
- const paneH = b.height;
22132
- const cellBorder = t.borderColor && t.borderWidth > 0 ? `${t.borderWidth}px solid ${t.borderColor}` : "none";
22133
- for (let r = 0; r < t.rows; r += 1) {
22134
- const tr = document.createElement("tr");
22135
- for (let c = 0; c < t.columns; c += 1) {
22136
- if (omit.has(`${r}:${c}`)) continue;
22137
- const cell = t.cells[r]?.[c] ?? null;
22138
- const td = document.createElement("td");
22139
- if (cell === null) {
22140
- Object.assign(td.style, { padding: "0", border: "none" });
22141
- tr.appendChild(td);
22142
- continue;
22143
- }
22144
- const sp = span.get(`${r}:${c}`);
22145
- if (sp) {
22146
- if (sp.cs > 1) td.colSpan = sp.cs;
22147
- if (sp.rs > 1) td.rowSpan = sp.rs;
22148
- }
22149
- Object.assign(td.style, {
22150
- border: cellBorder,
22151
- padding: "2px 6px",
22152
- background: cell.bgColor ?? "transparent",
22153
- color: cell.textColor ?? this.theme.textColor,
22154
- textAlign: cell.hAlign,
22155
- verticalAlign: cell.vAlign === "top" ? "top" : cell.vAlign === "bottom" ? "bottom" : "middle",
22156
- fontSize: `${fontPxOf(cell.textSize)}px`,
22157
- fontFamily: cell.fontFamily === "monospace" ? "monospace" : "inherit",
22158
- fontWeight: cell.bold ? "bold" : "normal",
22159
- fontStyle: cell.italic ? "italic" : "normal",
22160
- // Pine cell text never wraps; `\n` still breaks lines. Wrapping
22161
- // used to collapse unicode sparklines and ━━━ dividers.
22162
- whiteSpace: "pre"
22163
- });
22164
- if (cell.width) td.style.width = `${cell.width / 100 * plotW}px`;
22165
- if (cell.height) td.style.height = `${cell.height / 100 * paneH}px`;
22166
- if (cell.tooltip) td.title = cell.tooltip;
22167
- td.textContent = cell.text ?? "";
22168
- tr.appendChild(td);
22169
- }
22170
- table.appendChild(tr);
22171
- }
22172
- wrap.appendChild(table);
22173
- return wrap;
22174
- }
22175
- /**
22176
- * Position the wrapper at a Pine `position.*` corner/edge of the table's PANE
22177
- * (not the whole chart), inset past the right price axis so it never overlaps
22178
- * the Y-axis labels.
22179
- */
22180
- anchor(el, position, b) {
22181
- const m = 6;
22182
- const containerH = this.root.clientHeight || this.container.clientHeight;
22183
- const paneBottom = b.top + b.height;
22184
- if (position.startsWith("top")) el.style.top = `${b.top + m}px`;
22185
- else if (position.startsWith("bottom")) el.style.bottom = `${Math.max(0, containerH - paneBottom) + m}px`;
22186
- else el.style.top = `${b.top + b.height / 2}px`;
22187
- if (position.endsWith("left")) el.style.left = `${m}px`;
22188
- else if (position.endsWith("right")) el.style.right = `${b.rightAxis + m}px`;
22189
- else el.style.left = `calc(50% - ${b.rightAxis / 2}px)`;
22190
- const tx = position.endsWith("center") ? "-50%" : "0";
22191
- const ty = position.startsWith("middle") ? "-50%" : "0";
22192
- if (tx !== "0" || ty !== "0") el.style.transform = `translate(${tx}, ${ty})`;
22193
- }
22194
- };
22195
-
22196
22331
  // src/renderers/native/capabilities.ts
22197
22332
  var NATIVE_CAPABILITIES = {
22198
22333
  panes: true,
@@ -22213,7 +22348,7 @@ var NATIVE_CAPABILITIES = {
22213
22348
  drawingDepth: true,
22214
22349
  // drawings share the series' z space (backend-composited interleave layers)
22215
22350
  tables: true,
22216
- // reuses the DOM TableOverlay
22351
+ // canvas-painted into the owning indicator's interleave slice
22217
22352
  trades: true,
22218
22353
  // strategy order-fill markers (arrows + labels + fill-price ticks)
22219
22354
  inputsUI: true
@@ -22244,6 +22379,9 @@ function candleTier(spacing) {
22244
22379
  if (spacing < CANDLE_BODY_MIN_SPACING) return "wick";
22245
22380
  return "full";
22246
22381
  }
22382
+ function snapY(yCss, dpr) {
22383
+ return Math.round(yCss * dpr) / dpr;
22384
+ }
22247
22385
  function candleGeometry(xCss, spacing, dpr, bodyScale = 1) {
22248
22386
  const wickDev = Math.max(1, Math.round(wickWidth(spacing) * dpr));
22249
22387
  const wickLeftDev = Math.round(xCss * dpr - wickDev / 2);
@@ -22767,11 +22905,9 @@ var WebGL2Backend = class {
22767
22905
  };
22768
22906
  b.alpha = this.modelAlpha;
22769
22907
  for (const m of models) for (const bgSpan of m.backgrounds) if (bgSpan.overlay !== true) this.emitBackground(b, bgSpan, pane, coords);
22770
- for (const m of models) for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, effPane(m), coords, i0, i1, scene.offsetOf(m.id));
22771
22908
  if (isPrice) {
22772
22909
  for (const m of scene.indicators.values()) {
22773
22910
  for (const bgSpan of m.backgrounds) if (bgSpan.overlay === true) this.emitBackground(b, bgSpan, pane, coords);
22774
- for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, i1, scene.offsetOf(m.id));
22775
22911
  }
22776
22912
  }
22777
22913
  const drawCandles = isPrice && !scene.candlesHidden;
@@ -22787,6 +22923,7 @@ var WebGL2Backend = class {
22787
22923
  b.alpha = this.modelAlpha;
22788
22924
  const off = scene.offsetOf(m.id);
22789
22925
  const mp = effPane(m);
22926
+ for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, mp, coords, i0, i1, off);
22790
22927
  for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
22791
22928
  }
22792
22929
  if (drawCandles && !candleDrawn) {
@@ -22794,14 +22931,18 @@ var WebGL2Backend = class {
22794
22931
  b.alpha = this.candleStructureAlpha;
22795
22932
  this.emitPriceSeries(b, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
22796
22933
  }
22797
- drawSlicesUpTo(Infinity);
22798
22934
  b.alpha = this.modelAlpha;
22799
22935
  if (isPrice) {
22936
+ for (const m of scene.indicators.values()) {
22937
+ const off = scene.offsetOf(m.id);
22938
+ for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, i1, off);
22939
+ }
22800
22940
  for (const m of scene.indicators.values()) {
22801
22941
  const off = scene.offsetOf(m.id);
22802
22942
  for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0, i1, theme, off);
22803
22943
  }
22804
22944
  }
22945
+ drawSlicesUpTo(Infinity);
22805
22946
  for (const m of models) {
22806
22947
  const mp = effPane(m);
22807
22948
  for (const pl of m.priceLines) this.emitHline(b, pl, mp, coords, dataW, theme);
@@ -23196,12 +23337,12 @@ var WebGL2Backend = class {
23196
23337
  if (drawBody) {
23197
23338
  const oY = coords.priceToY(bar.open, pane.scale, pane.bounds);
23198
23339
  const cY = coords.priceToY(bar.close, pane.scale, pane.bounds);
23199
- bodyTop = Math.min(oY, cY);
23200
- bodyH = Math.max(1, Math.abs(cY - oY));
23340
+ bodyTop = snapY(Math.min(oY, cY), coords.dpr);
23341
+ bodyH = Math.max(1 / coords.dpr, snapY(Math.max(oY, cY), coords.dpr) - bodyTop);
23201
23342
  }
23202
23343
  if (cs.wickVisible) {
23203
- const hY = coords.priceToY(bar.high, pane.scale, pane.bounds);
23204
- const lY = coords.priceToY(bar.low, pane.scale, pane.bounds);
23344
+ const hY = snapY(coords.priceToY(bar.high, pane.scale, pane.bounds), coords.dpr);
23345
+ const lY = snapY(coords.priceToY(bar.low, pane.scale, pane.bounds), coords.dpr);
23205
23346
  b.alpha = this.candleStructureAlpha;
23206
23347
  const wCol = parseColor((isUp ? cs.wickUpColor : cs.wickDownColor) ?? (drawBody ? dir : bodyColorStr));
23207
23348
  if (drawBody) {
@@ -23758,6 +23899,7 @@ var DOUBLE_TAP_MS = 350;
23758
23899
  var DOUBLE_TAP_SLOP = 30;
23759
23900
  var TIME_SCALE_K = 4e-3;
23760
23901
  var WHEEL_ZOOM_K = 4e-3;
23902
+ var WHEEL_PRICE_DRAG_PX = 0.25;
23761
23903
  function wheelZoomAnchor(coords, cursorX, rightEdge) {
23762
23904
  if (rightEdge) return { logical: coords.rightEdgeLogical, x: coords.width };
23763
23905
  return { logical: coords.xToLogical(cursorX), x: cursorX };
@@ -23888,7 +24030,7 @@ var InputController = class {
23888
24030
  this.capture(e.pointerId);
23889
24031
  return;
23890
24032
  }
23891
- if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y)) {
24033
+ if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y, this.snapMode(e))) {
23892
24034
  this.region = "drawing";
23893
24035
  this.capture(e.pointerId);
23894
24036
  return;
@@ -24002,7 +24144,7 @@ var InputController = class {
24002
24144
  const wasTouch = e.pointerType === "touch";
24003
24145
  const tapRelease = this.dragging && !this.moved && (!wasTouch || Math.hypot(x - this.startX, y - this.startY) <= TOUCH_TAP_SLOP);
24004
24146
  if (this.dragging && this.region === "drawing") {
24005
- this.deps.drawingsPointerUp?.(x, y);
24147
+ this.deps.drawingsPointerUp?.(x, y, this.snapMode(e));
24006
24148
  } else if (tapRelease && this.region === "data") {
24007
24149
  this.deps.onClick(x, y);
24008
24150
  } else if (this.dragging && this.region === "data") {
@@ -24023,7 +24165,7 @@ var InputController = class {
24023
24165
  if (e.pointerType === "touch") this.touches.delete(e.pointerId);
24024
24166
  this.cancelLongPress();
24025
24167
  if (!this.dragging) return;
24026
- if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY);
24168
+ if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY, this.snapMode(e));
24027
24169
  if (this.region === "crosshair" || e.pointerType === "touch") this.deps.onPointerMove(null, null);
24028
24170
  this.endGesture(e);
24029
24171
  };
@@ -24039,6 +24181,12 @@ var InputController = class {
24039
24181
  this.onWheel = (e) => {
24040
24182
  e.preventDefault();
24041
24183
  this.deps.drawingsClearTransient?.();
24184
+ const { x, y } = this.local(e);
24185
+ if (this.regionAt(x, y) === "price" && e.deltaY !== 0) {
24186
+ this.deps.beginPriceScale(x, y);
24187
+ this.deps.priceScaleBy(e.deltaY * WHEEL_PRICE_DRAG_PX);
24188
+ return;
24189
+ }
24042
24190
  const coords = this.deps.getCoords();
24043
24191
  const vp = coords.getViewport();
24044
24192
  const pan = wheelPanDelta(e.deltaX, e.deltaY, e.shiftKey);
@@ -24046,9 +24194,8 @@ var InputController = class {
24046
24194
  this.deps.apply({ barSpacing: vp.barSpacing, rightOffset: wheelPanRightOffset(vp.rightOffset, pan, coords.pxPerBar()) });
24047
24195
  return;
24048
24196
  }
24049
- const cursorX = this.local(e).x;
24050
24197
  const rightEdge = this.rightEdgeZoom && !(e.ctrlKey || e.metaKey);
24051
- const anchor = wheelZoomAnchor(coords, cursorX, rightEdge);
24198
+ const anchor = wheelZoomAnchor(coords, x, rightEdge);
24052
24199
  const target = clampBarSpacing(vp.barSpacing * Math.exp(-e.deltaY * WHEEL_ZOOM_K));
24053
24200
  this.deps.zoomTo(target, anchor.logical, anchor.x);
24054
24201
  };
@@ -24534,9 +24681,10 @@ var SceneGraph = class {
24534
24681
  * so each indicator arrives behind the candles (and behind older indicators);
24535
24682
  * `setIndicatorZ`/`bringToFront`/`sendToBack` change it. */
24536
24683
  this.seriesZ = /* @__PURE__ */ new Map();
24537
- /** Per-pane raster layers of user drawings interleaved into the series stack each is a
24684
+ /** Per-pane raster layers of drawings interleaved into the series stack (each
24685
+ * indicator's Pine drawings at its model's z, plus in-stack user drawings) — each a
24538
24686
  * prepainted canvas the backend composites just before the series carrying `beforeZ`.
24539
- * Rebuilt by the renderer per data frame; empty when every drawing sits over the stack. */
24687
+ * Rebuilt by the renderer per data frame. */
24540
24688
  this.drawingSlices = /* @__PURE__ */ new Map();
24541
24689
  /** Per-model index offset: the chart bar index of the model's `anchorTime` — its
24542
24690
  * index-aligned payloads (dense series arrays, `bar_index` drawings) count from that
@@ -24771,11 +24919,9 @@ var Canvas2dBackend = class {
24771
24919
  };
24772
24920
  ctx.globalAlpha = this.modelAlpha;
24773
24921
  for (const m of models) for (const bg of m.backgrounds) if (bg.overlay !== true) this.drawBackground(ctx, bg, pane, coords);
24774
- for (const m of models) for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, effPane(m), coords, i0, i1, scene.offsetOf(m.id));
24775
24922
  if (isPrice) {
24776
24923
  for (const m of scene.indicators.values()) {
24777
24924
  for (const bg of m.backgrounds) if (bg.overlay === true) this.drawBackground(ctx, bg, pane, coords);
24778
- for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, scene.offsetOf(m.id));
24779
24925
  }
24780
24926
  }
24781
24927
  const slices = scene.drawingSlices.get(pane.id) ?? [];
@@ -24799,6 +24945,7 @@ var Canvas2dBackend = class {
24799
24945
  ctx.globalAlpha = this.modelAlpha;
24800
24946
  const off = scene.offsetOf(m.id);
24801
24947
  const mp = effPane(m);
24948
+ for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, mp, coords, i0, i1, off);
24802
24949
  for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
24803
24950
  }
24804
24951
  if (drawCandles && !candleDrawn) {
@@ -24806,14 +24953,18 @@ var Canvas2dBackend = class {
24806
24953
  ctx.globalAlpha = this.candleStructureAlpha;
24807
24954
  this.drawPriceSeries(ctx, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
24808
24955
  }
24809
- drawSlicesUpTo(Infinity);
24810
24956
  if (isPrice) {
24811
24957
  ctx.globalAlpha = this.modelAlpha;
24958
+ for (const m of scene.indicators.values()) {
24959
+ const off = scene.offsetOf(m.id);
24960
+ for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, off);
24961
+ }
24812
24962
  for (const m of scene.indicators.values()) {
24813
24963
  const off = scene.offsetOf(m.id);
24814
24964
  for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0, i1, theme, off);
24815
24965
  }
24816
24966
  }
24967
+ drawSlicesUpTo(Infinity);
24817
24968
  ctx.globalAlpha = this.modelAlpha;
24818
24969
  for (const m of models) {
24819
24970
  const mp = effPane(m);
@@ -25057,13 +25208,13 @@ var Canvas2dBackend = class {
25057
25208
  if (drawBody) {
25058
25209
  const oY = coords.priceToY(b.open, pane.scale, pane.bounds);
25059
25210
  const cY = coords.priceToY(b.close, pane.scale, pane.bounds);
25060
- top = Math.min(oY, cY);
25061
- bodyH = Math.max(1, Math.abs(cY - oY));
25211
+ top = snapY(Math.min(oY, cY), coords.dpr);
25212
+ bodyH = Math.max(1 / coords.dpr, snapY(Math.max(oY, cY), coords.dpr) - top);
25062
25213
  }
25063
25214
  if (cs.wickVisible) {
25064
25215
  const wick = (up ? cs.wickUpColor : cs.wickDownColor) ?? (drawBody ? dir : color);
25065
- const hY = coords.priceToY(b.high, pane.scale, pane.bounds);
25066
- const lY = coords.priceToY(b.low, pane.scale, pane.bounds);
25216
+ const hY = snapY(coords.priceToY(b.high, pane.scale, pane.bounds), coords.dpr);
25217
+ const lY = snapY(coords.priceToY(b.low, pane.scale, pane.bounds), coords.dpr);
25067
25218
  ctx.globalAlpha = this.candleStructureAlpha;
25068
25219
  ctx.strokeStyle = wick;
25069
25220
  ctx.lineWidth = g.wickW;
@@ -25510,9 +25661,31 @@ function autoFontSize(lines, boxW, boxH, bold) {
25510
25661
 
25511
25662
  // src/renderers/shared/DrawingSceneRenderer.ts
25512
25663
  var EMPTY_DRAWING_SET = { lines: [], boxes: [], labels: [], polylines: [], linefills: [] };
25664
+ function modelDrawingSet(m, overlay) {
25665
+ const want = (d) => Boolean(d.overlay) === overlay;
25666
+ return {
25667
+ lines: (m.lines ?? []).filter(want),
25668
+ boxes: (m.boxes ?? []).filter(want),
25669
+ labels: (m.labels ?? []).filter(want),
25670
+ polylines: (m.polylines ?? []).filter(want),
25671
+ linefills: (m.linefills ?? []).filter(want)
25672
+ };
25673
+ }
25674
+ function drawingSetEmpty(s) {
25675
+ return !s.lines.length && !s.boxes.length && !s.labels.length && !s.polylines.length && !s.linefills.length;
25676
+ }
25513
25677
  function fontSizePx(size) {
25514
25678
  return size === "auto" ? 12 : namedFontSize(size);
25515
25679
  }
25680
+ function lineCoversWindow(a, b, extend, lo, hi) {
25681
+ const minX = Math.min(a, b);
25682
+ const maxX = Math.max(a, b);
25683
+ if (a === b) return a >= lo && a <= hi;
25684
+ if (extend === "both") return true;
25685
+ if (extend === "left") return maxX >= lo;
25686
+ if (extend === "right") return minX <= hi;
25687
+ return maxX >= lo && minX <= hi;
25688
+ }
25516
25689
  var DrawingSceneRenderer = class {
25517
25690
  constructor(deps, set = EMPTY_DRAWING_SET) {
25518
25691
  this.deps = deps;
@@ -25577,7 +25750,7 @@ var DrawingSceneRenderer = class {
25577
25750
  };
25578
25751
  for (const ln of this.set.lines) {
25579
25752
  if (ln.invisible) continue;
25580
- if (!visible(this.logicalOf(ln.xloc, ln.x1), this.logicalOf(ln.xloc, ln.x2), ln.extend)) continue;
25753
+ if (!lineCoversWindow(this.logicalOf(ln.xloc, ln.x1), this.logicalOf(ln.xloc, ln.x2), ln.extend, lo, hi)) continue;
25581
25754
  fold(ln.y1);
25582
25755
  fold(ln.y2);
25583
25756
  }
@@ -26362,10 +26535,8 @@ var ChromeRenderer = class {
26362
26535
  this.ctx = null;
26363
26536
  // The color for axis tick labels — the host-passed surface text, set each frame in render().
26364
26537
  this.axisTextColor = DARK_THEME.textColor;
26365
- // Shared Pine-drawing renderer (line/box/label/polyline/linefill); widthCache persists.
26538
+ // Shared Pine-drawing renderer, used here for autoscale geometry only; widthCache persists.
26366
26539
  this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
26367
- // Tooltip hit-rects of every label drawn this frame, in plot coords (rebuilt per render).
26368
- this.labelTips = [];
26369
26540
  }
26370
26541
  mount(canvas) {
26371
26542
  this.canvas = canvas;
@@ -26389,8 +26560,8 @@ var ChromeRenderer = class {
26389
26560
  */
26390
26561
  paneDrawingsRange(ownModels, scene, isPricePane, vr) {
26391
26562
  let dr = null;
26392
- for (const m of ownModels) dr = unionRange(dr, this.drawingsRange(this.ownDrawings(m), vr, scene.offsetOf(m.id)));
26393
- if (isPricePane) for (const m of scene.indicators.values()) dr = unionRange(dr, this.drawingsRange(this.overlayDrawings(m), vr, scene.offsetOf(m.id)));
26563
+ for (const m of ownModels) dr = unionRange(dr, this.drawingsRange(modelDrawingSet(m, false), vr, scene.offsetOf(m.id)));
26564
+ if (isPricePane) for (const m of scene.indicators.values()) dr = unionRange(dr, this.drawingsRange(modelDrawingSet(m, true), vr, scene.offsetOf(m.id)));
26394
26565
  return dr;
26395
26566
  }
26396
26567
  /** Clear the chrome canvas and draw drawings + axes + current-price line.
@@ -26407,7 +26578,6 @@ var ChromeRenderer = class {
26407
26578
  const dataW = coords.width;
26408
26579
  const dataH = coords.height;
26409
26580
  this.axisTextColor = surface?.textColor ?? theme.textColor;
26410
- this.labelTips = [];
26411
26581
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
26412
26582
  ctx.clearRect(0, 0, fullW, fullH);
26413
26583
  if (surface && (fullW > dataW || fullH > dataH)) {
@@ -26423,17 +26593,6 @@ var ChromeRenderer = class {
26423
26593
  return;
26424
26594
  }
26425
26595
  const pricePane = panes.find((p) => p.kind === "price") ?? null;
26426
- for (const pane of panes) {
26427
- if (pane.collapsed) continue;
26428
- for (const m of scene.indicatorsForPane(pane.id)) {
26429
- const sc = scene.scaleFor(m, pane);
26430
- const mp = sc === pane.scale ? pane : { ...pane, scale: sc };
26431
- this.renderDrawings(ctx, coords, this.ownDrawings(m), mp, dataW, scene.offsetOf(m.id));
26432
- }
26433
- }
26434
- if (pricePane) {
26435
- for (const m of scene.indicators.values()) this.renderDrawings(ctx, coords, this.overlayDrawings(m), pricePane, dataW, scene.offsetOf(m.id));
26436
- }
26437
26596
  if (pricePane && !pricePane.collapsed && scene.tradeMarkers.visible) {
26438
26597
  for (const m of scene.indicators.values()) {
26439
26598
  if (m.trades?.length) this.renderTrades(ctx, coords, scene, theme, m.trades, pricePane, dataW);
@@ -26449,25 +26608,6 @@ var ChromeRenderer = class {
26449
26608
  this.canvas = null;
26450
26609
  this.ctx = null;
26451
26610
  }
26452
- // ── Pine-drawing helpers (own vs force_overlay routing) ──
26453
- ownDrawings(m) {
26454
- return {
26455
- lines: (m.lines ?? []).filter((d) => !d.overlay),
26456
- boxes: (m.boxes ?? []).filter((d) => !d.overlay),
26457
- labels: (m.labels ?? []).filter((d) => !d.overlay),
26458
- polylines: (m.polylines ?? []).filter((d) => !d.overlay),
26459
- linefills: (m.linefills ?? []).filter((d) => !d.overlay)
26460
- };
26461
- }
26462
- overlayDrawings(m) {
26463
- return {
26464
- lines: (m.lines ?? []).filter((d) => d.overlay),
26465
- boxes: (m.boxes ?? []).filter((d) => d.overlay),
26466
- labels: (m.labels ?? []).filter((d) => d.overlay),
26467
- polylines: (m.polylines ?? []).filter((d) => d.overlay),
26468
- linefills: (m.linefills ?? []).filter((d) => d.overlay)
26469
- };
26470
- }
26471
26611
  drawingsRange(set, vr, indexOffset = 0) {
26472
26612
  this.drawScene.setSet(set, indexOffset);
26473
26613
  if (this.drawScene.isEmpty()) return null;
@@ -26501,34 +26641,6 @@ var ChromeRenderer = class {
26501
26641
  );
26502
26642
  ctx.restore();
26503
26643
  }
26504
- renderDrawings(ctx, coords, set, pane, dataW, indexOffset = 0) {
26505
- this.drawScene.setSet(set, indexOffset);
26506
- if (this.drawScene.isEmpty()) return;
26507
- ctx.save();
26508
- ctx.translate(0, pane.bounds.top);
26509
- ctx.beginPath();
26510
- ctx.rect(0, 0, dataW, pane.bounds.height);
26511
- ctx.clip();
26512
- this.drawScene.render(
26513
- ctx,
26514
- dataW,
26515
- pane.bounds.height,
26516
- (l) => coords.logicalToX(l),
26517
- (price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
26518
- );
26519
- ctx.restore();
26520
- for (const r of this.drawScene.labelTipRegions()) {
26521
- this.labelTips.push({ ...r, top: r.top + pane.bounds.top, bottom: r.bottom + pane.bounds.top });
26522
- }
26523
- }
26524
- /** Tooltip of the topmost label under a plot-space point, or null. Fed by the last render. */
26525
- labelTooltipAt(x, y) {
26526
- for (let i = this.labelTips.length - 1; i >= 0; i -= 1) {
26527
- const r = this.labelTips[i];
26528
- if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return r.text;
26529
- }
26530
- return null;
26531
- }
26532
26644
  // ── axes ──
26533
26645
  drawPriceAxes(ctx, scene, coords, theme, dataW, panes) {
26534
26646
  ctx.strokeStyle = scene.style.borderColor ?? theme.borderColor;
@@ -29488,6 +29600,22 @@ var DrawingInteraction = class {
29488
29600
  this.snapAt = changed ? { point: snapped, paneId } : null;
29489
29601
  return snapped;
29490
29602
  }
29603
+ /**
29604
+ * Resolve a cursor pixel through the magnet and return the snapped pixel — the same
29605
+ * conversion drawing placement uses. Updates the snap-ring marker. The measure
29606
+ * ruler goes through this so its endpoints follow weak/strong/Ctrl magnet too.
29607
+ */
29608
+ snapCursor(x, y, mode) {
29609
+ const proj = this.deps.projector();
29610
+ const paneId = proj.paneIdAtY(y) ?? "price";
29611
+ const point = this.resolve(x, y, paneId, mode);
29612
+ const sy = proj.yOf(point.price, paneId);
29613
+ return { x: proj.xOf(point.time), y: sy ?? y };
29614
+ }
29615
+ /** Drop the snap-ring marker (a transient mode ended without going through `up`). */
29616
+ clearSnapMarker() {
29617
+ this.snapAt = null;
29618
+ }
29491
29619
  /** Resolve a pixel to a data point with the segment angle locked to 45° steps around
29492
29620
  * `pivot` (Shift held on a line tool). Works in PIXEL space — the user reasons about
29493
29621
  * the angle they see, not about time/price units. The magnet is bypassed: snapping
@@ -30847,17 +30975,17 @@ function glyphIcon(glyph) {
30847
30975
  return textGlyph(String(glyph), 15);
30848
30976
  }
30849
30977
  function stampSizeIcon(size) {
30850
- return textGlyph("\u25CF", (SIZE_PX4[String(size)] ?? 13) + 4);
30978
+ return textGlyph("\u25CF", (SIZE_PX3[String(size)] ?? 13) + 4);
30851
30979
  }
30852
30980
  function sizeIcon(size) {
30853
30981
  return textGlyph(String(size).charAt(0).toUpperCase(), 15);
30854
30982
  }
30855
- var SIZE_PX4 = { small: 10, normal: 13, large: 16, huge: 20 };
30983
+ var SIZE_PX3 = { small: 10, normal: 13, large: 16, huge: 20 };
30856
30984
  function numbersSizeIcon(size) {
30857
- return textGlyph("12", (SIZE_PX4[String(size)] ?? 13) - 1, 16.5, 'font-weight="600"');
30985
+ return textGlyph("12", (SIZE_PX3[String(size)] ?? 13) - 1, 16.5, 'font-weight="600"');
30858
30986
  }
30859
30987
  function labelSizeIcon(size) {
30860
- return textGlyph("T", (SIZE_PX4[String(size)] ?? 13) + 2);
30988
+ return textGlyph("T", (SIZE_PX3[String(size)] ?? 13) + 2);
30861
30989
  }
30862
30990
  function capitalize(s) {
30863
30991
  return s.charAt(0).toUpperCase() + s.slice(1);
@@ -30893,30 +31021,39 @@ var MeasureOverlay = class {
30893
31021
  isFinished() {
30894
31022
  return this.state === "finished";
30895
31023
  }
30896
- /** A press: begin the measurement, or finish it on the second click. */
30897
- down(x, y) {
31024
+ /** A press: begin the measurement, or finish it on the second click.
31025
+ * `x,y` are the raw cursor (drag-slop vs click-move-click). `gx,gy` are the
31026
+ * graphic endpoints — magnet-snapped when the magnet is on, else the same as `x,y`. */
31027
+ down(x, y, gx = x, gy = y) {
30898
31028
  if (this.state === "measuring") {
30899
- this.end = { x, y };
31029
+ this.end = { x: gx, y: gy };
30900
31030
  this.state = "finished";
30901
31031
  return;
30902
31032
  }
30903
- this.start = { x, y };
30904
- this.end = { x, y };
31033
+ this.start = { x: gx, y: gy };
31034
+ this.end = { x: gx, y: gy };
30905
31035
  this.pressX = x;
30906
31036
  this.pressY = y;
30907
31037
  this.state = "measuring";
30908
31038
  }
31039
+ /** Size the in-progress ruler. `x,y` are the graphic (magnet-snapped) cursor. */
30909
31040
  move(x, y) {
30910
31041
  if (this.state === "measuring") this.end = { x, y };
30911
31042
  }
30912
31043
  /** A release: finish if the press was actually dragged (press-drag-release), else wait
30913
- * for the second click (click-move-click). */
30914
- up(x, y) {
31044
+ * for the second click (click-move-click). `x,y` are the raw cursor (slop); `gx,gy`
31045
+ * are the graphic end (magnet-snapped when the magnet is on). */
31046
+ up(x, y, gx = x, gy = y) {
30915
31047
  if (this.state === "measuring" && Math.hypot(x - this.pressX, y - this.pressY) > DRAG_SLOP4) {
30916
- this.end = { x, y };
31048
+ this.end = { x: gx, y: gy };
30917
31049
  this.state = "finished";
30918
31050
  }
30919
31051
  }
31052
+ /** Current graphic endpoints in media pixels, or null when idle. */
31053
+ points() {
31054
+ if (!this.start || !this.end) return null;
31055
+ return { start: this.start, end: this.end };
31056
+ }
30920
31057
  clear() {
30921
31058
  this.state = "idle";
30922
31059
  this.start = null;
@@ -31293,11 +31430,13 @@ var UserDrawingController = class {
31293
31430
  }
31294
31431
  /** Shift+press on the empty plot: arm the measure ruler AND start it at (x, y) in one
31295
31432
  * gesture — the equivalent of clicking the toolbar's Measure button, then pressing.
31296
- * Returns false when a mode/tool is already active (the normal press path owns it). */
31297
- beginMeasureAt(x, y) {
31433
+ * `snap` is the effective magnet (sticky mode, or Ctrl/Cmd-forced strong). Returns
31434
+ * false when a mode/tool is already active (the normal press path owns it). */
31435
+ beginMeasureAt(x, y, snap = "off") {
31298
31436
  if (this.measureMode || this.eraserMode || this.activeTool != null) return false;
31299
31437
  this.withModeIntent(() => this.toggleMeasure());
31300
- this.measure.down(x, y);
31438
+ const g = this.interaction.snapCursor(x, y, snap);
31439
+ this.measure.down(x, y, g.x, g.y);
31301
31440
  this.render();
31302
31441
  return true;
31303
31442
  }
@@ -31319,7 +31458,8 @@ var UserDrawingController = class {
31319
31458
  return;
31320
31459
  }
31321
31460
  if (this.measureMode) {
31322
- this.measure.down(x, y);
31461
+ const g = this.interaction.snapCursor(x, y, snap);
31462
+ this.measure.down(x, y, g.x, g.y);
31323
31463
  if (this.measure.isFinished()) this.withModeIntent(() => this.exitMeasure(false));
31324
31464
  this.render();
31325
31465
  return;
@@ -31332,7 +31472,8 @@ var UserDrawingController = class {
31332
31472
  return;
31333
31473
  }
31334
31474
  if (this.measureMode) {
31335
- this.measure.move(x, y);
31475
+ const g = this.interaction.snapCursor(x, y, snap);
31476
+ this.measure.move(g.x, g.y);
31336
31477
  this.render();
31337
31478
  return;
31338
31479
  }
@@ -31350,13 +31491,14 @@ var UserDrawingController = class {
31350
31491
  this.render();
31351
31492
  }
31352
31493
  }
31353
- pointerUp(x, y) {
31494
+ pointerUp(x, y, snap = "off") {
31354
31495
  if (this.eraserMode) {
31355
31496
  this.erasing = false;
31356
31497
  return;
31357
31498
  }
31358
31499
  if (this.measureMode) {
31359
- this.measure.up(x, y);
31500
+ const g = this.interaction.snapCursor(x, y, snap);
31501
+ this.measure.up(x, y, g.x, g.y);
31360
31502
  if (this.measure.isFinished()) this.withModeIntent(() => this.exitMeasure(false));
31361
31503
  this.render();
31362
31504
  return;
@@ -31401,6 +31543,7 @@ var UserDrawingController = class {
31401
31543
  /** Leave ruler mode. `clearGraphic` keeps a just-finished measurement on screen (false). */
31402
31544
  exitMeasure(clearGraphic = true) {
31403
31545
  this.measureMode = false;
31546
+ this.interaction.clearSnapMarker();
31404
31547
  if (clearGraphic) this.measure.clear();
31405
31548
  this.toolbar.setMeasureActive(false);
31406
31549
  this.render();
@@ -31541,15 +31684,28 @@ var UserDrawingController = class {
31541
31684
  if (this.eraserMode) return "pointer";
31542
31685
  return this.interaction.cursorAt(x, y);
31543
31686
  }
31544
- /** Right-click while placing: cancel the in-progress drawing and revert to the
31545
- * pointer the gesture is an explicit escape, so it disarms even in
31546
- * stay-in-drawing-mode (where Escape would leave the tool armed). Returns whether
31547
- * the press was consumed; false lets the host's context menu open normally. */
31687
+ /** Right-click: an explicit escape back to the pointer. Cancels an in-progress
31688
+ * placement or measurement, and also plain-disarms an armed-but-idle drawing
31689
+ * tool or the eraser — so a right-click ALWAYS reverts to the pointer, even in
31690
+ * stay-in-drawing-mode (where Escape would leave a drawing tool armed).
31691
+ * Persistent toggles (magnet, stay-mode, favorites) are untouched. Returns
31692
+ * whether the press was consumed; false lets the host's context menu open
31693
+ * normally. */
31548
31694
  cancelPlacement() {
31549
- if (!this.interaction.isPlacing()) return false;
31550
- this.interaction.cancel();
31551
- if (this.activeTool != null) this.emit({ kind: "arm", type: null });
31552
- return true;
31695
+ if (this.measureMode || this.eraserMode) {
31696
+ this.withModeIntent(() => this.measureMode ? this.exitMeasure() : this.exitEraser());
31697
+ return true;
31698
+ }
31699
+ if (this.interaction.isPlacing()) {
31700
+ this.interaction.cancel();
31701
+ if (this.activeTool != null) this.emit({ kind: "arm", type: null });
31702
+ return true;
31703
+ }
31704
+ if (this.activeTool != null) {
31705
+ this.emit({ kind: "arm", type: null });
31706
+ return true;
31707
+ }
31708
+ return false;
31553
31709
  }
31554
31710
  /** Double-click over a drawing → suppress the chart's view reset (single-click already
31555
31711
  * opens settings). Returns true only when a drawing is under the cursor. */
@@ -31575,6 +31731,10 @@ var UserDrawingController = class {
31575
31731
  return true;
31576
31732
  }
31577
31733
  if (this.interaction.cancel()) return true;
31734
+ if (this.measureMode) {
31735
+ this.withModeIntent(() => this.exitMeasure());
31736
+ return true;
31737
+ }
31578
31738
  if (this.selectedIds.size) {
31579
31739
  this.clearSelection();
31580
31740
  return true;
@@ -31832,6 +31992,315 @@ var UserDrawingController = class {
31832
31992
  }
31833
31993
  };
31834
31994
 
31995
+ // src/renderers/shared/TableOverlay.ts
31996
+ var SIZE_PX4 = {
31997
+ auto: 13,
31998
+ tiny: 10,
31999
+ small: 11,
32000
+ normal: 13,
32001
+ large: 16,
32002
+ huge: 20
32003
+ };
32004
+ function fontPxOf(size) {
32005
+ if (typeof size === "number") return size > 0 ? size : SIZE_PX4.auto;
32006
+ return SIZE_PX4[size] ?? SIZE_PX4.auto;
32007
+ }
32008
+ function tableHasContent(t) {
32009
+ return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
32010
+ }
32011
+ function mergeRenderPlan(t) {
32012
+ const span = /* @__PURE__ */ new Map();
32013
+ const omit = /* @__PURE__ */ new Set();
32014
+ for (const m of t.merges) {
32015
+ span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
32016
+ for (let r = m.startRow; r <= m.endRow; r += 1) {
32017
+ for (let c = m.startCol; c <= m.endCol; c += 1) {
32018
+ if (r !== m.startRow || c !== m.startCol) omit.add(`${r}:${c}`);
32019
+ }
32020
+ }
32021
+ }
32022
+ for (let r = 0; r < t.rows; r += 1) {
32023
+ for (let c = 0; c < t.columns; c += 1) {
32024
+ if (t.cells[r]?.[c]?.merged && !span.has(`${r}:${c}`)) omit.add(`${r}:${c}`);
32025
+ }
32026
+ }
32027
+ for (const key of span.keys()) omit.delete(key);
32028
+ return { span, omit };
32029
+ }
32030
+
32031
+ // src/renderers/shared/TableCanvasRenderer.ts
32032
+ var PAD_X = 6;
32033
+ var PAD_Y = 2;
32034
+ var MARGIN = 6;
32035
+ var LINE_HEIGHT = 1.2;
32036
+ function paintTable(ctx, t, args, tips) {
32037
+ if (!tableHasContent(t)) return;
32038
+ const layout = layoutTable(ctx, t, args);
32039
+ if (!layout || layout.w <= 0 || layout.h <= 0) return;
32040
+ const fw = t.frameColor && t.frameWidth > 0 ? t.frameWidth : 0;
32041
+ const { x, y } = anchorOrigin(t.position, layout.w + 2 * fw, layout.h + 2 * fw, args);
32042
+ const x0 = x + fw;
32043
+ const y0 = y + fw;
32044
+ if (t.bgColor) {
32045
+ ctx.fillStyle = t.bgColor;
32046
+ ctx.fillRect(x0, y0, layout.w, layout.h);
32047
+ }
32048
+ if (fw > 0 && t.frameColor) {
32049
+ ctx.strokeStyle = t.frameColor;
32050
+ ctx.lineWidth = fw;
32051
+ ctx.strokeRect(x + fw / 2, y + fw / 2, layout.w + fw, layout.h + fw);
32052
+ }
32053
+ const colX = [0];
32054
+ for (const w of layout.colW) colX.push(colX[colX.length - 1] + w);
32055
+ const rowY = [0];
32056
+ for (const h of layout.rowH) rowY.push(rowY[rowY.length - 1] + h);
32057
+ const prevBaseline = ctx.textBaseline;
32058
+ const prevAlign = ctx.textAlign;
32059
+ ctx.textBaseline = "middle";
32060
+ for (const box of layout.boxes) {
32061
+ const rx = x0 + colX[box.c];
32062
+ const ry = y0 + rowY[box.r];
32063
+ const rw = colX[box.c + box.cs] - colX[box.c];
32064
+ const rh = rowY[box.r + box.rs] - rowY[box.r];
32065
+ const cell = box.cell;
32066
+ if (cell.bgColor) {
32067
+ ctx.fillStyle = cell.bgColor;
32068
+ ctx.fillRect(rx, ry, rw, rh);
32069
+ }
32070
+ const text = cell.text ?? "";
32071
+ if (text.length > 0) {
32072
+ const px = fontPxOf(cell.textSize);
32073
+ ctx.font = cellFont(cell, px, args.theme);
32074
+ ctx.fillStyle = cell.textColor ?? args.theme.textColor;
32075
+ const lines = text.split("\n");
32076
+ const blockH = lines.length * px * LINE_HEIGHT;
32077
+ const blockTop = cell.vAlign === "top" ? ry + PAD_Y : cell.vAlign === "bottom" ? ry + rh - PAD_Y - blockH : ry + (rh - blockH) / 2;
32078
+ const tx = cell.hAlign === "left" ? rx + PAD_X : cell.hAlign === "right" ? rx + rw - PAD_X : rx + rw / 2;
32079
+ ctx.textAlign = cell.hAlign;
32080
+ lines.forEach((line, i) => ctx.fillText(line, tx, blockTop + (i + 0.5) * px * LINE_HEIGHT));
32081
+ }
32082
+ if (cell.tooltip) tips.push({ left: rx, top: ry, right: rx + rw, bottom: ry + rh, text: cell.tooltip });
32083
+ }
32084
+ ctx.textBaseline = prevBaseline;
32085
+ ctx.textAlign = prevAlign;
32086
+ if (t.borderColor && t.borderWidth > 0) {
32087
+ ctx.strokeStyle = t.borderColor;
32088
+ ctx.lineWidth = t.borderWidth;
32089
+ const seen = /* @__PURE__ */ new Set();
32090
+ ctx.beginPath();
32091
+ const edge = (ax, ay, bx, by) => {
32092
+ const key = `${ax},${ay},${bx},${by}`;
32093
+ if (seen.has(key)) return;
32094
+ seen.add(key);
32095
+ ctx.moveTo(ax, ay);
32096
+ ctx.lineTo(bx, by);
32097
+ };
32098
+ for (const box of layout.boxes) {
32099
+ const l = Math.round(x0 + colX[box.c]);
32100
+ const r = Math.round(x0 + colX[box.c + box.cs]);
32101
+ const tp = Math.round(y0 + rowY[box.r]);
32102
+ const bt = Math.round(y0 + rowY[box.r + box.rs]);
32103
+ edge(l, tp, r, tp);
32104
+ edge(l, bt, r, bt);
32105
+ edge(l, tp, l, bt);
32106
+ edge(r, tp, r, bt);
32107
+ }
32108
+ ctx.stroke();
32109
+ }
32110
+ }
32111
+ function layoutTable(ctx, t, args) {
32112
+ const { span, omit } = mergeRenderPlan(t);
32113
+ const colW = new Array(t.columns).fill(0);
32114
+ const rowH = new Array(t.rows).fill(0);
32115
+ const boxes = [];
32116
+ for (let r = 0; r < t.rows; r += 1) {
32117
+ for (let c = 0; c < t.columns; c += 1) {
32118
+ if (omit.has(`${r}:${c}`)) continue;
32119
+ const cell = t.cells[r]?.[c];
32120
+ if (cell == null) continue;
32121
+ const sp = span.get(`${r}:${c}`);
32122
+ boxes.push({ cell, r, c, cs: Math.min(sp?.cs ?? 1, t.columns - c), rs: Math.min(sp?.rs ?? 1, t.rows - r) });
32123
+ }
32124
+ }
32125
+ if (boxes.length === 0) return null;
32126
+ const sizeOf = (cell) => {
32127
+ const px = fontPxOf(cell.textSize);
32128
+ ctx.font = cellFont(cell, px, args.theme);
32129
+ const lines = (cell.text ?? "").split("\n");
32130
+ let maxW = 0;
32131
+ for (const line of lines) maxW = Math.max(maxW, ctx.measureText(line).width);
32132
+ let w2 = Math.ceil(maxW) + 2 * PAD_X;
32133
+ let h2 = Math.ceil(lines.length * px * LINE_HEIGHT) + 2 * PAD_Y;
32134
+ if (cell.width) w2 = Math.max(w2, cell.width / 100 * args.plotWidth);
32135
+ if (cell.height) h2 = Math.max(h2, cell.height / 100 * args.paneHeight);
32136
+ return { w: w2, h: h2 };
32137
+ };
32138
+ const spanning = [];
32139
+ for (const box of boxes) {
32140
+ const { w: w2, h: h2 } = sizeOf(box.cell);
32141
+ if (box.cs === 1) colW[box.c] = Math.max(colW[box.c], w2);
32142
+ if (box.rs === 1) rowH[box.r] = Math.max(rowH[box.r], h2);
32143
+ if (box.cs > 1 || box.rs > 1) spanning.push({ box, w: w2, h: h2 });
32144
+ }
32145
+ for (const { box, w: w2, h: h2 } of spanning) {
32146
+ if (box.cs > 1) {
32147
+ let sum = 0;
32148
+ for (let c = box.c; c < box.c + box.cs; c += 1) sum += colW[c];
32149
+ if (w2 > sum) for (let c = box.c; c < box.c + box.cs; c += 1) colW[c] += (w2 - sum) / box.cs;
32150
+ }
32151
+ if (box.rs > 1) {
32152
+ let sum = 0;
32153
+ for (let r = box.r; r < box.r + box.rs; r += 1) sum += rowH[r];
32154
+ if (h2 > sum) for (let r = box.r; r < box.r + box.rs; r += 1) rowH[r] += (h2 - sum) / box.rs;
32155
+ }
32156
+ }
32157
+ let w = 0;
32158
+ for (const cw of colW) w += cw;
32159
+ let h = 0;
32160
+ for (const rh of rowH) h += rh;
32161
+ return { colW, rowH, w, h, boxes };
32162
+ }
32163
+ function anchorOrigin(position, totalW, totalH, args) {
32164
+ let y;
32165
+ if (position.startsWith("top")) y = MARGIN;
32166
+ else if (position.startsWith("bottom")) y = args.paneHeight - MARGIN - totalH;
32167
+ else y = args.paneHeight / 2 - totalH / 2;
32168
+ let x;
32169
+ if (position.endsWith("left")) x = MARGIN;
32170
+ else if (position.endsWith("right")) x = args.plotWidth - MARGIN - totalW;
32171
+ else x = args.plotWidth / 2 - totalW / 2;
32172
+ return { x, y };
32173
+ }
32174
+ function cellFont(cell, px, theme) {
32175
+ const family = cell.fontFamily === "monospace" ? "monospace" : theme.fontFamily || "sans-serif";
32176
+ return `${cell.italic ? "italic " : ""}${cell.bold ? "bold " : ""}${px}px ${family}`;
32177
+ }
32178
+
32179
+ // src/renderers/native/drawings/IndicatorDrawingSlices.ts
32180
+ function indicatorSliceKey(z, boundaries) {
32181
+ return boundaries.find((b) => b > z) ?? Infinity;
32182
+ }
32183
+ var IndicatorDrawingSlices = class {
32184
+ constructor() {
32185
+ this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
32186
+ /** Slice canvas cache, keyed `paneId|beforeZ` — same lifecycle as the user-drawing cache. */
32187
+ this.sliceCache = /* @__PURE__ */ new Map();
32188
+ /** Tooltip hit-rects of every label drawn this frame, in plot coords (rebuilt per prepare). */
32189
+ this.tips = [];
32190
+ }
32191
+ /**
32192
+ * Rebuild the per-indicator drawing slices for this data frame. `ref` is the data
32193
+ * canvas the slices must match pixel-for-pixel (the backend composites them 1:1).
32194
+ * Runs from the renderer's data paint, just before the backend composites the scene.
32195
+ */
32196
+ prepare(scene, coords, theme, ref) {
32197
+ this.tips = [];
32198
+ const out = /* @__PURE__ */ new Map();
32199
+ if (ref.width === 0 || ref.height === 0) {
32200
+ this.sliceCache.clear();
32201
+ return out;
32202
+ }
32203
+ this.drawScene.setDeps({
32204
+ timeToLogical: (ms) => coords.timeToLogical(ms),
32205
+ barAt: (logical) => {
32206
+ const b = scene.bars[Math.round(logical)];
32207
+ return b ? { high: b.high, low: b.low } : null;
32208
+ },
32209
+ theme
32210
+ });
32211
+ const dpr = coords.dpr;
32212
+ const dataW = coords.width;
32213
+ const buckets = /* @__PURE__ */ new Map();
32214
+ const add = (paneId, beforeZ, entry) => {
32215
+ const key = `${paneId}|${beforeZ}`;
32216
+ const bucket = buckets.get(key);
32217
+ if (bucket) bucket.entries.push(entry);
32218
+ else buckets.set(key, { paneId, beforeZ, entries: [entry] });
32219
+ };
32220
+ for (const pane of scene.orderedPanes()) {
32221
+ if (pane.collapsed) continue;
32222
+ const boundaries = scene.seriesBoundaries(pane.id);
32223
+ for (const m of scene.orderedIndicatorsForPane(pane.id)) {
32224
+ const set = modelDrawingSet(m, false);
32225
+ const tables = (m.tables ?? []).filter((t) => !t.overlay);
32226
+ if (drawingSetEmpty(set) && tables.length === 0) continue;
32227
+ const sc = scene.scaleFor(m, pane);
32228
+ const mp = sc === pane.scale ? pane : { ...pane, scale: sc };
32229
+ const beforeZ = indicatorSliceKey(scene.zOf(m.id), boundaries);
32230
+ add(pane.id, beforeZ, { set, tables, pane: mp, indexOffset: scene.offsetOf(m.id) });
32231
+ }
32232
+ if (pane.kind === "price") {
32233
+ for (const m of scene.indicators.values()) {
32234
+ const set = modelDrawingSet(m, true);
32235
+ const tables = (m.tables ?? []).filter((t) => t.overlay === true);
32236
+ if (drawingSetEmpty(set) && tables.length === 0) continue;
32237
+ add(pane.id, Infinity, { set, tables, pane, indexOffset: scene.offsetOf(m.id) });
32238
+ }
32239
+ }
32240
+ }
32241
+ for (const [key, { paneId, beforeZ, entries }] of buckets) {
32242
+ let canvas = this.sliceCache.get(key);
32243
+ if (!canvas) {
32244
+ canvas = document.createElement("canvas");
32245
+ this.sliceCache.set(key, canvas);
32246
+ }
32247
+ if (canvas.width !== ref.width || canvas.height !== ref.height) {
32248
+ canvas.width = ref.width;
32249
+ canvas.height = ref.height;
32250
+ }
32251
+ const ctx = canvas.getContext("2d");
32252
+ if (!ctx) continue;
32253
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
32254
+ ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
32255
+ for (const e of entries) this.paintEntry(ctx, e, coords, dataW, theme);
32256
+ const slices = out.get(paneId) ?? [];
32257
+ slices.push({ beforeZ, canvas });
32258
+ out.set(paneId, slices);
32259
+ }
32260
+ for (const key of [...this.sliceCache.keys()]) if (!buckets.has(key)) this.sliceCache.delete(key);
32261
+ for (const slices of out.values()) slices.sort((a, b) => a.beforeZ - b.beforeZ);
32262
+ return out;
32263
+ }
32264
+ paintEntry(ctx, e, coords, dataW, theme) {
32265
+ const { pane } = e;
32266
+ const paneTips = [];
32267
+ ctx.save();
32268
+ ctx.translate(0, pane.bounds.top);
32269
+ ctx.beginPath();
32270
+ ctx.rect(0, 0, dataW, pane.bounds.height);
32271
+ ctx.clip();
32272
+ this.drawScene.setSet(e.set, e.indexOffset);
32273
+ this.drawScene.render(
32274
+ ctx,
32275
+ dataW,
32276
+ pane.bounds.height,
32277
+ (l) => coords.logicalToX(l),
32278
+ (price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
32279
+ );
32280
+ paneTips.push(...this.drawScene.labelTipRegions());
32281
+ for (const t of e.tables) paintTable(ctx, t, { paneHeight: pane.bounds.height, plotWidth: dataW, theme }, paneTips);
32282
+ ctx.restore();
32283
+ for (const r of paneTips) {
32284
+ this.tips.push({ ...r, top: r.top + pane.bounds.top, bottom: r.bottom + pane.bounds.top });
32285
+ }
32286
+ }
32287
+ /** Tooltip of the topmost label or table cell under a plot-space point, or null. Fed by the last prepare. */
32288
+ labelTooltipAt(x, y) {
32289
+ for (let i = this.tips.length - 1; i >= 0; i -= 1) {
32290
+ const r = this.tips[i];
32291
+ if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return r.text;
32292
+ }
32293
+ return null;
32294
+ }
32295
+ };
32296
+ function mergeSlices(indicator, user) {
32297
+ const out = /* @__PURE__ */ new Map();
32298
+ for (const [paneId, slices] of indicator) out.set(paneId, [...slices]);
32299
+ for (const [paneId, slices] of user) out.set(paneId, [...out.get(paneId) ?? [], ...slices]);
32300
+ for (const slices of out.values()) slices.sort((a, b) => a.beforeZ - b.beforeZ);
32301
+ return out;
32302
+ }
32303
+
31835
32304
  // src/renderers/native/drawings/Projector.ts
31836
32305
  function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
31837
32306
  return {
@@ -32577,6 +33046,11 @@ var NativeRenderer = class {
32577
33046
  this.vpvrRenderer = new VpvrRenderer();
32578
33047
  this.resizeObserver = null;
32579
33048
  this.dprMedia = null;
33049
+ /** Plot size in INTEGER device px, as last reported by the resize observer's
33050
+ * device-pixel-content-box — the browser's own statement of how many device pixels
33051
+ * it paints the plot into. `null` until the first report or where the box type is
33052
+ * unsupported (WebKit); syncSize then falls back to rounding the client rect. */
33053
+ this.plotDeviceSize = null;
32580
33054
  this.coords = new CoordinateSystem();
32581
33055
  this.scene = new SceneGraph();
32582
33056
  // chosen at mount (WebGL2 if available, else canvas2d)
@@ -32584,6 +33058,8 @@ var NativeRenderer = class {
32584
33058
  this.glowAmount = 0;
32585
33059
  // WebGL2 neon-glow intensity (canvas2d ignores it)
32586
33060
  this.chrome = new ChromeRenderer();
33061
+ /** Prepaints each indicator's Pine drawings into interleave slices at the model's z. */
33062
+ this.indicatorSlices = new IndicatorDrawingSlices();
32587
33063
  /** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
32588
33064
  this.labelTooltip = null;
32589
33065
  this.crosshairLayer = new CrosshairRenderer();
@@ -32732,7 +33208,6 @@ var NativeRenderer = class {
32732
33208
  this.toggleVisibleCbs = /* @__PURE__ */ new Set();
32733
33209
  this.moveIndicatorCbs = /* @__PURE__ */ new Set();
32734
33210
  this.priceStyleCbs = /* @__PURE__ */ new Set();
32735
- this.tableOverlays = /* @__PURE__ */ new Map();
32736
33211
  this.name = "native";
32737
33212
  this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "intro", "zoomAnchor", "axisDrag", "paneResize", "candleZOrder", "candleVisible", "seriesOrder", "highlights", "sessionZones", "gridlines", "axisLabels", "scaleMode", "invertScale", "paneScales", "autoScale", "timezone", "keyboard", "historyChords", "priceStyle", "priceBaseline", "baselinePrice", "settings", "attribution", "dialogHost", "tradeMarkers", "indicatorTitles", "indicatorValues"];
32738
33213
  /** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
@@ -33046,7 +33521,6 @@ var NativeRenderer = class {
33046
33521
  * hidden) on clear so a re-show picks up the current theme.
33047
33522
  */
33048
33523
  setLoading(loading) {
33049
- for (const overlay of this.tableOverlays.values()) overlay.setVisible(!loading);
33050
33524
  if (!loading || !this.wrapper) {
33051
33525
  this.loadingEl?.remove();
33052
33526
  this.loadingEl = null;
@@ -33685,13 +34159,13 @@ var NativeRenderer = class {
33685
34159
  resetView: () => this.resetView(),
33686
34160
  // User drawings claim a gesture before pan when armed / over a drawing.
33687
34161
  drawingsClaim: (x, y) => this.userDrawings?.claim(x, y) ?? false,
33688
- drawingsMeasureStart: (x, y) => this.userDrawings?.beginMeasureAt(x, y) ?? false,
34162
+ drawingsMeasureStart: (x, y, snap) => this.userDrawings?.beginMeasureAt(x, y, snap) ?? false,
33689
34163
  drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y) ?? false,
33690
34164
  drawingsCancelPlacement: () => this.userDrawings?.cancelPlacement() ?? false,
33691
34165
  drawingsSnapMode: () => this.snapMode,
33692
34166
  drawingsPointerDown: (x, y, snap, shift) => this.userDrawings?.pointerDown(x, y, snap, shift),
33693
34167
  drawingsPointerMove: (x, y, snap, shift) => this.userDrawings?.pointerMove(x, y, snap, shift),
33694
- drawingsPointerUp: (x, y) => this.userDrawings?.pointerUp(x, y),
34168
+ drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
33695
34169
  drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
33696
34170
  drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
33697
34171
  drawingsClearTransient: () => this.userDrawings?.clearTransient()
@@ -33708,7 +34182,7 @@ var NativeRenderer = class {
33708
34182
  this.plot.addEventListener("pointerleave", this.onScrollProximityLeave);
33709
34183
  this.labelTooltip = new LabelTooltip(this.plot, {
33710
34184
  theme: () => this.chromeTheme(),
33711
- lookup: (x, y) => this.chrome.labelTooltipAt(x, y)
34185
+ lookup: (x, y) => this.indicatorSlices.labelTooltipAt(x, y)
33712
34186
  });
33713
34187
  this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
33714
34188
  projector: () => this.drawingProjector(),
@@ -33793,6 +34267,7 @@ var NativeRenderer = class {
33793
34267
  this.emitPaneAction({ type: "maximize", paneId, maximized });
33794
34268
  }
33795
34269
  });
34270
+ this.paneControls.setSuspended(this.layoutMode === "mobile");
33796
34271
  this.axisScaleButtons = new AxisScaleButtons(this.plot, theme, {
33797
34272
  panes: () => this.axisScaleViews(),
33798
34273
  rightAxis: () => this.rightAxisW,
@@ -33802,8 +34277,19 @@ var NativeRenderer = class {
33802
34277
  if (pane) this.setPaneLog(paneId, !paneLogScale(this.scene, pane));
33803
34278
  }
33804
34279
  });
33805
- this.resizeObserver = new ResizeObserver(() => this.resize());
34280
+ this.resizeObserver = new ResizeObserver((entries) => {
34281
+ for (const e of entries) {
34282
+ if (e.target !== this.plot) continue;
34283
+ const s = e.devicePixelContentBoxSize?.[0];
34284
+ if (s) this.plotDeviceSize = { width: s.inlineSize, height: s.blockSize };
34285
+ }
34286
+ this.resize();
34287
+ });
33806
34288
  this.resizeObserver.observe(this.wrapper);
34289
+ try {
34290
+ this.resizeObserver.observe(this.plot, { box: "device-pixel-content-box" });
34291
+ } catch {
34292
+ }
33807
34293
  this.watchDpr();
33808
34294
  this.syncSize();
33809
34295
  }
@@ -33928,8 +34414,6 @@ var NativeRenderer = class {
33928
34414
  this.inputsUI?.destroy();
33929
34415
  this.paneControls?.destroy();
33930
34416
  this.axisScaleButtons?.destroy();
33931
- for (const overlay of this.tableOverlays.values()) overlay.destroy();
33932
- this.tableOverlays.clear();
33933
34417
  this.resizeObserver?.disconnect();
33934
34418
  this.resizeObserver = null;
33935
34419
  this.dprMedia?.removeEventListener("change", this.onDprChange);
@@ -33958,6 +34442,7 @@ var NativeRenderer = class {
33958
34442
  this.attributionEl = null;
33959
34443
  this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
33960
34444
  this.mountContainer?.style.removeProperty("--vela-scale-gutter");
34445
+ this.mountContainer?.style.removeProperty("--vela-bottom-gutter");
33961
34446
  this.mountContainer?.style.removeProperty("--vela-price-pane-top");
33962
34447
  this.mountContainer?.style.removeProperty("--vela-price-pane-bottom");
33963
34448
  this.mountContainer = null;
@@ -33995,7 +34480,8 @@ var NativeRenderer = class {
33995
34480
  }
33996
34481
  const skipFit = opts?.preserveView === true && this.didInitialFit;
33997
34482
  if (this.coords.width > 0 && !skipFit) {
33998
- this.fitContent();
34483
+ if (this.didInitialFit) this.reframeKeepZoom();
34484
+ else this.fitContent();
33999
34485
  this.didInitialFit = true;
34000
34486
  }
34001
34487
  if (!this.introPlayed && this.bars.length > 0) {
@@ -34050,7 +34536,6 @@ var NativeRenderer = class {
34050
34536
  ensurePane(pane) {
34051
34537
  this.scene.ensurePane(pane.id, pane.kind, pane.order, pane.heightWeight ?? (pane.kind === "price" ? 3 : 1));
34052
34538
  this.layoutPanes();
34053
- this.repositionTables();
34054
34539
  this.paneControls?.refresh();
34055
34540
  this.scheduler.invalidate(4 /* Full */);
34056
34541
  }
@@ -34072,17 +34557,14 @@ var NativeRenderer = class {
34072
34557
  if (!model.ownScale) this.scene.dropIndicatorScale(handle.id);
34073
34558
  this.inputsUI.setPane(handle.id, paneId);
34074
34559
  this.refreshAnchorOffset(model);
34075
- this.syncTables(model);
34076
34560
  this.refreshAxisWidth();
34077
34561
  this.layoutPanes();
34078
- this.repositionTables();
34079
34562
  this.paneControls?.refresh();
34080
34563
  this.scheduler.invalidate(4 /* Full */);
34081
34564
  }
34082
34565
  orderPanes(orderedIds) {
34083
34566
  this.scene.orderPanes(orderedIds);
34084
34567
  this.layoutPanes();
34085
- this.repositionTables();
34086
34568
  this.paneControls?.refresh();
34087
34569
  this.scheduler.invalidate(4 /* Full */);
34088
34570
  }
@@ -34091,7 +34573,6 @@ var NativeRenderer = class {
34091
34573
  if (!pane || pane.collapsed === collapsed) return;
34092
34574
  pane.collapsed = collapsed;
34093
34575
  this.layoutPanes();
34094
- this.repositionTables();
34095
34576
  this.paneControls?.refresh();
34096
34577
  this.scheduler.invalidate(4 /* Full */);
34097
34578
  }
@@ -34099,7 +34580,6 @@ var NativeRenderer = class {
34099
34580
  if (paneId !== null && !this.scene.panes.has(paneId)) paneId = null;
34100
34581
  this.maximizedPaneId = paneId;
34101
34582
  this.layoutPanes();
34102
- this.repositionTables();
34103
34583
  this.paneControls?.refresh();
34104
34584
  this.scheduler.invalidate(4 /* Full */);
34105
34585
  }
@@ -34186,7 +34666,6 @@ var NativeRenderer = class {
34186
34666
  native: !!model.native,
34187
34667
  ...model.props ? { props: model.props, propValues: model.propValues ?? {} } : {}
34188
34668
  });
34189
- this.syncTables(model);
34190
34669
  if (model.native?.type === "volume") {
34191
34670
  this.volumeActive = true;
34192
34671
  this.volumeHidden = false;
@@ -34210,7 +34689,6 @@ var NativeRenderer = class {
34210
34689
  }
34211
34690
  }
34212
34691
  applyPatch(model, patch);
34213
- this.syncTables(model);
34214
34692
  this.scheduler.invalidate(3 /* Light */);
34215
34693
  }
34216
34694
  removeIndicator(handle) {
@@ -34229,8 +34707,6 @@ var NativeRenderer = class {
34229
34707
  this.scene.forgetAnchorOffset(handle.id);
34230
34708
  this.scene.dropIndicatorScale(handle.id);
34231
34709
  this.inputsUI.remove(handle.id);
34232
- this.tableOverlays.get(handle.id)?.destroy();
34233
- this.tableOverlays.delete(handle.id);
34234
34710
  this.refreshAxisWidth();
34235
34711
  this.paneControls?.refresh();
34236
34712
  this.scheduler.invalidate(4 /* Full */);
@@ -34274,8 +34750,6 @@ var NativeRenderer = class {
34274
34750
  }
34275
34751
  if (!visible) {
34276
34752
  this.scene.indicators.delete(handle.id);
34277
- this.tableOverlays.get(handle.id)?.destroy();
34278
- this.tableOverlays.delete(handle.id);
34279
34753
  }
34280
34754
  this.inputsUI.setVisible(handle.id, visible);
34281
34755
  this.scheduler.invalidate(4 /* Full */);
@@ -34321,6 +34795,7 @@ var NativeRenderer = class {
34321
34795
  this.userDrawings?.setLayoutMode(mode);
34322
34796
  this.settingsDialog?.setLayoutMode(mode);
34323
34797
  this.inputsUI?.setLayoutMode(mode);
34798
+ this.paneControls?.setSuspended(mode === "mobile");
34324
34799
  if (this.scrollButton) {
34325
34800
  const px = mode === "mobile" ? SCROLL_BTN_SIZE_TOUCH : SCROLL_BTN_SIZE;
34326
34801
  this.scrollButton.style.width = `${px}px`;
@@ -34654,6 +35129,7 @@ var NativeRenderer = class {
34654
35129
  this.scaleDragHeight = res.height;
34655
35130
  this.scaleDragStart = { ...res.holder.scale };
34656
35131
  res.holder.manualScale = { ...res.holder.scale };
35132
+ this.axisScaleButtons?.reposition();
34657
35133
  this.scheduler.invalidate(4 /* Full */);
34658
35134
  }
34659
35135
  /** Rescale the grabbed scale around its center by the total drag (down ⇒ zoom out). */
@@ -34685,6 +35161,7 @@ var NativeRenderer = class {
34685
35161
  const res = this.resolveScaleHolder(x, y);
34686
35162
  if (!res) return;
34687
35163
  res.holder.manualScale = null;
35164
+ this.axisScaleButtons?.reposition();
34688
35165
  this.scheduler.invalidate(4 /* Full */);
34689
35166
  }
34690
35167
  /**
@@ -34716,7 +35193,6 @@ var NativeRenderer = class {
34716
35193
  /** Relayout + repaint + refresh the hover buttons after a collapse/maximize/order change. */
34717
35194
  afterPaneLayoutChange() {
34718
35195
  this.layoutPanes();
34719
- this.repositionTables();
34720
35196
  this.paneControls?.refresh();
34721
35197
  this.scheduler.invalidate(4 /* Full */);
34722
35198
  }
@@ -34797,7 +35273,6 @@ var NativeRenderer = class {
34797
35273
  above.heightWeight = next.above;
34798
35274
  below.heightWeight = next.below;
34799
35275
  this.layoutPanes();
34800
- this.repositionTables();
34801
35276
  this.scheduler.invalidate(4 /* Full */);
34802
35277
  }
34803
35278
  /** Double-click a separator → split the two adjacent panes evenly (each gets half of
@@ -34812,7 +35287,6 @@ var NativeRenderer = class {
34812
35287
  above.heightWeight = half;
34813
35288
  below.heightWeight = half;
34814
35289
  this.layoutPanes();
34815
- this.repositionTables();
34816
35290
  this.scheduler.invalidate(4 /* Full */);
34817
35291
  }
34818
35292
  // ── keyboard navigation / accessibility (item 11) ──
@@ -35091,7 +35565,10 @@ var NativeRenderer = class {
35091
35565
  const liveActual = li >= 0 ? this.bars[li] : void 0;
35092
35566
  const easeLive = !!liveActual && this.liveEaseTime === liveActual.time && (liveActual.high !== this.liveEaseHigh || liveActual.low !== this.liveEaseLow || liveActual.close !== this.liveEaseClose);
35093
35567
  if (easeLive && liveActual) this.bars[li] = { ...liveActual, high: this.liveEaseHigh, low: this.liveEaseLow, close: this.liveEaseClose };
35094
- this.scene.drawingSlices = this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map();
35568
+ this.scene.drawingSlices = mergeSlices(
35569
+ this.indicatorSlices.prepare(this.scene, this.coords, this.theme, this.dataCanvas),
35570
+ this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map()
35571
+ );
35095
35572
  this.backdropRenderer.render(this.scene, this.coords, this.theme, gridAlpha);
35096
35573
  this.backend.render(this.scene, this.coords, this.theme);
35097
35574
  this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
@@ -35322,6 +35799,20 @@ var NativeRenderer = class {
35322
35799
  this.coords.setViewport(v);
35323
35800
  this.targetBarSpacing = v.barSpacing;
35324
35801
  }
35802
+ /** Re-frame after a series replacement (a symbol/timeframe switch): keep the user's
35803
+ * zoom (bar spacing), re-anchor the newest bars at the default right offset.
35804
+ * `clampViewport`'s fit-all-bars floor deliberately does NOT apply — a progressive
35805
+ * head may still be backfilling toward the previous depth, and raising the spacing
35806
+ * to its temporary bar count would lose the zoom this exists to keep. */
35807
+ reframeKeepZoom() {
35808
+ this.animator?.stop();
35809
+ this.panVelocity = 0;
35810
+ for (const pane of this.scene.panes.values()) pane.manualScale = null;
35811
+ for (const sl of this.scene.indicatorScales.values()) sl.manualScale = null;
35812
+ const v = { barSpacing: clampBarSpacing(this.coords.getViewport().barSpacing), rightOffset: defaultViewport().rightOffset };
35813
+ this.coords.setViewport(v);
35814
+ this.targetBarSpacing = v.barSpacing;
35815
+ }
35325
35816
  paneBoundsFor(paneId) {
35326
35817
  const p = this.scene.panes.get(paneId);
35327
35818
  return { top: p?.bounds.top ?? 0, height: p?.bounds.height ?? 0, rightAxis: this.rightAxisW };
@@ -35529,27 +36020,6 @@ var NativeRenderer = class {
35529
36020
  }
35530
36021
  return maxVol;
35531
36022
  }
35532
- /** Create/update/destroy an indicator's DOM table overlay (anchored off real pane geometry). */
35533
- syncTables(model) {
35534
- const tables = model.tables ?? [];
35535
- let overlay = this.tableOverlays.get(model.id);
35536
- if (tables.length === 0) {
35537
- if (overlay) {
35538
- overlay.destroy();
35539
- this.tableOverlays.delete(model.id);
35540
- }
35541
- return;
35542
- }
35543
- if (!overlay) {
35544
- overlay = new TableOverlay(this.plot, this.theme, (id) => this.paneBoundsFor(id));
35545
- overlay.setVisible(this.loadingEl === null);
35546
- this.tableOverlays.set(model.id, overlay);
35547
- }
35548
- overlay.update(tables);
35549
- }
35550
- repositionTables() {
35551
- for (const overlay of this.tableOverlays.values()) overlay.reposition();
35552
- }
35553
36023
  layoutPanes() {
35554
36024
  const panes = this.scene.orderedPanes();
35555
36025
  const dataHeight = this.coords.height;
@@ -35600,6 +36070,7 @@ var NativeRenderer = class {
35600
36070
  const visible = maxPane ? [maxPane] : this.scene.orderedPanes().filter((p) => !p.collapsed);
35601
36071
  const paneBottom = visible.length ? Math.max(...visible.map((p) => p.bounds.top + p.bounds.height)) : dataHeight;
35602
36072
  this.scrollBtnBottomPx = SCROLL_BTN_BOTTOM + Math.max(0, dataHeight - paneBottom);
36073
+ this.mountContainer?.style.setProperty("--vela-bottom-gutter", `${TIME_AXIS_H + Math.max(0, dataHeight - paneBottom)}px`);
35603
36074
  this.scrollBtnRightPx = this.rightAxisW + SCROLL_BTN_RIGHT_INSET;
35604
36075
  if (this.scrollButton) {
35605
36076
  this.scrollButton.style.bottom = `${this.scrollBtnBottomPx}px`;
@@ -35692,30 +36163,33 @@ var NativeRenderer = class {
35692
36163
  if (w <= 0 || h <= 0) return;
35693
36164
  const dpr = window.devicePixelRatio || 1;
35694
36165
  this.plot.style.left = `${this.toolbarGutter}px`;
35695
- const pw = Math.max(1, w - this.toolbarGutter);
35696
- const ph = h;
35697
- this.dataCanvas.width = Math.round(pw * dpr);
35698
- this.dataCanvas.height = Math.round(ph * dpr);
35699
- this.backdropCanvas.width = this.dataCanvas.width;
35700
- this.backdropCanvas.height = this.dataCanvas.height;
35701
- this.volumeCanvas.width = this.dataCanvas.width;
35702
- this.volumeCanvas.height = this.dataCanvas.height;
35703
- for (const l of this.extLayers) {
35704
- l.canvas.width = this.dataCanvas.width;
35705
- l.canvas.height = this.dataCanvas.height;
35706
- }
35707
- this.vpvrCanvas.width = this.dataCanvas.width;
35708
- this.vpvrCanvas.height = this.dataCanvas.height;
35709
- this.chromeCanvas.width = this.dataCanvas.width;
35710
- this.chromeCanvas.height = this.dataCanvas.height;
35711
- this.drawingsCanvas.width = this.dataCanvas.width;
35712
- this.drawingsCanvas.height = this.dataCanvas.height;
35713
- this.cursorCanvas.width = this.dataCanvas.width;
35714
- this.cursorCanvas.height = this.dataCanvas.height;
36166
+ const rect = this.plot.getBoundingClientRect();
36167
+ let bw = Math.max(1, Math.round(rect.width * dpr));
36168
+ let bh = Math.max(1, Math.round(rect.height * dpr));
36169
+ const dev = this.plotDeviceSize;
36170
+ if (dev && Math.abs(dev.width - rect.width * dpr) <= 1 && Math.abs(dev.height - rect.height * dpr) <= 1) {
36171
+ bw = Math.max(1, dev.width);
36172
+ bh = Math.max(1, dev.height);
36173
+ }
36174
+ const pw = bw / dpr;
36175
+ const ph = bh / dpr;
36176
+ const size = (canvas) => {
36177
+ canvas.width = bw;
36178
+ canvas.height = bh;
36179
+ canvas.style.width = `${pw}px`;
36180
+ canvas.style.height = `${ph}px`;
36181
+ };
36182
+ size(this.dataCanvas);
36183
+ size(this.backdropCanvas);
36184
+ size(this.volumeCanvas);
36185
+ for (const l of this.extLayers) size(l.canvas);
36186
+ size(this.vpvrCanvas);
36187
+ size(this.chromeCanvas);
36188
+ size(this.drawingsCanvas);
36189
+ size(this.cursorCanvas);
35715
36190
  this.coords.setSize(Math.max(1, pw - this.rightAxisW), Math.max(1, ph - TIME_AXIS_H), dpr);
35716
36191
  this.scene.crosshair = null;
35717
36192
  this.layoutPanes();
35718
- this.repositionTables();
35719
36193
  this.userDrawings?.onResize();
35720
36194
  if (!this.didInitialFit && this.coords.barCount > 0) {
35721
36195
  this.fitContent();
@@ -37034,9 +37508,182 @@ var Watermark = class {
37034
37508
  }
37035
37509
  };
37036
37510
 
37511
+ // src/widget/cell-controls.ts
37512
+ var CELL_CONTROLS_PROXIMITY_PX = 120;
37513
+ var TIME_AXIS_H2 = 22;
37514
+ var CONTROLS_BOTTOM_PX = TIME_AXIS_H2 + 12;
37515
+ var CLUSTER_H2 = 24;
37516
+ var CLUSTER_PILL2 = "rgba(0,0,0,0.65)";
37517
+ var CLUSTER_LEFT_CSS = "calc((100% + var(--vela-toolbar-gutter, 0px) - var(--vela-scale-gutter, 0px)) / 2)";
37518
+ var STYLE_ID26 = "vela-cell-controls";
37519
+ var CSS23 = `
37520
+ .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;}
37521
+ .vela-cc-btn svg{display:block;}
37522
+ .vela-cc-btn:hover{background:var(--vela-active);color:var(--vela-fg-bright);}
37523
+ .vela-cc-on,.vela-cc-on:hover{background:var(--vela-selected-bg);color:var(--vela-selected-fg);}
37524
+ .vela-cc-grip{cursor:grab;touch-action:none;}
37525
+ .vela-cc-grip:active{cursor:grabbing;}
37526
+ `;
37527
+ function plotCenterX(width, toolbarGutter = 0, scaleGutter = 0) {
37528
+ return (width + toolbarGutter - scaleGutter) / 2;
37529
+ }
37530
+ function nearBottomCenter(x, y, width, height, proximityPx = CELL_CONTROLS_PROXIMITY_PX, gutters = {}) {
37531
+ const cx = plotCenterX(width, gutters.toolbar ?? 0, gutters.scale ?? 0);
37532
+ const cy = height - CONTROLS_BOTTOM_PX - CLUSTER_H2 / 2;
37533
+ return Math.hypot(x - cx, y - cy) <= proximityPx;
37534
+ }
37535
+ var CellControls = class {
37536
+ constructor(host, deps) {
37537
+ this.host = host;
37538
+ this.deps = deps;
37539
+ this.near = false;
37540
+ /** A grip drag is underway — the proximity reveal must not hide the cluster
37541
+ * while captured pointer moves sweep across the whole grid. */
37542
+ this.dragging = false;
37543
+ /** Mobile: the proximity reveal is meaningless without a cursor — the mobile
37544
+ * bar's maximize stop replaces the cluster. */
37545
+ this.suspended = false;
37546
+ this.onHostMove = (e) => {
37547
+ if (this.suspended) return;
37548
+ if (this.dragging) return;
37549
+ const rect = this.host.getBoundingClientRect();
37550
+ this.setNear(nearBottomCenter(e.clientX - rect.left, e.clientY - rect.top, rect.width, rect.height, CELL_CONTROLS_PROXIMITY_PX, this.hostGutters()));
37551
+ };
37552
+ this.onHostLeave = () => {
37553
+ if (this.dragging) return;
37554
+ this.setNear(false);
37555
+ };
37556
+ injectStyles(STYLE_ID26, CSS23, host.ownerDocument);
37557
+ this.glider = new Glider(deps.chart);
37558
+ this.root = host.ownerDocument.createElement("div");
37559
+ Object.assign(this.root.style, {
37560
+ position: "absolute",
37561
+ left: CLUSTER_LEFT_CSS,
37562
+ bottom: `${CONTROLS_BOTTOM_PX}px`,
37563
+ transform: "translateX(-50%)",
37564
+ zIndex: "6",
37565
+ display: "none",
37566
+ // revealed by cursor proximity (onHostMove)
37567
+ gap: "2px",
37568
+ padding: "2px",
37569
+ borderRadius: "var(--vela-radius-md)",
37570
+ background: CLUSTER_PILL2,
37571
+ pointerEvents: "auto"
37572
+ });
37573
+ this.host.addEventListener("pointermove", this.onHostMove);
37574
+ this.host.addEventListener("pointerleave", this.onHostLeave);
37575
+ this.host.appendChild(this.root);
37576
+ this.refresh();
37577
+ }
37578
+ /** Rebuild the buttons (the multi-cell gate or the maximized state changed). */
37579
+ refresh() {
37580
+ this.root.textContent = "";
37581
+ const multi = this.deps.multiCell();
37582
+ const maximized = multi && this.deps.isMaximized();
37583
+ if (multi && !maximized) this.root.appendChild(this.makeGrip());
37584
+ this.root.appendChild(this.button("minus", "Zoom out", () => this.glider.zoom(ZOOM_OUT)));
37585
+ this.root.appendChild(this.button("plus", "Zoom in", () => this.glider.zoom(ZOOM_IN)));
37586
+ if (multi) {
37587
+ this.root.appendChild(
37588
+ this.button(maximized ? "restore" : "maximize", maximized ? "Restore layout" : "Maximize chart", () => this.deps.toggleMaximize(), {
37589
+ // The maximized state reads as an inverse chip (white-on-dark, dark-on-light),
37590
+ // the same active-state affordance as a collapsed pane's expand button.
37591
+ selected: maximized
37592
+ })
37593
+ );
37594
+ }
37595
+ this.root.appendChild(
37596
+ this.button("reset", "Reset chart", () => {
37597
+ this.glider.stop();
37598
+ this.deps.reset();
37599
+ })
37600
+ );
37601
+ }
37602
+ button(iconId, title, onClick, opts = {}) {
37603
+ const b = this.host.ownerDocument.createElement("button");
37604
+ b.type = "button";
37605
+ b.title = title;
37606
+ b.setAttribute("aria-label", title);
37607
+ b.className = opts.selected === true ? "vela-cc-btn vela-cc-on" : "vela-cc-btn";
37608
+ b.innerHTML = icon(iconId);
37609
+ b.addEventListener("click", (e) => {
37610
+ e.stopPropagation();
37611
+ onClick();
37612
+ });
37613
+ return b;
37614
+ }
37615
+ /** The drag handle (2×3 dot grip): press and drag onto another cell to trade
37616
+ * slots with it. The preview highlight follows the pointer; releasing outside
37617
+ * any other cell cancels. */
37618
+ makeGrip() {
37619
+ const b = this.host.ownerDocument.createElement("button");
37620
+ b.type = "button";
37621
+ b.title = "Drag to move chart";
37622
+ b.setAttribute("aria-label", "Drag to move chart");
37623
+ b.className = "vela-cc-btn vela-cc-grip";
37624
+ b.innerHTML = icon("grip");
37625
+ b.addEventListener("pointerdown", (e) => this.onGripDown(b, e));
37626
+ return b;
37627
+ }
37628
+ onGripDown(btn2, e) {
37629
+ if (e.button !== 0 && e.pointerType === "mouse") return;
37630
+ e.preventDefault();
37631
+ e.stopPropagation();
37632
+ try {
37633
+ btn2.setPointerCapture(e.pointerId);
37634
+ } catch {
37635
+ }
37636
+ this.dragging = true;
37637
+ let target = null;
37638
+ const move = (ev) => {
37639
+ target = this.deps.dragTargetAt(ev.clientX, ev.clientY);
37640
+ this.deps.previewDrop(target);
37641
+ };
37642
+ const finish = (commit) => () => {
37643
+ this.dragging = false;
37644
+ this.deps.previewDrop(null);
37645
+ btn2.removeEventListener("pointermove", move);
37646
+ btn2.removeEventListener("pointerup", onUp);
37647
+ btn2.removeEventListener("pointercancel", onCancel);
37648
+ if (commit && target != null) this.deps.dropOn(target);
37649
+ };
37650
+ const onUp = finish(true);
37651
+ const onCancel = finish(false);
37652
+ btn2.addEventListener("pointermove", move);
37653
+ btn2.addEventListener("pointerup", onUp);
37654
+ btn2.addEventListener("pointercancel", onCancel);
37655
+ }
37656
+ /** Mobile flips the cluster off entirely (and hides it if currently revealed). */
37657
+ setSuspended(on) {
37658
+ this.suspended = on;
37659
+ if (on) this.setNear(false);
37660
+ }
37661
+ /** Live renderer gutters on the cell host (0 when unpublished — a test stub). */
37662
+ hostGutters() {
37663
+ const view = this.host.ownerDocument.defaultView;
37664
+ if (!view) return { toolbar: 0, scale: 0 };
37665
+ const cs = view.getComputedStyle(this.host);
37666
+ return {
37667
+ toolbar: Number.parseFloat(cs.getPropertyValue("--vela-toolbar-gutter")) || 0,
37668
+ scale: Number.parseFloat(cs.getPropertyValue("--vela-scale-gutter")) || 0
37669
+ };
37670
+ }
37671
+ setNear(near) {
37672
+ if (near === this.near) return;
37673
+ this.near = near;
37674
+ this.root.style.display = near ? "flex" : "none";
37675
+ }
37676
+ destroy() {
37677
+ this.glider.stop();
37678
+ this.host.removeEventListener("pointermove", this.onHostMove);
37679
+ this.host.removeEventListener("pointerleave", this.onHostLeave);
37680
+ this.root.remove();
37681
+ }
37682
+ };
37683
+
37037
37684
  // src/widget/context-menu.ts
37038
37685
  var PRICE_AXIS_W = 60;
37039
- var TIME_AXIS_H2 = 26;
37686
+ var TIME_AXIS_H3 = 26;
37040
37687
  var ChartContextMenu = class {
37041
37688
  constructor(host, cbs) {
37042
37689
  this.cbs = cbs;
@@ -37075,7 +37722,7 @@ var ChartContextMenu = class {
37075
37722
  zoneOf(e) {
37076
37723
  const rect = this.host.getBoundingClientRect();
37077
37724
  if (e.clientX - rect.left > rect.width - PRICE_AXIS_W) return "price-axis";
37078
- if (e.clientY - rect.top > rect.height - TIME_AXIS_H2) return "time-axis";
37725
+ if (e.clientY - rect.top > rect.height - TIME_AXIS_H3) return "time-axis";
37079
37726
  return "body";
37080
37727
  }
37081
37728
  /** The pane under the pointer, so every pane's price scale has its own menu. */
@@ -37261,6 +37908,12 @@ function cellDrawings(opt) {
37261
37908
  if (opt === true || opt == null) return { toolbar: false };
37262
37909
  return { ...opt, toolbar: false };
37263
37910
  }
37911
+ function instanceDeltas(handle) {
37912
+ if (!handle) return void 0;
37913
+ const inputs = inputDeltas(handle.inputs, handle.inputValues());
37914
+ const props = inputDeltas(handle.props, handle.propValues());
37915
+ return inputs || props ? { ...inputs ? { inputs } : {}, ...props ? { props } : {} } : void 0;
37916
+ }
37264
37917
  var ChartCell = class {
37265
37918
  constructor(id, gridHost, seed, deps) {
37266
37919
  this.id = id;
@@ -37409,11 +38062,18 @@ var ChartCell = class {
37409
38062
  if (this.inner && this.state.symbol) this.marketStatus?.track(this.inner.data, this.state.symbol);
37410
38063
  });
37411
38064
  this.syncStatuslineColors();
38065
+ this.cellControls = new CellControls(this.host, {
38066
+ chart: () => this.inner,
38067
+ reset: () => this.resetView(),
38068
+ multiCell: () => deps.multiCell(),
38069
+ isMaximized: () => deps.isMaximized(id),
38070
+ toggleMaximize: () => deps.toggleMaximize(id),
38071
+ dragTargetAt: (x, y) => deps.cellDragTarget(id, x, y),
38072
+ previewDrop: (target) => deps.previewDropTarget(target),
38073
+ dropOn: (target) => deps.dropCell(id, target)
38074
+ });
37412
38075
  this.contextMenu = new ChartContextMenu(this.host, {
37413
- resetView: () => {
37414
- this.inner?.renderer.set("autoScale", true);
37415
- this.inner?.setVisibleRangePreset("ALL");
37416
- },
38076
+ resetView: () => this.resetView(),
37417
38077
  timezone: () => this.deps.timezone(),
37418
38078
  setTimezone: (zone) => this.deps.setTimezone(zone),
37419
38079
  // Right-clicking activates the cell first (capture-phase pointerdown), so the
@@ -37429,6 +38089,7 @@ var ChartCell = class {
37429
38089
  this.syncPresentNatives();
37430
38090
  this.refreshNativeCatalog();
37431
38091
  });
38092
+ this.inner.on("indicator:inputs", () => this.deps.onStateDirty());
37432
38093
  this.inner.on("indicator:removed", ({ id: id2 }) => {
37433
38094
  if (this.destroyed) return;
37434
38095
  const idx = this.instances.findIndex((it) => it.handle?.id === id2);
@@ -37437,7 +38098,7 @@ var ChartCell = class {
37437
38098
  this.instances.splice(idx, 1);
37438
38099
  this.history.push({
37439
38100
  undo: () => {
37440
- snapshot.handle = this.addToChart(snapshot.entry);
38101
+ snapshot.handle = this.addToChart(snapshot.entry, snapshot.values);
37441
38102
  this.instances.push(snapshot);
37442
38103
  this.deps.onIndicatorsChanged(this.id);
37443
38104
  },
@@ -37787,6 +38448,20 @@ var ChartCell = class {
37787
38448
  this.inner.setVisibleRangePreset(preset.preset);
37788
38449
  }
37789
38450
  }
38451
+ /** Reset this cell's view: re-enable auto scale and frame the full history —
38452
+ * the same action the chart context menu offers. */
38453
+ resetView() {
38454
+ this.inner?.renderer.set("autoScale", true);
38455
+ this.inner?.setVisibleRangePreset("ALL");
38456
+ }
38457
+ /** Rebuild the view-controls cluster (the maximize gate or state changed). */
38458
+ refreshControls() {
38459
+ this.cellControls.refresh();
38460
+ }
38461
+ /** Mobile flips the per-cell cluster off (the shell's mobile bar replaces it). */
38462
+ setControlsSuspended(on) {
38463
+ this.cellControls.setSuspended(on);
38464
+ }
37790
38465
  /** Make this cell the active one and put keyboard focus on its chart surface. */
37791
38466
  focus() {
37792
38467
  this.deps.activate(this.id);
@@ -37812,9 +38487,9 @@ var ChartCell = class {
37812
38487
  this.manifest = list;
37813
38488
  if (this.pendingManifestNames) {
37814
38489
  if (list.length === 0) return;
37815
- for (const name of this.pendingManifestNames) {
37816
- const entry = list.find((e) => e.name === name);
37817
- if (entry) this.addManifestInstance(entry, { record: false });
38490
+ for (const led of this.pendingManifestNames) {
38491
+ const entry = list.find((e) => e.name === ledgerEntryName(led));
38492
+ if (entry) this.addManifestInstance(entry, { record: false, ...typeof led === "object" ? { inputs: led.inputs, props: led.props } : {} });
37818
38493
  }
37819
38494
  this.pendingManifestNames = null;
37820
38495
  return;
@@ -37844,9 +38519,9 @@ var ChartCell = class {
37844
38519
  }
37845
38520
  for (const it of [...this.instances]) this.dropInstance(it);
37846
38521
  if (this.manifest.length > 0) {
37847
- for (const name of led.manifest) {
37848
- const entry = this.manifest.find((e) => e.name === name);
37849
- if (entry) this.addManifestInstance(entry, { record: false });
38522
+ for (const item of led.manifest) {
38523
+ const entry = this.manifest.find((e) => e.name === ledgerEntryName(item));
38524
+ if (entry) this.addManifestInstance(entry, { record: false, ...typeof item === "object" ? { inputs: item.inputs, props: item.props } : {} });
37850
38525
  }
37851
38526
  this.pendingManifestNames = null;
37852
38527
  } else if (!this.deps.manifestSettled()) {
@@ -37895,12 +38570,13 @@ var ChartCell = class {
37895
38570
  * a persistence handler's `restore` runs silently, a user-driven call records.
37896
38571
  */
37897
38572
  addExternalIndicator(entry) {
37898
- this.addManifestInstance({ ...entry, enabled: true }, { external: true });
38573
+ this.addManifestInstance({ ...entry, enabled: true }, { external: true, ...entry.inputs ? { inputs: entry.inputs } : {}, ...entry.props ? { props: entry.props } : {} });
37899
38574
  }
37900
38575
  /** Add ONE instance of a manifest entry (repeatable — duplicates are legitimate). */
37901
38576
  addManifestInstance(entry, opts = {}) {
37902
38577
  if (this.destroyed) return;
37903
- const it = { entry, handle: this.addToChart(entry), ...opts.external ? { external: true } : {} };
38578
+ const values = opts.inputs || opts.props ? { inputs: opts.inputs, props: opts.props } : void 0;
38579
+ const it = { entry, handle: this.addToChart(entry, values), ...opts.external ? { external: true } : {}, ...values ? { values } : {} };
37904
38580
  this.instances.push(it);
37905
38581
  this.deps.onIndicatorsChanged(this.id);
37906
38582
  if (opts.record === false) return;
@@ -37908,7 +38584,7 @@ var ChartCell = class {
37908
38584
  this.history.push({
37909
38585
  undo: () => this.dropInstance(snapshot),
37910
38586
  redo: () => {
37911
- snapshot.handle = this.addToChart(snapshot.entry);
38587
+ snapshot.handle = this.addToChart(snapshot.entry, snapshot.values);
37912
38588
  this.instances.push(snapshot);
37913
38589
  this.deps.onIndicatorsChanged(this.id);
37914
38590
  }
@@ -37921,7 +38597,7 @@ var ChartCell = class {
37921
38597
  const snapshot = it;
37922
38598
  this.history.push({
37923
38599
  undo: () => {
37924
- snapshot.handle = this.addToChart(snapshot.entry);
38600
+ snapshot.handle = this.addToChart(snapshot.entry, snapshot.values);
37925
38601
  this.instances.push(snapshot);
37926
38602
  this.deps.onIndicatorsChanged(this.id);
37927
38603
  },
@@ -37931,6 +38607,9 @@ var ChartCell = class {
37931
38607
  dropInstance(it) {
37932
38608
  const idx = this.instances.indexOf(it);
37933
38609
  if (idx >= 0) this.instances.splice(idx, 1);
38610
+ const captured = instanceDeltas(it.handle);
38611
+ if (captured) it.values = captured;
38612
+ else delete it.values;
37934
38613
  try {
37935
38614
  it.handle?.remove();
37936
38615
  } catch {
@@ -37973,9 +38652,13 @@ var ChartCell = class {
37973
38652
  this.deps.onIndicatorsChanged(this.id);
37974
38653
  });
37975
38654
  }
37976
- addToChart(entry) {
38655
+ addToChart(entry, values) {
37977
38656
  try {
37978
- return this.inner?.addIndicator(entry.script, entry.language !== void 0 ? { language: entry.language } : void 0) ?? null;
38657
+ return this.inner?.addIndicator(entry.script, {
38658
+ ...entry.language !== void 0 ? { language: entry.language } : {},
38659
+ ...values?.inputs ? { inputs: values.inputs } : {},
38660
+ ...values?.props ? { props: values.props } : {}
38661
+ }) ?? null;
37979
38662
  } catch (err) {
37980
38663
  console.warn(`[vela] indicator "${entry.name}" failed to add:`, err);
37981
38664
  return null;
@@ -38089,7 +38772,10 @@ var ChartCell = class {
38089
38772
  // manifest — their plugin persists them via the `ext` seam instead.
38090
38773
  indicators: indicatorLedger({
38091
38774
  present: this.inner ? this.inner.presentNativeIndicators() : [],
38092
- instanceNames: this.instances.filter((it) => !it.external).map((it) => it.entry.name),
38775
+ instanceEntries: this.instances.filter((it) => !it.external).map((it) => {
38776
+ const d = it.handle ? instanceDeltas(it.handle) : it.values;
38777
+ return d ? { name: it.entry.name, ...d } : it.entry.name;
38778
+ }),
38093
38779
  pendingManifest: this.pendingManifestNames,
38094
38780
  manifestSettled: this.deps.manifestSettled(),
38095
38781
  volumePending: this.volumeMayBePending && this.volumeIntent
@@ -38100,6 +38786,7 @@ var ChartCell = class {
38100
38786
  destroy() {
38101
38787
  this.destroyed = true;
38102
38788
  this.offMarket();
38789
+ this.cellControls.destroy();
38103
38790
  this.contextMenu.destroy();
38104
38791
  this.history.destroy();
38105
38792
  this.marketStatus?.stop();
@@ -38392,10 +39079,10 @@ var SplitterLayer = class {
38392
39079
  var DEFAULT_TIMEFRAMES = ["1", "5", "15", "60", "240", "D", "W"];
38393
39080
  var GAP_PX = 2;
38394
39081
  var POOL_CAP = 16;
38395
- var TIME_AXIS_H3 = 22;
39082
+ var TIME_AXIS_H4 = 22;
38396
39083
  var ALERT_CAP = 50;
38397
- var STYLE_ID26 = "vela-workspace";
38398
- var CSS23 = `
39084
+ var STYLE_ID27 = "vela-workspace";
39085
+ var CSS24 = `
38399
39086
  .vela-workspace { position: relative; width: 100%; height: 100%; display: flex; flex-direction: column; background: var(--vela-bg); }
38400
39087
  .vela-ws-main { position: relative; display: flex; flex-direction: row; flex: 1 1 auto; min-height: 0; }
38401
39088
  .vela-ws-toolbar { position: relative; flex: none; }
@@ -38422,6 +39109,21 @@ var CSS23 = `
38422
39109
  /* Mobile: the docked drawing-toolbar column would eat a phone-width grid \u2014 the shell's
38423
39110
  drawings drawer + on-chart pill replace it (same policy as the widget's in-chart bar). */
38424
39111
  [data-layout='mobile'] .vela-ws-toolbar { display: none; }
39112
+ /* A maximized cell owns the whole grid: the splitter strips have no seams to grab and
39113
+ the active ring would just outline the only visible chart \u2014 both are noise here. */
39114
+ .vela-ws-grid[data-maximized='1'] .vela-ws-splitter { display: none; }
39115
+ .vela-ws-grid[data-maximized='1'] .vela-cell[data-active='1']::after { display: none; }
39116
+ /* Drop-target preview while a cell's drag handle is held: a dashed ring + the same
39117
+ soft wash the splitter hover uses, over the chart, inert to the pointer. */
39118
+ .vela-cell[data-drop-target='1']::before {
39119
+ content: '';
39120
+ position: absolute;
39121
+ inset: 0;
39122
+ border: 2px dashed var(--vela-fg-bright);
39123
+ background: var(--vela-separator-hover-band);
39124
+ pointer-events: none;
39125
+ z-index: 11;
39126
+ }
38425
39127
  `;
38426
39128
  registerIcon("layout", svg16('<rect x="1.5" y="1.5" width="13" height="13" rx="1.5"/><path d="M8 1.5v13M1.5 8h13"/>'));
38427
39129
  function declaredOrder(cells) {
@@ -38451,6 +39153,9 @@ var VelaWorkspace = class {
38451
39153
  * slots beyond the list get auto identities. Grows, never reorders. */
38452
39154
  this.order = [];
38453
39155
  this.activeId = null;
39156
+ /** The cell maximized over the whole grid (null = normal grid). TRANSIENT view
39157
+ * state — never persisted; any structural change (layout, applyState) restores. */
39158
+ this.maximizedId = null;
38454
39159
  this.cellBackend = "auto";
38455
39160
  this.destroyed = false;
38456
39161
  this.shortcutsHelp = null;
@@ -38555,7 +39260,7 @@ var VelaWorkspace = class {
38555
39260
  this.order = boot?.charts ? boot.charts.map((c) => c.id) : declaredOrder(opts.cells);
38556
39261
  const bootActive = boot?.activeCellId ?? null;
38557
39262
  const doc = hostEl.ownerDocument;
38558
- injectStyles(STYLE_ID26, CSS23, doc);
39263
+ injectStyles(STYLE_ID27, CSS24, doc);
38559
39264
  this.root = doc.createElement("div");
38560
39265
  this.root.className = "vela-workspace";
38561
39266
  ensureUIHost(this.root, resolveTheme(opts.theme));
@@ -38665,7 +39370,11 @@ var VelaWorkspace = class {
38665
39370
  if (attribution !== false) {
38666
39371
  const background = resolveTheme(opts.theme).background;
38667
39372
  const mark = typeof attribution === "string" && attribution.trim() ? createCustomMark(doc, attribution, background) : createAttributionMark(doc, background);
38668
- Object.assign(mark.style, { left: "12px", bottom: `${TIME_AXIS_H3 + 10}px`, zIndex: "11" });
39373
+ Object.assign(mark.style, {
39374
+ left: "calc(var(--vela-toolbar-gutter, 0px) + 12px)",
39375
+ bottom: `calc(var(--vela-bottom-gutter, ${TIME_AXIS_H4}px) + 10px)`,
39376
+ zIndex: "11"
39377
+ });
38669
39378
  this.gridEl.appendChild(mark);
38670
39379
  this.attributionMark = mark;
38671
39380
  }
@@ -38731,6 +39440,9 @@ var VelaWorkspace = class {
38731
39440
  ...topbarHas(this.topbarComp, "indicators") && (picker || this.indicatorsOverride) ? { onIndicatorsClick: this.indicatorsOverride ? () => this.runOverride(this.indicatorsOverride) : () => picker.open() } : {},
38732
39441
  getContext: () => this.context(),
38733
39442
  ...this.drawingsEnabled ? { onDrawingsClick: () => this.openDrawingsDrawer() } : {},
39443
+ // Multi-chart only: the stop that isolates the ACTIVE chart (the
39444
+ // per-cell hover cluster has no cursor to reveal it on mobile).
39445
+ ...this.monoLayout ? {} : { onMaximizeClick: () => this.toggleMobileMaximize() },
38734
39446
  onMoreClick: () => this.openMoreDrawer(),
38735
39447
  onSettingsClick: () => this.active.chart.renderer.openSettings()
38736
39448
  }) : null;
@@ -38937,7 +39649,9 @@ var VelaWorkspace = class {
38937
39649
  this.pool.clear();
38938
39650
  for (const { id, ...cs } of st.charts.slice(liveCount)) this.pool.set(id, cs);
38939
39651
  this.order = st.charts.map((c) => c.id);
39652
+ this.clearMaximized();
38940
39653
  this.applyGrid();
39654
+ this.refreshCellControls();
38941
39655
  const nextActive2 = st.activeCellId && this.cellsById.has(st.activeCellId) ? st.activeCellId : this.order[0] ?? null;
38942
39656
  if (nextActive2 === this.activeId) this.projectActiveCell();
38943
39657
  else this.setActiveCell(nextActive2);
@@ -38963,6 +39677,7 @@ var VelaWorkspace = class {
38963
39677
  const def = this.monoLayout ? null : ensureLayout(st.layout);
38964
39678
  if (def) this.def = def;
38965
39679
  this.cellBackend = this.backendFor(this.def);
39680
+ this.clearMaximized();
38966
39681
  this.applyGrid();
38967
39682
  this.buildCells();
38968
39683
  this.syncCellPresentation();
@@ -39026,6 +39741,7 @@ var VelaWorkspace = class {
39026
39741
  setLayout(layout) {
39027
39742
  if (this.destroyed) return;
39028
39743
  if (this.monoLayout) return;
39744
+ this.clearMaximized();
39029
39745
  const next = this.resolveLayout(layout);
39030
39746
  const nextBackend = this.backendFor(next);
39031
39747
  const rebuildAll = nextBackend !== this.cellBackend;
@@ -39046,6 +39762,7 @@ var VelaWorkspace = class {
39046
39762
  this.buildCells();
39047
39763
  this.alignNewCellStyles(preexisting);
39048
39764
  this.syncCellPresentation();
39765
+ this.refreshCellControls();
39049
39766
  this.topbar.setLayout(next.id);
39050
39767
  const nextActive = activeAfterLayout(this.activeId, this.order.slice(0, next.cells.length));
39051
39768
  if (nextActive === this.activeId) this.projectActiveCell();
@@ -39054,6 +39771,67 @@ var VelaWorkspace = class {
39054
39771
  this.events.emit("layout:changed", { layout: next.id });
39055
39772
  this.markStateDirty();
39056
39773
  }
39774
+ /** The identity of the cell maximized over the whole grid, or null. */
39775
+ get maximizedCell() {
39776
+ return this.maximizedId;
39777
+ }
39778
+ /**
39779
+ * Maximize one cell over the whole grid, or restore the layout with `null`. Pure
39780
+ * presentation: the other cells stay alive underneath — charts, subscriptions and
39781
+ * state untouched — so restoring is instant. The maximized cell becomes the active
39782
+ * one. Transient view state (also reachable from each cell's bottom-center view
39783
+ * cluster): switching layouts or applying a state document restores the grid.
39784
+ */
39785
+ maximizeCell(id) {
39786
+ if (this.destroyed) return;
39787
+ if (id != null && (!this.cellsById.has(id) || this.def.cells.length <= 1)) return;
39788
+ if (id === this.maximizedId) return;
39789
+ this.maximizedId = id;
39790
+ if (id) this.setActiveCell(id);
39791
+ this.applyGrid();
39792
+ this.refreshCellControls();
39793
+ this.syncMobileMaximize();
39794
+ this.events.emit("cell:maximized", { id });
39795
+ }
39796
+ /** The mobile bar's maximize stop: one press isolates the ACTIVE chart over the
39797
+ * grid; while something is already isolated — the chart, or a pane inside it
39798
+ * (mobile's double-tap) — the press restores that instead. Every branch re-syncs
39799
+ * the stop on its own (`maximizeCell` directly, `panes.maximize` via its
39800
+ * synchronous `pane:changed`). */
39801
+ toggleMobileMaximize() {
39802
+ const cell = this.activeId ? this.cellsById.get(this.activeId) : void 0;
39803
+ if (!cell) return;
39804
+ if (this.maximizedId) this.maximizeCell(null);
39805
+ else if (cell.chart.panes.list().some((p) => p.maximized)) cell.chart.panes.maximize(null);
39806
+ else this.maximizeCell(cell.id);
39807
+ }
39808
+ /** Keep the mobile bar's maximize stop truthful: lit (inverse chip, restore
39809
+ * glyph) while the active chart covers the grid OR one of its panes is
39810
+ * maximized — the state a double-tap toggles is otherwise invisible on mobile. */
39811
+ syncMobileMaximize() {
39812
+ if (!this.mobileBar) return;
39813
+ const cell = this.activeId ? this.cellsById.get(this.activeId) : void 0;
39814
+ const paneMax = cell ? cell.chart.panes.list().some((p) => p.maximized) : false;
39815
+ this.mobileBar.setMaximizeActive(this.maximizedId != null || paneMax);
39816
+ }
39817
+ /**
39818
+ * Trade the SLOTS of two live cells — the grid arrangement changes, the cells
39819
+ * themselves (charts, indicators, drawings, the active flag) stay untouched.
39820
+ * What each cell's drag handle commits; also callable directly by hosts.
39821
+ */
39822
+ swapCells(a, b) {
39823
+ if (this.destroyed || a === b) return;
39824
+ const i = this.order.indexOf(a);
39825
+ const j = this.order.indexOf(b);
39826
+ if (i < 0 || j < 0 || !this.cellsById.has(a) || !this.cellsById.has(b)) return;
39827
+ [this.order[i], this.order[j]] = [this.order[j], this.order[i]];
39828
+ for (const [k] of this.def.cells.entries()) {
39829
+ const host = this.cellsById.get(this.order[k] ?? "")?.host;
39830
+ if (host) this.gridEl.appendChild(host);
39831
+ }
39832
+ this.applyGrid();
39833
+ this.markStateDirty();
39834
+ }
39057
39835
  resize() {
39058
39836
  this.splitters.layout();
39059
39837
  }
@@ -39123,6 +39901,7 @@ var VelaWorkspace = class {
39123
39901
  this.mobileBar?.renderActions();
39124
39902
  this.mobileBar?.setSymbol(cell.symbol);
39125
39903
  this.mobileBar?.setTimeframe(cell.timeframe);
39904
+ this.syncMobileMaximize();
39126
39905
  this.drawingPill?.onChart(cell.chart);
39127
39906
  const pushHistory = () => this.topbar.setHistoryState(cell.history.canUndo, cell.history.canRedo);
39128
39907
  this.historyUnsub?.();
@@ -39212,8 +39991,78 @@ var VelaWorkspace = class {
39212
39991
  const host = this.cellsById.get(this.order[i] ?? "")?.host;
39213
39992
  if (host) host.style.gridArea = perCell[slot.id]?.gridArea ?? "";
39214
39993
  }
39994
+ this.applyMaximizePresentation();
39995
+ this.mountAttributionMark();
39215
39996
  this.splitters.layout();
39216
39997
  }
39998
+ /** Overlay the maximize presentation on the freshly applied grid: EVERY cell spans
39999
+ * the full track grid — the maximized one on top, the siblings invisible beneath
40000
+ * it (their charts stay alive — restoring is instant). The siblings must span too:
40001
+ * left in their slots they would auto-flow into implicit zero-height rows, whose
40002
+ * gaps steal height from the maximized cell and collapse their renderers to 0.
40003
+ * The splitter strips and the active ring hide via the `data-maximized` rules. */
40004
+ applyMaximizePresentation() {
40005
+ const maxId = this.maximizedId;
40006
+ if (maxId) this.gridEl.dataset.maximized = "1";
40007
+ else delete this.gridEl.dataset.maximized;
40008
+ for (const [id, cell] of this.cellsById) {
40009
+ const style = cell.host.style;
40010
+ if (maxId) style.gridArea = "1 / 1 / -1 / -1";
40011
+ style.zIndex = maxId && id === maxId ? "5" : "";
40012
+ style.visibility = maxId && id !== maxId ? "hidden" : "";
40013
+ }
40014
+ }
40015
+ /** Rebuild every cell's view cluster (the maximize gate or state changed). */
40016
+ refreshCellControls() {
40017
+ for (const cell of this.cellsById.values()) cell.refreshControls();
40018
+ }
40019
+ /** Drop the transient maximize on a structural change (layout switch, state
40020
+ * document) — WITH the event, so hosts tracking `cell:maximized` never drift
40021
+ * from `maximizedCell`. The caller's own grid re-apply paints the restore. */
40022
+ clearMaximized() {
40023
+ if (this.maximizedId == null) return;
40024
+ this.maximizedId = null;
40025
+ this.events.emit("cell:maximized", { id: null });
40026
+ }
40027
+ /** The live cell under a viewport point, excluding `excludeId` and any host a
40028
+ * maximize has hidden — the drag handle's hit-test. */
40029
+ cellAtPoint(x, y, excludeId) {
40030
+ for (const [id, cell] of this.cellsById) {
40031
+ if (id === excludeId || cell.host.style.visibility === "hidden") continue;
40032
+ const r = cell.host.getBoundingClientRect();
40033
+ if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return id;
40034
+ }
40035
+ return null;
40036
+ }
40037
+ /** Mark one cell as the live drop target of a grip drag (null clears all) —
40038
+ * the `data-drop-target` stylesheet rule paints the dashed preview ring. */
40039
+ setDropTarget(id) {
40040
+ for (const [cid, cell] of this.cellsById) {
40041
+ if (cid === id) cell.host.dataset.dropTarget = "1";
40042
+ else delete cell.host.dataset.dropTarget;
40043
+ }
40044
+ }
40045
+ /** The cell whose bottom-left corner the grid's attribution mark floats in — the
40046
+ * maximized cell while one covers the grid, else the bottom-left slot's cell. */
40047
+ bottomLeftCell() {
40048
+ if (this.maximizedId) return this.cellsById.get(this.maximizedId);
40049
+ const grid = occupancyGrid(this.def);
40050
+ const slot = grid[grid.length - 1]?.[0];
40051
+ const idx = this.def.cells.findIndex((c) => (c.area ?? c.id) === slot);
40052
+ return this.cellsById.get(this.order[idx >= 0 ? idx : 0] ?? "");
40053
+ }
40054
+ /** Keep the shared attribution mark inside the BOTTOM-LEFT visible cell: its
40055
+ * offsets ride that cell's renderer-published `--vela-bottom-gutter` /
40056
+ * `--vela-toolbar-gutter`, so collapsed pane strips push the mark up without any
40057
+ * bookkeeping here. Re-run after anything that changes which host that is
40058
+ * (layout switch, maximize, cell rebuild); a destroyed host drops the mark from
40059
+ * the DOM, and this re-mount brings it back. */
40060
+ mountAttributionMark() {
40061
+ const mark = this.attributionMark;
40062
+ if (!mark) return;
40063
+ const host = this.bottomLeftCell()?.host ?? this.gridEl;
40064
+ if (mark.parentElement !== host) host.appendChild(mark);
40065
+ }
39217
40066
  /** Create the cells the current layout wants but don't exist yet (pool-first).
39218
40067
  * A slot's CELL IDENTITY is `order[i]` (declaration order — never the slot's own
39219
40068
  * positional id); slots past the declared list mint an auto identity once. */
@@ -39245,6 +40094,12 @@ var VelaWorkspace = class {
39245
40094
  setTimezone: (zone) => this.setTimezone(zone),
39246
40095
  context: () => this.context(),
39247
40096
  activate: (id2) => this.setActiveCell(id2),
40097
+ multiCell: () => !this.monoLayout && this.def.cells.length > 1,
40098
+ isMaximized: (id2) => this.maximizedId === id2,
40099
+ toggleMaximize: (id2) => this.maximizeCell(this.maximizedId === id2 ? null : id2),
40100
+ cellDragTarget: (id2, x, y) => this.cellAtPoint(x, y, id2),
40101
+ previewDropTarget: (target) => this.setDropTarget(target),
40102
+ dropCell: (id2, target) => this.swapCells(id2, target),
39248
40103
  onMarketChanged: (id2) => this.onCellMarketChanged(id2),
39249
40104
  onPriceStyleChanged: (id2) => this.onCellPriceStyleChanged(id2),
39250
40105
  onIndicatorsChanged: (id2) => this.onCellIndicatorsChanged(id2),
@@ -39258,6 +40113,7 @@ var VelaWorkspace = class {
39258
40113
  if (id === this.activeId) cell.host.dataset.active = "1";
39259
40114
  this.wireCell(cell);
39260
40115
  cell.chart.renderer.setLayoutMode(this.layoutCtl.current);
40116
+ cell.setControlsSuspended(this.layoutCtl.current === "mobile");
39261
40117
  if (this.favs.length > 0) cell.chart.drawings.setFavorites(this.favs);
39262
40118
  cell.setManifest(this.manifest, pooled?.indicators == null);
39263
40119
  cell.restorePersistedExt();
@@ -39267,6 +40123,7 @@ var VelaWorkspace = class {
39267
40123
  const host = this.cellsById.get(this.order[i] ?? "")?.host;
39268
40124
  if (host) this.gridEl.appendChild(host);
39269
40125
  }
40126
+ this.mountAttributionMark();
39270
40127
  }
39271
40128
  /** Per-cell chart subscriptions (trigger ② — the chart instance is stable for the
39272
40129
  * cell's whole life, so these live and die with the cell). */
@@ -39326,6 +40183,9 @@ var VelaWorkspace = class {
39326
40183
  chart.on("viewport:changed", (range) => this.propagateViewport(cell.id, range));
39327
40184
  chart.renderer.onConfigChanged(() => this.propagateStylePrefs(cell.id));
39328
40185
  chart.on("theme:changed", (t) => this.setTheme(t));
40186
+ chart.on("pane:changed", () => {
40187
+ if (cell.id === this.activeId) this.syncMobileMaximize();
40188
+ });
39329
40189
  chart.renderer.onAxisLongPress((e) => {
39330
40190
  if (this.layoutCtl.current !== "mobile") return;
39331
40191
  if (e.axis === "time") this.openTimezoneDrawer();
@@ -39646,6 +40506,7 @@ var VelaWorkspace = class {
39646
40506
  for (const cell of this.cellsById.values()) {
39647
40507
  cell.chart.renderer.closeDialogs();
39648
40508
  cell.chart.renderer.setLayoutMode(mode);
40509
+ cell.setControlsSuspended(mode === "mobile");
39649
40510
  }
39650
40511
  this.syncCellPresentation();
39651
40512
  }