@luxalgo/vela 0.6.8 → 0.6.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/{DataProvider-BSLlBpB9.d.ts → DataProvider-CWmp31dA.d.ts} +49 -1
  2. package/dist/{DataProvider-BsQM2WNH.d.cts → DataProvider-l_eLMly_.d.cts} +49 -1
  3. package/dist/{chunk-L3I2CCYO.js → chunk-4KZVZ7QQ.js} +49 -2
  4. package/dist/{chunk-SQETIBFU.js → chunk-FFOP37FQ.js} +996 -377
  5. package/dist/{chunk-2R7BANEM.js → chunk-G77Y7LK2.js} +2 -2
  6. package/dist/{chunk-PN5KWFZ4.js → chunk-KG4YT3TI.js} +5 -3
  7. package/dist/{chunk-OICPLNU7.js → chunk-MHY7MVXH.js} +192 -2
  8. package/dist/{chunk-N7LGMKCE.js → chunk-NMTQXNT4.js} +637 -164
  9. package/dist/{contributions-DLLdV9jD.d.ts → contributions-BCz6Dr6a.d.ts} +99 -7
  10. package/dist/{contributions-Vw-Hm58R.d.cts → contributions-D9vTzm5p.d.cts} +99 -7
  11. package/dist/index.cjs +1210 -383
  12. package/dist/index.d.cts +37 -11
  13. package/dist/index.d.ts +37 -11
  14. package/dist/index.js +4 -4
  15. package/dist/{options-BaVTMXaO.d.ts → options-DSqHsQyN.d.cts} +84 -7
  16. package/dist/{options-BaVTMXaO.d.cts → options-DSqHsQyN.d.ts} +84 -7
  17. package/dist/{plugin-BnJgjLAy.d.cts → plugin-Bt8hLR8Z.d.cts} +3 -3
  18. package/dist/{plugin-CC7rBrOY.d.ts → plugin-CH7pfMrE.d.ts} +3 -3
  19. package/dist/plugin.cjs +21 -3
  20. package/dist/plugin.d.cts +4 -4
  21. package/dist/plugin.d.ts +4 -4
  22. package/dist/plugin.js +2 -2
  23. package/dist/providers/binance.d.cts +2 -2
  24. package/dist/providers/binance.d.ts +2 -2
  25. package/dist/providers/coinbase.d.cts +2 -2
  26. package/dist/providers/coinbase.d.ts +2 -2
  27. package/dist/providers/hyperliquid.d.cts +2 -2
  28. package/dist/providers/hyperliquid.d.ts +2 -2
  29. package/dist/{statusline-CGkB2EOo.d.ts → statusline-5K7y4Ya2.d.ts} +5 -3
  30. package/dist/{statusline-DqzBqy42.d.cts → statusline-TYLQmoeh.d.cts} +5 -3
  31. package/dist/ui.cjs +208 -5
  32. package/dist/ui.d.cts +91 -2
  33. package/dist/ui.d.ts +91 -2
  34. package/dist/ui.js +3 -3
  35. package/dist/vela.global.js +1210 -383
  36. package/dist/vela.global.min.js +99 -48
  37. package/dist/widget.cjs +1771 -449
  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 +1771 -449
  42. package/dist/workspace.d.cts +100 -6
  43. package/dist/workspace.d.ts +100 -6
  44. package/dist/workspace.js +6 -6
  45. package/package.json +1 -1
@@ -114,6 +114,7 @@ var Vela = (function (exports) {
114
114
  constructor(id, title, controller, source) {
115
115
  this.controller = controller;
116
116
  this.schema = [];
117
+ this.propsSchema = [];
117
118
  this.visibleState = true;
118
119
  this.bus = new TypedEventBus();
119
120
  this.id = id;
@@ -123,6 +124,9 @@ var Vela = (function (exports) {
123
124
  get inputs() {
124
125
  return this.schema;
125
126
  }
127
+ get props() {
128
+ return this.propsSchema;
129
+ }
126
130
  get visible() {
127
131
  return this.visibleState;
128
132
  }
@@ -132,6 +136,12 @@ var Vela = (function (exports) {
132
136
  setInputs(values) {
133
137
  this.controller.applyInputs(this.id, values);
134
138
  }
139
+ setProp(key, value) {
140
+ this.controller.applyProps(this.id, { [key]: value });
141
+ }
142
+ setProps(values) {
143
+ this.controller.applyProps(this.id, values);
144
+ }
135
145
  setVisible(visible) {
136
146
  this.controller.setVisible(this.id, visible);
137
147
  }
@@ -151,6 +161,9 @@ var Vela = (function (exports) {
151
161
  setSchema(schema) {
152
162
  this.schema = schema;
153
163
  }
164
+ setPropsSchema(schema) {
165
+ this.propsSchema = schema;
166
+ }
154
167
  /** Sync the public `visible` getter when the orchestrator changes visibility (API or legend eye). */
155
168
  setVisibleState(visible) {
156
169
  this.visibleState = visible;
@@ -164,6 +177,8 @@ var Vela = (function (exports) {
164
177
  function summarizeModel(model) {
165
178
  const series = {};
166
179
  for (const s of model.series) series[s.kind] = (series[s.kind] ?? 0) + 1;
180
+ const countOverlay = (items) => items?.filter((i) => i.overlay === true).length ?? 0;
181
+ const forcedOverlay = countOverlay(model.series) + countOverlay(model.fills) + countOverlay(model.backgrounds) + countOverlay(model.lines) + countOverlay(model.boxes) + countOverlay(model.labels) + countOverlay(model.polylines) + countOverlay(model.linefills) + countOverlay(model.tables);
167
182
  return {
168
183
  id: model.id,
169
184
  title: model.title,
@@ -183,7 +198,8 @@ var Vela = (function (exports) {
183
198
  tables: model.tables?.length ?? 0,
184
199
  barColors: model.barColors?.length ?? 0,
185
200
  trades: model.trades?.length ?? 0,
186
- inputs: model.inputs.length
201
+ inputs: model.inputs.length,
202
+ forcedOverlay
187
203
  };
188
204
  }
189
205
  function inspectModels(models) {
@@ -204,7 +220,8 @@ var Vela = (function (exports) {
204
220
  linefills: sum((s) => s.linefills),
205
221
  tables: sum((s) => s.tables),
206
222
  barColors: sum((s) => s.barColors),
207
- trades: sum((s) => s.trades)
223
+ trades: sum((s) => s.trades),
224
+ forcedOverlay: sum((s) => s.forcedOverlay)
208
225
  }
209
226
  };
210
227
  }
@@ -474,11 +491,11 @@ var Vela = (function (exports) {
474
491
  );
475
492
  registerIcon(
476
493
  "market-pre",
477
- svg24('<path d="M12 2v8"/><path d="m4.93 10.93 1.41 1.41"/><path d="M2 18h2"/><path d="M20 18h2"/><path d="m19.07 10.93-1.41 1.41"/><path d="M22 22H2"/><path d="m16 6-4 4-4-4"/><path d="M16 18a4 4 0 0 0-8 0"/>')
494
+ svg24('<path d="M12 2v8"/><path d="m4.93 10.93 1.41 1.41"/><path d="M2 18h2"/><path d="M20 18h2"/><path d="m19.07 10.93-1.41 1.41"/><path d="M22 22H2"/><path d="m8 6 4-4 4 4"/><path d="M16 18a4 4 0 0 0-8 0"/>')
478
495
  );
479
496
  registerIcon(
480
497
  "market-post",
481
- svg24('<path d="M12 10V2"/><path d="m4.93 10.93 1.41 1.41"/><path d="M2 18h2"/><path d="M20 18h2"/><path d="m19.07 10.93-1.41 1.41"/><path d="M22 22H2"/><path d="m16 18-4-4-4 4"/><path d="M16 6a4 4 0 0 0-8 0"/>')
498
+ svg24('<path d="M12 10V2"/><path d="m4.93 10.93 1.41 1.41"/><path d="M2 18h2"/><path d="M20 18h2"/><path d="m19.07 10.93-1.41 1.41"/><path d="M22 22H2"/><path d="m16 14-4 4-4-4"/><path d="M16 6a4 4 0 0 0-8 0"/>')
482
499
  );
483
500
  registerIcon("market-closed", svg24('<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>'));
484
501
  registerIcon(
@@ -511,6 +528,8 @@ var Vela = (function (exports) {
511
528
  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"/>'));
512
529
  registerIcon("collapse", S('<path d="M3 8h10"/>'));
513
530
  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"/>'));
531
+ registerIcon("plus", S('<path d="M8 2.8v10.4M2.8 8h10.4"/>'));
532
+ registerIcon("minus", S('<path d="M2.8 8h10.4"/>'));
514
533
  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"/>'));
515
534
  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"/>'));
516
535
  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"/>'));
@@ -518,7 +537,7 @@ var Vela = (function (exports) {
518
537
  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"'));
519
538
  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"'));
520
539
  registerIcon("burger", S('<path d="M2.5 4.5h11M2.5 8h11M2.5 11.5h11"/>'));
521
- 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"/>'));
540
+ 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"/>'));
522
541
  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"/>'));
523
542
  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"/>'));
524
543
  registerIcon("cursor", svg24('<path d="M5 3l6 16 2-6 6-2z"/>', 'fill="currentColor" stroke="none"'));
@@ -6889,6 +6908,23 @@ var Vela = (function (exports) {
6889
6908
  return 36e5;
6890
6909
  }
6891
6910
 
6911
+ // src/data/symbol-groups.ts
6912
+ function isGroupRow(d) {
6913
+ return d.group != null && d.ticker === d.group;
6914
+ }
6915
+ function groupKeyOf(d) {
6916
+ return `${(d.prefix ?? d.provider ?? "").toLowerCase()}:${(d.group ?? "").toUpperCase()}`;
6917
+ }
6918
+ function groupMembers(pool, groupRow) {
6919
+ const key = groupKeyOf(groupRow);
6920
+ return pool.filter((s) => s.group != null && !isGroupRow(s) && groupKeyOf(s) === key);
6921
+ }
6922
+ function defaultMemberOf(pool, groupRow) {
6923
+ const members = groupMembers(pool, groupRow);
6924
+ const defaults2 = members.filter((m) => m.default);
6925
+ return (defaults2.length === 1 ? defaults2[0] : members[0]) ?? null;
6926
+ }
6927
+
6892
6928
  // src/data/ProviderRegistry.ts
6893
6929
  var normName = (n) => n.trim().toLowerCase();
6894
6930
  var normTicker = (t) => t.trim().toUpperCase();
@@ -6978,17 +7014,24 @@ var Vela = (function (exports) {
6978
7014
  resolve(raw, opts = {}) {
6979
7015
  const { provider, ticker } = this.parse(raw);
6980
7016
  if (provider) {
6981
- if (this.entries.has(provider)) return { provider, ticker };
7017
+ if (this.entries.has(provider)) {
7018
+ const d = this.entries.get(provider).byTicker?.get(normTicker(ticker));
7019
+ return { provider, ticker: d != null && isGroupRow(d) ? this.loadableTicker(provider, d) : ticker };
7020
+ }
6982
7021
  const key = `${provider}:${normTicker(ticker)}`;
6983
7022
  for (const name of this.candidateOrder(opts.default)) {
6984
7023
  const d = this.entries.get(name).prefixIndex?.get(key);
6985
- if (d) return { provider: name, ticker: d.ticker };
7024
+ if (d) return { provider: name, ticker: this.loadableTicker(name, d) };
6986
7025
  }
6987
7026
  return null;
6988
7027
  }
6989
7028
  const norm = normTicker(ticker);
6990
7029
  for (const name of this.candidateOrder(opts.default)) {
6991
- if (this.entries.get(name).index?.has(norm)) return { provider: name, ticker };
7030
+ const e = this.entries.get(name);
7031
+ if (e.index?.has(norm)) {
7032
+ const d = e.byTicker?.get(norm);
7033
+ return { provider: name, ticker: d != null && isGroupRow(d) ? this.loadableTicker(name, d) : ticker };
7034
+ }
6992
7035
  }
6993
7036
  if (opts.lenient && this.entries.size === 1) {
6994
7037
  const only = this.names()[0];
@@ -6997,6 +7040,13 @@ var Vela = (function (exports) {
6997
7040
  }
6998
7041
  return null;
6999
7042
  }
7043
+ /** What a resolved descriptor LOADS: itself — unless it is a GROUP row (listed, never
7044
+ * served), which loads its default member (single `default`, else first listed). A
7045
+ * memberless group keeps its own ticker and fails downstream like any unknown symbol. */
7046
+ loadableTicker(name, d) {
7047
+ if (!isGroupRow(d)) return d.ticker;
7048
+ return defaultMemberOf(this.entries.get(name)?.descriptors ?? [], d)?.ticker ?? d.ticker;
7049
+ }
7000
7050
  /**
7001
7051
  * Resolve now if possible, else resolve later — re-attempting after each provider
7002
7052
  * settles. The promise stays pending until some registered provider can serve the
@@ -7285,6 +7335,10 @@ var Vela = (function (exports) {
7285
7335
  /** Invalidates detached async work (backfill loops, in-flight loads, gap heals):
7286
7336
  * bumped by init(), setMarket() and destroy(). */
7287
7337
  this.generation = 0;
7338
+ /** Aborts the in-flight PROGRESSIVE load's source polling on supersession — an
7339
+ * abandoned stream left polling to its own budget starves the browser's per-host
7340
+ * connection pool, and the NEXT symbol's very first fetch with it (measured). */
7341
+ this.progressiveAbort = null;
7288
7342
  /** Awaiters racing a superseded load (setMarket callers) — released on every bump so they never hang. */
7289
7343
  this.supersedeWaiters = [];
7290
7344
  /** `history:complete` fired for the CURRENT load. Each market load re-arms the cycle
@@ -7318,7 +7372,9 @@ var Vela = (function (exports) {
7318
7372
  for (const engine of engines) this.registerEngine(engine.language, engine);
7319
7373
  this.defaultLanguage = config.defaultLanguage;
7320
7374
  this.renderer.mount(container, config.theme);
7321
- this.renderer.onInputChange((e) => this.applyInputs(e.indicatorId, { [e.key]: e.value }));
7375
+ this.renderer.onInputChange(
7376
+ (e) => e.kind === "prop" ? this.applyProps(e.indicatorId, { [e.key]: e.value }) : this.applyInputs(e.indicatorId, { [e.key]: e.value })
7377
+ );
7322
7378
  this.renderer.onRemoveIndicator((id) => this.removeIndicator(id));
7323
7379
  this.renderer.onToggleIndicatorVisible?.((id, visible) => this.setVisible(id, visible));
7324
7380
  this.paneActionUnsub = this.renderer.onPaneAction?.((a) => this.handlePaneAction(a)) ?? null;
@@ -7425,6 +7481,8 @@ var Vela = (function (exports) {
7425
7481
  * superseded setMarket awaiters so their promises resolve instead of hanging. */
7426
7482
  bumpGeneration() {
7427
7483
  const gen = ++this.generation;
7484
+ this.progressiveAbort?.abort();
7485
+ this.progressiveAbort = null;
7428
7486
  for (const w of this.supersedeWaiters.splice(0)) w();
7429
7487
  return gen;
7430
7488
  }
@@ -7478,7 +7536,47 @@ var Vela = (function (exports) {
7478
7536
  const requested = market.bars ?? 500;
7479
7537
  const initialRange = market.visibleRange;
7480
7538
  const deep = !market.data?.length && initialRange == null && requested > SINGLE_LOAD_BARS;
7481
- if (deep && this.feed.loadRange) {
7539
+ let progressiveServed = false;
7540
+ if (!market.data?.length && initialRange == null && this.feed.loadProgressive) {
7541
+ let painted = false;
7542
+ const paint = (bars, final) => {
7543
+ if (this.generation !== gen || !final && bars.length === 0) return;
7544
+ this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
7545
+ if (!painted && bars.length > 0) {
7546
+ painted = true;
7547
+ if (opts.firstLoad) this.activateBarLayers();
7548
+ if (!final) this.historyState = "backfill";
7549
+ }
7550
+ };
7551
+ const abort = new AbortController();
7552
+ this.progressiveAbort = abort;
7553
+ progressiveServed = await new Promise((firstPaint) => {
7554
+ let signaled = false;
7555
+ const signal = (served) => {
7556
+ if (!signaled) {
7557
+ signaled = true;
7558
+ firstPaint(served);
7559
+ }
7560
+ };
7561
+ abort.signal.addEventListener("abort", () => signal(true), { once: true });
7562
+ this.feed.loadProgressive(market, (bars) => {
7563
+ paint(bars, false);
7564
+ if (painted) signal(true);
7565
+ }, { signal: abort.signal }).then((full) => {
7566
+ if (this.progressiveAbort === abort) this.progressiveAbort = null;
7567
+ if (full == null) return signal(false);
7568
+ if (this.generation !== gen) return signal(true);
7569
+ paint(full, true);
7570
+ this.completeHistory(full.length >= requested ? "depth" : "genesis");
7571
+ signal(true);
7572
+ }).catch(() => {
7573
+ if (this.progressiveAbort === abort) this.progressiveAbort = null;
7574
+ if (this.generation === gen) this.completeHistory("aborted");
7575
+ signal(true);
7576
+ });
7577
+ });
7578
+ }
7579
+ if (progressiveServed) ; else if (deep && this.feed.loadRange) {
7482
7580
  const head = await this.feed.load({ ...market, bars: Math.min(requested, CHUNK_BARS) });
7483
7581
  if (this.generation !== gen) return;
7484
7582
  this.setBarSeries(head);
@@ -7998,7 +8096,7 @@ var Vela = (function (exports) {
7998
8096
  const title = options.title ?? "Indicator";
7999
8097
  const handle = new IndicatorHandleImpl(id, title, this, source);
8000
8098
  this.handles.set(id, handle);
8001
- this.registry.add({ id, title, source, options, inputValues: { ...options.inputs ?? {} } });
8099
+ this.registry.add({ id, title, source, options, inputValues: { ...options.inputs ?? {} }, propValues: { ...options.props ?? {} } });
8002
8100
  void this.startIndicator(id, source, options, handle);
8003
8101
  return handle;
8004
8102
  }
@@ -8021,7 +8119,7 @@ var Vela = (function (exports) {
8021
8119
  return handle;
8022
8120
  }
8023
8121
  const inputValues = { ...descriptor.defaultInputs(), ...options.inputs ?? {} };
8024
- this.registry.add({ id, title, source: type, inputValues, native: { type, instance: descriptor.create(), descriptor } });
8122
+ this.registry.add({ id, title, source: type, inputValues, propValues: {}, native: { type, instance: descriptor.create(), descriptor } });
8025
8123
  handle.setSchema(descriptor.inputsSchema());
8026
8124
  void this.startNativeIndicator(id, handle);
8027
8125
  return handle;
@@ -8074,6 +8172,19 @@ var Vela = (function (exports) {
8074
8172
  if (record.session) record.session.update(record.inputValues);
8075
8173
  else if (record.native && !record.hidden) record.native.instance.setInputs(record.inputValues);
8076
8174
  }
8175
+ /** IndicatorController: re-run an indicator with merged declaration-prop overrides.
8176
+ * Same lifecycle as {@link applyInputs} — a prop change replays the whole script.
8177
+ * Script indicators only: natives have no declaration props. */
8178
+ applyProps(id, values) {
8179
+ const record = this.registry.get(id);
8180
+ if (!record?.session) return;
8181
+ record.propValues = { ...record.propValues, ...values };
8182
+ if (record.renderHandle) this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
8183
+ record.pendingStructural = true;
8184
+ if (!record.hidden) this.setLoading(record, true);
8185
+ record.pendingCause = "inputs";
8186
+ record.session.update(record.inputValues, record.propValues);
8187
+ }
8077
8188
  /** IndicatorController: tear down an indicator and (if now empty) its pane. */
8078
8189
  /** Live handles of every indicator on the chart (script + native), insertion order. */
8079
8190
  listIndicators() {
@@ -8222,6 +8333,10 @@ var Vela = (function (exports) {
8222
8333
  for (const input of prepared.inputs) defaults2[input.key] = input.defval;
8223
8334
  record.inputValues = { ...defaults2, ...record.inputValues };
8224
8335
  handle.setSchema(prepared.inputs);
8336
+ const propDefaults = {};
8337
+ for (const prop of prepared.props ?? []) propDefaults[prop.key] = prop.defval;
8338
+ record.propValues = { ...propDefaults, ...record.propValues };
8339
+ handle.setPropsSchema(prepared.props ?? []);
8225
8340
  this.mountLoadingPlaceholder(id, record);
8226
8341
  this.executeIndicator(id, handle);
8227
8342
  } catch (err) {
@@ -8245,6 +8360,7 @@ var Vela = (function (exports) {
8245
8360
  getBars: () => this.bars,
8246
8361
  fetchSeries: (sym, tf, range) => this.fetchSeries(sym, tf, range),
8247
8362
  inputs: record.inputValues,
8363
+ props: record.propValues,
8248
8364
  visibleRange: this.currentVisibleRange(),
8249
8365
  mode,
8250
8366
  // Mid-backfill session starts (add / re-show / price-style re-execute) let the
@@ -8338,6 +8454,9 @@ var Vela = (function (exports) {
8338
8454
  const meta = record.prepared.meta;
8339
8455
  const model = {
8340
8456
  id,
8457
+ // Deliberately the FULL title, never a shorttitle: while the script loads the
8458
+ // legend identifies it by its full name; the compact shorttitle arrives with
8459
+ // the first computed model and takes over from there.
8341
8460
  title: record.options?.title ?? meta.title,
8342
8461
  overlay: meta.overlay,
8343
8462
  paneHint: meta.overlay ? "price" : "new",
@@ -8346,7 +8465,8 @@ var Vela = (function (exports) {
8346
8465
  backgrounds: [],
8347
8466
  priceLines: [],
8348
8467
  inputs: record.prepared.inputs,
8349
- inputValues: record.inputValues
8468
+ inputValues: record.inputValues,
8469
+ ...record.prepared.props ? { props: record.prepared.props, propValues: record.propValues } : {}
8350
8470
  };
8351
8471
  const paneId = this.routePane(id, model, record.options ?? {});
8352
8472
  this.placeModel(model, id, paneId);
@@ -8579,7 +8699,7 @@ var Vela = (function (exports) {
8579
8699
  record.model = model;
8580
8700
  if (record.pendingStructural) {
8581
8701
  record.renderHandle = this.renderer.mountIndicator(model);
8582
- this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues);
8702
+ this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
8583
8703
  record.pendingStructural = false;
8584
8704
  } else {
8585
8705
  this.renderer.updateIndicator(record.renderHandle, modelToValuePatch(model));
@@ -8606,16 +8726,17 @@ var Vela = (function (exports) {
8606
8726
  placeModel(model, id, paneId) {
8607
8727
  model.id = id;
8608
8728
  model.paneId = paneId;
8609
- for (const series of model.series) series.paneId = paneId;
8610
- for (const fill of model.fills) fill.paneId = paneId;
8611
- for (const bg of model.backgrounds) bg.paneId = paneId;
8729
+ const paneOf = (item) => item.overlay === true ? "price" : paneId;
8730
+ for (const series of model.series) series.paneId = paneOf(series);
8731
+ for (const fill of model.fills) fill.paneId = paneOf(fill);
8732
+ for (const bg of model.backgrounds) bg.paneId = paneOf(bg);
8612
8733
  for (const line of model.priceLines) line.paneId = paneId;
8613
- if (model.lines) for (const ln of model.lines) ln.paneId = paneId;
8614
- if (model.boxes) for (const bx of model.boxes) bx.paneId = paneId;
8615
- if (model.labels) for (const lb of model.labels) lb.paneId = paneId;
8616
- if (model.polylines) for (const pl of model.polylines) pl.paneId = paneId;
8617
- if (model.linefills) for (const lf of model.linefills) lf.paneId = paneId;
8618
- if (model.tables) for (const tb of model.tables) tb.paneId = paneId;
8734
+ if (model.lines) for (const ln of model.lines) ln.paneId = paneOf(ln);
8735
+ if (model.boxes) for (const bx of model.boxes) bx.paneId = paneOf(bx);
8736
+ if (model.labels) for (const lb of model.labels) lb.paneId = paneOf(lb);
8737
+ if (model.polylines) for (const pl of model.polylines) pl.paneId = paneOf(pl);
8738
+ if (model.linefills) for (const lf of model.linefills) lf.paneId = paneOf(lf);
8739
+ if (model.tables) for (const tb of model.tables) tb.paneId = paneOf(tb);
8619
8740
  }
8620
8741
  emitContextChanged(id) {
8621
8742
  if (!this.registry.get(id)?.session?.getContext) return;
@@ -8768,6 +8889,15 @@ var Vela = (function (exports) {
8768
8889
  setLegendActions(provider) {
8769
8890
  this.renderer.setLegendActions?.(provider);
8770
8891
  }
8892
+ /**
8893
+ * Wire the legend rows' HOST-CONTRIBUTED callout bubbles (the shells route the
8894
+ * plugin registry through this; see `registerLegendCallout`). Silent on a renderer
8895
+ * without the seam — contributed callouts simply never show there, same graceful
8896
+ * degradation as {@link setLegendActions}.
8897
+ */
8898
+ setLegendCallouts(provider) {
8899
+ this.renderer.setLegendCallouts?.(provider);
8900
+ }
8771
8901
  /**
8772
8902
  * Replace the indicator legend's fold toggle with a host action (or restore it with
8773
8903
  * `null`) — multi-chart shells point the chip at their indicator overview instead of
@@ -9118,6 +9248,28 @@ var Vela = (function (exports) {
9118
9248
  this.store.merge(key, dropForming(bars));
9119
9249
  return bars;
9120
9250
  }
9251
+ /**
9252
+ * Progressive twin of {@link load}: a COLD load streams through the inner feed's
9253
+ * progressive path — batches forwarded verbatim, the FINAL answer cached exactly as
9254
+ * `load` caches — while a cache-covered load answers once through `load` itself (a
9255
+ * warm chart has nothing to stream). Falls back to `load` wholesale when the inner
9256
+ * feed lacks the capability, so callers may prefer this method unconditionally.
9257
+ */
9258
+ async loadProgressive(cfg, onBatch, opts) {
9259
+ if (cfg.data && cfg.data.length > 0) return this.inner.load(cfg);
9260
+ if (!this.inner.loadProgressive) return null;
9261
+ const symbol = cfg.symbol ?? "TEST";
9262
+ const key = cacheKey(symbol, cfg.timeframe ?? "60", cfg.session);
9263
+ const cached = this.store.get(key);
9264
+ const n = cfg.bars ?? 500;
9265
+ const lastCached = cached?.[cached.length - 1];
9266
+ if (cached && lastCached && cached.length >= n - 1 && this.inner.loadRange) return this.load(cfg);
9267
+ this.store.retainSymbol(symbol);
9268
+ const bars = await this.inner.loadProgressive(cfg, onBatch, opts);
9269
+ if (bars == null) return null;
9270
+ this.store.merge(key, dropForming(bars));
9271
+ return bars;
9272
+ }
9121
9273
  /**
9122
9274
  * Cache-backed ranged fetch — the gateway engines use for secondary series
9123
9275
  * (`request.security` HTF/LTF/cross-symbol) and the orchestrator's backward
@@ -9363,6 +9515,17 @@ var Vela = (function (exports) {
9363
9515
  this.prefetchSymbolInfo(resolved);
9364
9516
  return this.cache.load(canonical(cfg, resolved));
9365
9517
  }
9518
+ /** Progressive twin of {@link load} — same resolution, the cache streams the batches. */
9519
+ async loadProgressive(cfg, onBatch, opts) {
9520
+ if (cfg.data && cfg.data.length > 0) {
9521
+ this.liveBars = cfg.data.map((b) => ({ ...b }));
9522
+ return cfg.data;
9523
+ }
9524
+ const resolved = await this.registry.whenResolvable(rawSymbol(cfg), { default: this.primaryProvider });
9525
+ this.primaryProvider = resolved.provider;
9526
+ this.prefetchSymbolInfo(resolved);
9527
+ return this.cache.loadProgressive(canonical(cfg, resolved), onBatch, opts);
9528
+ }
9366
9529
  /**
9367
9530
  * Synchronous per-symbol metadata for engines (Pine `syminfo.*`), served from the cache
9368
9531
  * warmed by load(). Undefined until the prefetch lands (the engine then synthesizes a
@@ -9450,6 +9613,18 @@ var Vela = (function (exports) {
9450
9613
  if (!provider) return Promise.resolve([]);
9451
9614
  return safeBars(provider, ticker, cfg.timeframe ?? "60", { limit: cfg.bars ?? 500, session: cfg.session });
9452
9615
  }
9616
+ async loadProgressive(cfg, onBatch, opts) {
9617
+ const { provider: name, ticker } = parseSymbol(cfg.symbol ?? "");
9618
+ const provider = this.registry.get(name ?? "");
9619
+ if (!provider) return [];
9620
+ if (!provider.getBarsProgressive) return null;
9621
+ try {
9622
+ return await provider.getBarsProgressive(ticker, cfg.timeframe ?? "60", { limit: cfg.bars ?? 500, session: cfg.session }, onBatch, opts);
9623
+ } catch (e) {
9624
+ console.warn(`[vela] progressive fetch failed for ${ticker} ${cfg.timeframe ?? "60"} \u2014 ${e instanceof Error ? e.message : String(e)}`);
9625
+ return [];
9626
+ }
9627
+ }
9453
9628
  loadRange(cfg, range) {
9454
9629
  const { provider: name, ticker } = parseSymbol(cfg.symbol ?? "");
9455
9630
  const provider = this.registry.get(name ?? "");
@@ -16606,13 +16781,6 @@ var Vela = (function (exports) {
16606
16781
  }
16607
16782
  };
16608
16783
 
16609
- // src/core/model/inputs.ts
16610
- function inputVisible(when, values) {
16611
- if (!when) return true;
16612
- const conds = Array.isArray(when) ? when : [when];
16613
- return conds.every((c) => c.anyOf ? c.anyOf.some((x) => x === values[c.key]) : values[c.key] === c.equals);
16614
- }
16615
-
16616
16784
  // src/ui/tokens.ts
16617
16785
  var STATIC_ID = "vela-ui-tokens";
16618
16786
  var STATIC_DECLS = Object.entries(STATIC_TOKENS).map(([k, v]) => ` ${k}: ${v};`).join("\n");
@@ -16858,6 +17026,203 @@ ${STATIC_DECLS}
16858
17026
  }
16859
17027
  };
16860
17028
 
17029
+ // src/ui/components/callout-bubble/controller.ts
17030
+ function closesPanel(item) {
17031
+ return item.close !== false;
17032
+ }
17033
+ function calloutPanelRows(items) {
17034
+ const rows = [];
17035
+ for (const item of items) {
17036
+ if (item.type === "text") {
17037
+ rows.push({ type: "text", text: item.text });
17038
+ continue;
17039
+ }
17040
+ const last2 = rows[rows.length - 1];
17041
+ if (last2 && last2.type === "buttons") last2.buttons.push(item);
17042
+ else rows.push({ type: "buttons", buttons: [item] });
17043
+ }
17044
+ return rows;
17045
+ }
17046
+
17047
+ // src/ui/components/callout-bubble/styles.ts
17048
+ var CALLOUT_STYLE_ID = "vela-ui-callout-bubble";
17049
+ var CALLOUT_CSS = `
17050
+ .vela-callout {
17051
+ display: inline-grid;
17052
+ place-items: center;
17053
+ border-radius: 50%;
17054
+ flex: none;
17055
+ line-height: 0;
17056
+ box-sizing: border-box;
17057
+ cursor: default;
17058
+ user-select: none;
17059
+ -webkit-user-select: none;
17060
+ }
17061
+ .vela-callout[role='button'] { cursor: pointer; }
17062
+ .vela-callout svg { display: block; }
17063
+ /* The deployed panel \u2014 carries the kit's elevated-card look itself (the popover
17064
+ shell is bare positioning chrome). */
17065
+ .vela-callout-panel {
17066
+ display: flex;
17067
+ flex-direction: column;
17068
+ gap: 8px;
17069
+ padding: 10px 12px;
17070
+ max-width: 280px;
17071
+ box-sizing: border-box;
17072
+ background: var(--vela-surface-elev);
17073
+ border: 1px solid var(--vela-border-strong);
17074
+ border-radius: 6px;
17075
+ box-shadow: var(--vela-shadow);
17076
+ color: var(--vela-fg);
17077
+ font: var(--vela-font-size-md) var(--vela-font);
17078
+ }
17079
+ .vela-callout-title { font-weight: 600; color: var(--vela-fg-bright); }
17080
+ .vela-callout-text { color: var(--vela-fg-muted); line-height: 1.45; white-space: pre-line; }
17081
+ .vela-callout-actions { display: flex; flex-wrap: wrap; gap: 8px; }
17082
+ .vela-callout-btn {
17083
+ cursor: pointer;
17084
+ height: 26px;
17085
+ padding: 0 10px;
17086
+ border-radius: 5px;
17087
+ border: 1px solid var(--vela-border);
17088
+ background: transparent;
17089
+ color: var(--vela-fg);
17090
+ font-size: var(--vela-font-size-md);
17091
+ font-family: inherit;
17092
+ transition: background var(--vela-dur-fast) ease, color var(--vela-dur-fast) ease, opacity var(--vela-dur-fast) ease, border-color var(--vela-dur-fast) ease;
17093
+ }
17094
+ .vela-callout-btn:hover { background: var(--vela-hover); color: var(--vela-fg-bright); border-color: var(--vela-fg-muted); }
17095
+ .vela-callout-btn-primary { border-color: var(--vela-selected-bg); background: var(--vela-selected-bg); color: var(--vela-selected-fg); }
17096
+ .vela-callout-btn-primary:hover { background: var(--vela-selected-bg); color: var(--vela-selected-fg); opacity: 0.85; border-color: var(--vela-selected-bg); }
17097
+ `;
17098
+
17099
+ // src/ui/components/callout-bubble/view.ts
17100
+ var CalloutBubble = class {
17101
+ constructor(opts) {
17102
+ this.pop = null;
17103
+ this.spec = { icon: opts.icon, background: opts.background, label: opts.label };
17104
+ if (opts.color !== void 0) this.spec.color = opts.color;
17105
+ if (opts.panel !== void 0) this.spec.panel = opts.panel;
17106
+ this.size = opts.size ?? 16;
17107
+ this.host = opts.host;
17108
+ this.theme = opts.theme;
17109
+ this.boundary = opts.boundary;
17110
+ const doc = (opts.host ?? document.body).ownerDocument;
17111
+ injectStyles(CALLOUT_STYLE_ID, CALLOUT_CSS, doc);
17112
+ this.el = doc.createElement("span");
17113
+ this.el.className = "vela-callout";
17114
+ this.el.style.width = `${this.size}px`;
17115
+ this.el.style.height = `${this.size}px`;
17116
+ this.el.addEventListener("click", (e) => {
17117
+ if (!this.spec.panel) return;
17118
+ e.stopPropagation();
17119
+ this.toggle();
17120
+ });
17121
+ this.el.addEventListener("dblclick", (e) => {
17122
+ if (this.spec.panel) e.stopPropagation();
17123
+ });
17124
+ this.el.addEventListener("keydown", (e) => {
17125
+ if (!this.spec.panel || e.key !== "Enter" && e.key !== " ") return;
17126
+ e.preventDefault();
17127
+ this.toggle();
17128
+ });
17129
+ this.dress();
17130
+ }
17131
+ /** Re-dress the bubble (a status change: new icon, tint, label, panel). */
17132
+ set(spec) {
17133
+ this.spec = { ...this.spec, ...spec };
17134
+ if ("panel" in spec) this.pop?.hide();
17135
+ this.dress();
17136
+ }
17137
+ /** Whether the deployed panel is currently open. */
17138
+ get open() {
17139
+ return this.pop?.open ?? false;
17140
+ }
17141
+ /** Close the deployed panel, if any. The bubble itself stays. */
17142
+ hidePanel() {
17143
+ this.pop?.hide();
17144
+ }
17145
+ destroy() {
17146
+ this.pop?.destroy();
17147
+ this.pop = null;
17148
+ this.el.remove();
17149
+ }
17150
+ dress() {
17151
+ const clickable = this.spec.panel !== void 0;
17152
+ this.el.style.background = this.spec.background;
17153
+ this.el.style.color = this.spec.color ?? "";
17154
+ this.el.innerHTML = iconAt(this.spec.icon, this.size - 4);
17155
+ this.el.setAttribute("aria-label", this.spec.label);
17156
+ if (clickable) {
17157
+ this.el.setAttribute("role", "button");
17158
+ this.el.tabIndex = 0;
17159
+ } else {
17160
+ this.el.removeAttribute("role");
17161
+ this.el.removeAttribute("tabindex");
17162
+ }
17163
+ }
17164
+ toggle() {
17165
+ if (this.pop?.open) {
17166
+ this.pop.hide();
17167
+ return;
17168
+ }
17169
+ const panel = this.spec.panel;
17170
+ if (!panel) return;
17171
+ this.pop?.destroy();
17172
+ this.pop = new Popover({
17173
+ trigger: this.el,
17174
+ gap: 6,
17175
+ content: (body) => this.buildPanel(body, panel),
17176
+ ...this.host ? { host: this.host } : {},
17177
+ ...this.theme ? { theme: this.theme() } : {},
17178
+ ...this.boundary ? { boundary: this.boundary } : {}
17179
+ });
17180
+ this.pop.show();
17181
+ }
17182
+ buildPanel(body, panel) {
17183
+ const doc = body.ownerDocument;
17184
+ const root = doc.createElement("div");
17185
+ root.className = "vela-callout-panel";
17186
+ if (panel.title) {
17187
+ const title = doc.createElement("div");
17188
+ title.className = "vela-callout-title";
17189
+ title.textContent = panel.title;
17190
+ root.appendChild(title);
17191
+ }
17192
+ for (const row of calloutPanelRows(panel.items)) {
17193
+ if (row.type === "text") {
17194
+ const text = doc.createElement("div");
17195
+ text.className = "vela-callout-text";
17196
+ text.textContent = row.text;
17197
+ root.appendChild(text);
17198
+ continue;
17199
+ }
17200
+ const actions = doc.createElement("div");
17201
+ actions.className = "vela-callout-actions";
17202
+ for (const item of row.buttons) {
17203
+ const btn2 = doc.createElement("button");
17204
+ btn2.type = "button";
17205
+ btn2.className = "vela-callout-btn" + (item.primary ? " vela-callout-btn-primary" : "");
17206
+ btn2.textContent = item.label;
17207
+ btn2.addEventListener("click", () => {
17208
+ item.run();
17209
+ if (closesPanel(item)) this.pop?.hide();
17210
+ });
17211
+ actions.appendChild(btn2);
17212
+ }
17213
+ root.appendChild(actions);
17214
+ }
17215
+ body.appendChild(root);
17216
+ }
17217
+ };
17218
+
17219
+ // src/core/model/inputs.ts
17220
+ function inputVisible(when, values) {
17221
+ if (!when) return true;
17222
+ const conds = Array.isArray(when) ? when : [when];
17223
+ return conds.every((c) => c.anyOf ? c.anyOf.some((x) => x === values[c.key]) : values[c.key] === c.equals);
17224
+ }
17225
+
16861
17226
  // src/ui/components/select/controller.ts
16862
17227
  function selectController(opts) {
16863
17228
  const options = opts.options;
@@ -19961,6 +20326,10 @@ ${overlayScrollbarCss(".vela-dialog *")}
19961
20326
  }
19962
20327
 
19963
20328
  // src/renderers/shared/IndicatorInputsDialog.ts
20329
+ var PROPS_TAB = "Properties";
20330
+ function bagOf(row, decl) {
20331
+ return decl.prop ? row.propValues : row.values;
20332
+ }
19964
20333
  var SOURCES = ["close", "open", "high", "low", "hl2", "hlc3", "ohlc4", "volume"];
19965
20334
  var IndicatorInputsDialog = class {
19966
20335
  constructor(host) {
@@ -19971,6 +20340,7 @@ ${overlayScrollbarCss(".vela-dialog *")}
19971
20340
  this.openId = null;
19972
20341
  this.row = null;
19973
20342
  this.snapshot = null;
20343
+ this.propSnapshot = null;
19974
20344
  this.dialogTips = [];
19975
20345
  /** Re-applies every `when` gate against current values; set while the dialog is open. */
19976
20346
  this.refreshVisibility = null;
@@ -19993,10 +20363,11 @@ ${overlayScrollbarCss(".vela-dialog *")}
19993
20363
  }
19994
20364
  open(row) {
19995
20365
  this.close();
19996
- if (row.inputs.length === 0) return;
20366
+ if (row.inputs.length === 0 && row.props.length === 0) return;
19997
20367
  this.row = row;
19998
20368
  this.openId = row.id;
19999
20369
  this.snapshot = { ...row.values };
20370
+ this.propSnapshot = { ...row.propValues };
20000
20371
  ensureDialogStyles();
20001
20372
  const t = this.host.theme();
20002
20373
  const border = "var(--vela-border)";
@@ -20004,7 +20375,7 @@ ${overlayScrollbarCss(".vela-dialog *")}
20004
20375
  const host = this.host.dialogHost() ?? this.host.container;
20005
20376
  const ui = new Dialog({
20006
20377
  host,
20007
- title: row.settingsTitle,
20378
+ title: row.title,
20008
20379
  // Non-modal: live-edit dialog — a modal machine locks pointer events on the
20009
20380
  // whole body, killing the chart and the body-portaled popovers.
20010
20381
  modal: false,
@@ -20036,6 +20407,16 @@ ${overlayScrollbarCss(".vela-dialog *")}
20036
20407
  text: () => "Close"
20037
20408
  }));
20038
20409
  const tabDefs = tabInputs(row.inputs);
20410
+ if (row.props.length > 0) {
20411
+ const propDecls = row.props.map((p) => {
20412
+ const d = { ...p, prop: true };
20413
+ delete d.tab;
20414
+ return d;
20415
+ });
20416
+ const existing = tabDefs.find((t2) => t2.name === PROPS_TAB);
20417
+ if (existing) existing.inputs.push(...propDecls);
20418
+ else tabDefs.push({ name: PROPS_TAB, inputs: propDecls });
20419
+ }
20039
20420
  const tabs = document.createElement("div");
20040
20421
  tabs.style.cssText = `display:flex;gap:12px;padding:0 20px;border-bottom:1px solid ${border};flex:0 0 auto;`;
20041
20422
  const tabEls = [];
@@ -20138,9 +20519,10 @@ ${overlayScrollbarCss(".vela-dialog *")}
20138
20519
  this.openId = null;
20139
20520
  this.row = null;
20140
20521
  this.snapshot = null;
20522
+ this.propSnapshot = null;
20141
20523
  ui?.destroy();
20142
20524
  }
20143
- /** Restore every input to its open-time value (re-running the indicator), then close. */
20525
+ /** Restore every input and prop to its open-time value (re-running the indicator), then close. */
20144
20526
  revertAndClose() {
20145
20527
  const row = this.row;
20146
20528
  const snap = this.snapshot;
@@ -20154,6 +20536,17 @@ ${overlayScrollbarCss(".vela-dialog *")}
20154
20536
  }
20155
20537
  }
20156
20538
  }
20539
+ const propSnap = this.propSnapshot;
20540
+ if (row && propSnap) {
20541
+ for (const p of row.props) {
20542
+ const snapped = propSnap[p.key];
20543
+ const before = snapped !== void 0 ? snapped : p.defval;
20544
+ if (row.propValues[p.key] !== before) {
20545
+ row.propValues[p.key] = before;
20546
+ this.host.onChange?.({ indicatorId: row.id, key: p.key, value: before, kind: "prop" });
20547
+ }
20548
+ }
20549
+ }
20157
20550
  this.close();
20158
20551
  }
20159
20552
  /** The footer's reset button — same chip as Cancel, pinned to the LEFT edge
@@ -20171,19 +20564,27 @@ ${overlayScrollbarCss(".vela-dialog *")}
20171
20564
  const row = this.row;
20172
20565
  if (!row) return;
20173
20566
  const snap = this.snapshot;
20567
+ const propSnap = this.propSnapshot;
20174
20568
  for (const inp of row.inputs) {
20175
20569
  if (row.values[inp.key] !== inp.defval) {
20176
20570
  row.values[inp.key] = inp.defval;
20177
20571
  this.host.onChange?.({ indicatorId: row.id, key: inp.key, value: inp.defval });
20178
20572
  }
20179
20573
  }
20574
+ for (const p of row.props) {
20575
+ if (row.propValues[p.key] !== p.defval) {
20576
+ row.propValues[p.key] = p.defval;
20577
+ this.host.onChange?.({ indicatorId: row.id, key: p.key, value: p.defval, kind: "prop" });
20578
+ }
20579
+ }
20180
20580
  this.open(row);
20181
20581
  if (snap) this.snapshot = snap;
20582
+ if (propSnap) this.propSnapshot = propSnap;
20182
20583
  }
20183
20584
  /** Write one edit through: store it, notify the host, and re-apply the `when` gates. */
20184
- commit(row, key, value) {
20185
- row.values[key] = value;
20186
- this.host.onChange?.({ indicatorId: row.id, key, value });
20585
+ commit(row, decl, value) {
20586
+ bagOf(row, decl)[decl.key] = value;
20587
+ this.host.onChange?.({ indicatorId: row.id, key: decl.key, value, ...decl.prop ? { kind: "prop" } : {} });
20187
20588
  this.refreshVisibility?.();
20188
20589
  }
20189
20590
  /** One settings row (or several `inline=` inputs) placed into a section's grid. */
@@ -20210,7 +20611,7 @@ ${overlayScrollbarCss(".vela-dialog *")}
20210
20611
  const id = idOf(lead);
20211
20612
  const info = lead.tooltip ? this.infoButton(lead.tooltip) : void 0;
20212
20613
  if (lead.type === "bool") {
20213
- const current = Boolean(row.values[lead.key] ?? lead.defval);
20614
+ const current = Boolean(bagOf(row, lead)[lead.key] ?? lead.defval);
20214
20615
  return append(fieldRow({
20215
20616
  label: nameOf(lead),
20216
20617
  id,
@@ -20219,7 +20620,7 @@ ${overlayScrollbarCss(".vela-dialog *")}
20219
20620
  toggle: {
20220
20621
  id,
20221
20622
  checked: current,
20222
- onChange: (v) => this.commit(row, lead.key, v)
20623
+ onChange: (v) => this.commit(row, lead, v)
20223
20624
  }
20224
20625
  }));
20225
20626
  }
@@ -20244,8 +20645,8 @@ ${overlayScrollbarCss(".vela-dialog *")}
20244
20645
  }
20245
20646
  /** Build the typed control for one input, committing edits live via `onChange`. */
20246
20647
  buildControl(row, inp, id) {
20247
- const current = row.values[inp.key] ?? inp.defval;
20248
- const emit = (value) => this.commit(row, inp.key, value);
20648
+ const current = bagOf(row, inp)[inp.key] ?? inp.defval;
20649
+ const emit = (value) => this.commit(row, inp, value);
20249
20650
  if (inp.type === "bool") {
20250
20651
  return buildFieldControl({ kind: "switch", id, checked: Boolean(current), onChange: (v) => emit(v) }).el;
20251
20652
  }
@@ -20256,7 +20657,7 @@ ${overlayScrollbarCss(".vela-dialog *")}
20256
20657
  kind: "color",
20257
20658
  id,
20258
20659
  theme: this.host.theme(),
20259
- get: () => String(row.values[inp.key] ?? inp.defval),
20660
+ get: () => String(bagOf(row, inp)[inp.key] ?? inp.defval),
20260
20661
  onChange: (v) => emit(v)
20261
20662
  }).el;
20262
20663
  }
@@ -20819,6 +21220,7 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
20819
21220
  }
20820
21221
 
20821
21222
  // src/renderers/shared/InputsUI.ts
21223
+ var LEGEND_AT_TOP_ATTR = "data-vela-pane-at-top";
20822
21224
  var STATUS_KEYFRAMES = "@keyframes vela-ind-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.3;transform:scale(.6)}}";
20823
21225
  var LEGEND_ICON_PX2 = 16;
20824
21226
  var LEGEND_CTL_PX = 18;
@@ -20836,6 +21238,9 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
20836
21238
  var FOLD_SVG = iconAt("chevron-up", LEGEND_ICON_PX2);
20837
21239
  var UNFOLD_SVG = iconAt("chevron-down", LEGEND_ICON_PX2);
20838
21240
  var OVERVIEW_SVG = iconAt("objects", LEGEND_ICON_PX2);
21241
+ function legendCalloutsDisplay(open2, hasCallouts) {
21242
+ return !open2 && hasCallouts ? "inline-flex" : "none";
21243
+ }
20839
21244
  var InputsUI = class {
20840
21245
  constructor(container, theme, paneBoundsOf) {
20841
21246
  this.container = container;
@@ -20857,10 +21262,14 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
20857
21262
  this.moveApi = null;
20858
21263
  /** Host-contributed legend actions, resolved PER ROW at render time (see setLegendActions). */
20859
21264
  this.legendActions = null;
21265
+ /** Host-contributed callout bubbles, resolved PER ROW at render time (see setLegendCallouts). */
21266
+ this.legendCallouts = null;
20860
21267
  /** Chrome-tooltip disposers, per row id — a tip open at removal must not outlive its row. */
20861
21268
  this.rowTips = /* @__PURE__ */ new Map();
20862
21269
  /** Same, for the contributed extras only (rebuilt independently by setLegendActions). */
20863
21270
  this.extrasTips = /* @__PURE__ */ new Map();
21271
+ /** Same, for the contributed callouts only (rebuilt independently by setLegendCallouts). */
21272
+ this.calloutTips = /* @__PURE__ */ new Map();
20864
21273
  /** Open "Move to" menu (kept so it can be torn down). */
20865
21274
  this.moveMenu = null;
20866
21275
  this.moveTargets = [];
@@ -21018,6 +21427,42 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21018
21427
  extrasEl.appendChild(btn2);
21019
21428
  }
21020
21429
  }
21430
+ /**
21431
+ * Wire the host-contributed callout bubbles. Same contract as
21432
+ * {@link setLegendActions}: re-calling replaces the provider and re-projects the
21433
+ * rows already on screen.
21434
+ */
21435
+ setLegendCallouts(provider) {
21436
+ this.legendCallouts = provider;
21437
+ for (const row of this.rows.values()) this.renderCallouts(row);
21438
+ }
21439
+ /** (Re)build one row's callout bubbles — tinted icon circles beside the title whose
21440
+ * click (when the view carries content) deploys a panel of text and actions. */
21441
+ renderCallouts(row) {
21442
+ this.disposeTips(this.calloutTips, row.id);
21443
+ for (const bubble of row.callouts) bubble.destroy();
21444
+ row.callouts = [];
21445
+ row.calloutsEl.replaceChildren();
21446
+ const views = this.legendCallouts?.(row.id) ?? [];
21447
+ row.calloutsEl.style.display = legendCalloutsDisplay(row.highlighted, views.length > 0);
21448
+ for (const view of views) {
21449
+ const bubble = new CalloutBubble({
21450
+ icon: view.icon,
21451
+ background: view.background,
21452
+ ...view.color !== void 0 ? { color: view.color } : {},
21453
+ label: view.tooltip,
21454
+ ...view.content !== void 0 ? { panel: view.content } : {},
21455
+ // The panel portals into the plot host and self-tokens: it must work on
21456
+ // a bare chart, where no `.vela-ui` token ancestor exists.
21457
+ host: this.container,
21458
+ theme: () => this.theme
21459
+ });
21460
+ bubble.el.dataset.legendCallout = view.id;
21461
+ this.tip(this.calloutTips, row.id, bubble.el, view.tooltip);
21462
+ row.callouts.push(bubble);
21463
+ row.calloutsEl.appendChild(bubble.el);
21464
+ }
21465
+ }
21021
21466
  /** Reposition the per-pane legend containers after a layout change. */
21022
21467
  reposition() {
21023
21468
  for (const [paneId, lg] of this.legends) this.positionLegend(lg, paneId);
@@ -21185,6 +21630,7 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21185
21630
  }
21186
21631
  positionLegend(lg, paneId) {
21187
21632
  const bounds = this.paneBoundsOf ? this.paneBoundsOf(paneId) : { top: 0, height: Infinity };
21633
+ lg.toggleAttribute(LEGEND_AT_TOP_ATTR, bounds.top === 0);
21188
21634
  lg.style.display = bounds.height < 4 || !this.titlesVisible ? "none" : "flex";
21189
21635
  const collapsed = this.paneCollapse.has(paneId);
21190
21636
  const masterId = this.paneCollapse.get(paneId) ?? null;
@@ -21260,13 +21706,13 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21260
21706
  }
21261
21707
  /** Create or update an indicator's legend row (in the legend for its pane). */
21262
21708
  upsert(id, title, inputs, values, paneId = "price", opts = {}) {
21263
- const settingsTitle = opts.settingsTitle ?? title;
21264
21709
  const existing = this.rows.get(id);
21265
21710
  if (existing) {
21266
21711
  existing.title = title;
21267
- existing.settingsTitle = settingsTitle;
21268
21712
  existing.inputs = inputs;
21269
21713
  existing.values = { ...values };
21714
+ existing.props = opts.props ?? [];
21715
+ existing.propValues = { ...opts.propValues ?? {} };
21270
21716
  existing.titleEl.textContent = title;
21271
21717
  if (existing.paneId !== paneId) {
21272
21718
  existing.paneId = paneId;
@@ -21318,6 +21764,9 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21318
21764
  titleWrap.appendChild(beta);
21319
21765
  }
21320
21766
  el.appendChild(titleWrap);
21767
+ const calloutsEl = document.createElement("span");
21768
+ calloutsEl.style.cssText = `display:none;align-items:center;gap:4px;flex:none;margin-left:${LEGEND_TITLE_STATUS_GAP_PX}px;`;
21769
+ el.appendChild(calloutsEl);
21321
21770
  el.appendChild(statusEl);
21322
21771
  const valuesEl = document.createElement("span");
21323
21772
  valuesEl.style.cssText = `display:none;align-items:center;gap:5px;margin-left:${LEGEND_TITLE_VALUES_GAP_PX}px;white-space:nowrap;font-variant-numeric:tabular-nums;`;
@@ -21334,13 +21783,13 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21334
21783
  eye.className = "vela-ind-ctl";
21335
21784
  eye.style.cssText = LEGEND_CTL_CSS;
21336
21785
  eye.addEventListener("click", () => {
21337
- const row = this.rows.get(id);
21338
- this.onToggleVisible?.(id, Boolean(row?.hidden));
21786
+ const row2 = this.rows.get(id);
21787
+ this.onToggleVisible?.(id, Boolean(row2?.hidden));
21339
21788
  });
21340
21789
  controlsEl.appendChild(eye);
21341
21790
  eyeEl = eye;
21342
21791
  }
21343
- if (inputs.length > 0) {
21792
+ if (inputs.length > 0 || (opts.props ?? []).length > 0) {
21344
21793
  const gear = document.createElement("button");
21345
21794
  gear.type = "button";
21346
21795
  gear.setAttribute("aria-label", "Settings");
@@ -21380,7 +21829,9 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21380
21829
  controlsEl.appendChild(close);
21381
21830
  el.appendChild(controlsEl);
21382
21831
  this.attach(this.legendFor(paneId), el, !!opts.native);
21383
- this.rows.set(id, { id, title, settingsTitle, inputs, values: { ...values }, el, titleEl, statusEl, valuesEl, plotValues: [], plotValuesKey: "", showValues: null, highlighted: false, paneId, hidden: false, eyeEl, controlsEl, extrasEl, native: !!opts.native });
21832
+ const row = { id, title, inputs, values: { ...values }, props: opts.props ?? [], propValues: { ...opts.propValues ?? {} }, el, titleEl, statusEl, valuesEl, plotValues: [], plotValuesKey: "", showValues: null, highlighted: false, paneId, hidden: false, eyeEl, controlsEl, extrasEl, calloutsEl, callouts: [], native: !!opts.native };
21833
+ this.rows.set(id, row);
21834
+ this.renderCallouts(row);
21384
21835
  this.syncFoldToggle();
21385
21836
  }
21386
21837
  /** Place a row in its pane's legend — native rows PREPEND (pinned to the top), Pine rows append. */
@@ -21389,9 +21840,11 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21389
21840
  else container.appendChild(el);
21390
21841
  }
21391
21842
  /** Reflect programmatic input changes (so a re-opened dialog shows current values). */
21392
- setValues(id, values) {
21843
+ setValues(id, values, props) {
21393
21844
  const row = this.rows.get(id);
21394
- if (row) row.values = { ...row.values, ...values };
21845
+ if (!row) return;
21846
+ row.values = { ...row.values, ...values };
21847
+ if (props) row.propValues = { ...row.propValues, ...props };
21395
21848
  }
21396
21849
  /**
21397
21850
  * Reflect an indicator's live status in its legend row: `'loading'` shows three pulsing
@@ -21473,8 +21926,13 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21473
21926
  syncRowActions(row) {
21474
21927
  const open2 = row.highlighted;
21475
21928
  row.controlsEl.style.display = open2 || row.hidden ? "inline-flex" : "none";
21476
- if (open2) row.el.appendChild(row.statusEl);
21477
- else row.el.insertBefore(row.statusEl, row.valuesEl);
21929
+ if (open2) {
21930
+ row.el.appendChild(row.statusEl);
21931
+ for (const bubble of row.callouts) bubble.hidePanel();
21932
+ } else {
21933
+ row.el.insertBefore(row.statusEl, row.valuesEl);
21934
+ }
21935
+ row.calloutsEl.style.display = legendCalloutsDisplay(open2, row.callouts.length > 0);
21478
21936
  for (const child of Array.from(row.controlsEl.children)) {
21479
21937
  if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
21480
21938
  if (child === row.extrasEl) {
@@ -21492,10 +21950,12 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21492
21950
  }
21493
21951
  remove(id) {
21494
21952
  const row = this.rows.get(id);
21953
+ for (const bubble of row?.callouts ?? []) bubble.destroy();
21495
21954
  row?.el.remove();
21496
21955
  this.rows.delete(id);
21497
21956
  this.disposeTips(this.rowTips, id);
21498
21957
  this.disposeTips(this.extrasTips, id);
21958
+ this.disposeTips(this.calloutTips, id);
21499
21959
  if (this.selectedId === id) this.selectedId = null;
21500
21960
  if (this.inputsDialog.openId === id) this.inputsDialog.close();
21501
21961
  this.syncFoldToggle();
@@ -21515,6 +21975,8 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21515
21975
  this.rowMenu = null;
21516
21976
  for (const id of [...this.rowTips.keys()]) this.disposeTips(this.rowTips, id);
21517
21977
  for (const id of [...this.extrasTips.keys()]) this.disposeTips(this.extrasTips, id);
21978
+ for (const id of [...this.calloutTips.keys()]) this.disposeTips(this.calloutTips, id);
21979
+ for (const row of this.rows.values()) for (const bubble of row.callouts) bubble.destroy();
21518
21980
  for (const lg of this.legends.values()) lg.remove();
21519
21981
  this.legends.clear();
21520
21982
  this.rows.clear();
@@ -21560,7 +22022,7 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21560
22022
  };
21561
22023
  var STYLE_ID = "vela-pane-controls";
21562
22024
  var ICON_PX = 12;
21563
- var CLUSTER_PILL = "rgba(0,0,0,0.28)";
22025
+ var CLUSTER_PILL = "rgba(0,0,0,0.65)";
21564
22026
  function ensureStyles() {
21565
22027
  if (typeof document === "undefined" || document.getElementById(STYLE_ID)) return;
21566
22028
  const st = document.createElement("style");
@@ -21580,8 +22042,12 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21580
22042
  this.deps = deps;
21581
22043
  this.clusters = /* @__PURE__ */ new Map();
21582
22044
  this.hoverPaneId = null;
22045
+ /** Mobile: hover clusters are meaningless without a cursor — suppressed; a
22046
+ * collapsed pane's standalone expand chip stays (the only way back up). */
22047
+ this.suspended = false;
21583
22048
  /** Reveal the cluster for the pane under the cursor, resolved from the pointer's y in the plot. */
21584
22049
  this.onPlotMove = (e) => {
22050
+ if (this.suspended) return;
21585
22051
  const rect = this.plot.getBoundingClientRect();
21586
22052
  const y = e.clientY - rect.top;
21587
22053
  let hit = null;
@@ -21653,7 +22119,12 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21653
22119
  }
21654
22120
  if (p.count > 1) {
21655
22121
  cluster.appendChild(
21656
- this.button(p.maximized ? ICONS.restore : ICONS.maximize, p.maximized ? "Restore pane" : "Maximize pane", false, () => this.deps.onToggleMaximize(p.id), { role: "maximize" })
22122
+ this.button(p.maximized ? ICONS.restore : ICONS.maximize, p.maximized ? "Restore pane" : "Maximize pane", false, () => this.deps.onToggleMaximize(p.id), {
22123
+ role: "maximize",
22124
+ // Same inverse-chip treatment as the collapsed pane's expand toggle: the
22125
+ // maximized state must read as an active state, not just a swapped glyph.
22126
+ selected: p.maximized
22127
+ })
21657
22128
  );
21658
22129
  }
21659
22130
  }
@@ -21690,17 +22161,18 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21690
22161
  }
21691
22162
  const hovered = id === this.hoverPaneId;
21692
22163
  const hasButtons = cluster.children.length > 0;
21693
- const visible = hasButtons && (hovered || p.collapsed) && p.height > 8;
22164
+ const stateChipRole = p.collapsed ? "collapse" : !this.suspended && p.maximized ? "maximize" : null;
22165
+ const visible = hasButtons && (hovered || stateChipRole != null) && p.height > 8;
21694
22166
  cluster.style.right = `${rightPx}px`;
21695
22167
  cluster.style.top = p.collapsed ? `${p.top + Math.max(1, Math.round((p.height - 24) / 2))}px` : `${p.top + 4}px`;
21696
22168
  cluster.style.display = visible ? "flex" : "none";
21697
22169
  if (!visible) continue;
21698
- const soloExpand = p.collapsed && !hovered;
21699
- cluster.style.background = soloExpand ? "transparent" : CLUSTER_PILL;
22170
+ const soloChip = stateChipRole != null && !hovered;
22171
+ cluster.style.background = soloChip ? "transparent" : CLUSTER_PILL;
21700
22172
  for (const child of cluster.children) {
21701
22173
  const btn2 = child;
21702
22174
  btn2.style.display = "inline-flex";
21703
- btn2.style.visibility = soloExpand && btn2.dataset.role !== "collapse" ? "hidden" : "visible";
22175
+ btn2.style.visibility = soloChip && btn2.dataset.role !== stateChipRole ? "hidden" : "visible";
21704
22176
  }
21705
22177
  }
21706
22178
  }
@@ -21710,6 +22182,14 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21710
22182
  this.hoverPaneId = paneId;
21711
22183
  this.reposition();
21712
22184
  }
22185
+ /** Mobile suppression: no hover clusters (touch has no cursor; the shell's own
22186
+ * chrome covers maximize), while collapsed panes keep their expand chips. */
22187
+ setSuspended(on) {
22188
+ if (on === this.suspended) return;
22189
+ this.suspended = on;
22190
+ if (on) this.hoverPaneId = null;
22191
+ this.reposition();
22192
+ }
21713
22193
  destroy() {
21714
22194
  this.plot.removeEventListener("pointermove", this.onPlotMove);
21715
22195
  this.plot.removeEventListener("pointerleave", this.onPlotLeave);
@@ -21860,131 +22340,6 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21860
22340
  }
21861
22341
  };
21862
22342
 
21863
- // src/renderers/shared/TableOverlay.ts
21864
- var SIZE_PX3 = {
21865
- auto: 13,
21866
- tiny: 10,
21867
- small: 11,
21868
- normal: 13,
21869
- large: 16,
21870
- huge: 20
21871
- };
21872
- var TableOverlay = class {
21873
- constructor(container, theme, paneBounds) {
21874
- this.container = container;
21875
- this.theme = theme;
21876
- this.paneBounds = paneBounds;
21877
- this.lastTables = [];
21878
- if (getComputedStyle(container).position === "static") container.style.position = "relative";
21879
- this.root = document.createElement("div");
21880
- Object.assign(this.root.style, {
21881
- position: "absolute",
21882
- inset: "0",
21883
- pointerEvents: "none",
21884
- overflow: "hidden",
21885
- zIndex: "3"
21886
- });
21887
- container.appendChild(this.root);
21888
- }
21889
- update(tables) {
21890
- this.lastTables = tables;
21891
- this.root.replaceChildren();
21892
- for (const t of tables) this.root.appendChild(this.renderTable(t));
21893
- }
21894
- /** Re-render at the current pane geometry — after layout settles or on resize. */
21895
- reposition() {
21896
- if (this.root.isConnected) this.update(this.lastTables);
21897
- }
21898
- /** Show/hide the whole overlay. Tables anchor to pane corners, not to bars, so unlike the
21899
- * series content they DON'T vanish with an emptied chart — the loading state hides them. */
21900
- setVisible(visible) {
21901
- this.root.style.display = visible ? "" : "none";
21902
- }
21903
- destroy() {
21904
- this.root.remove();
21905
- }
21906
- renderTable(t) {
21907
- const wrap2 = document.createElement("div");
21908
- wrap2.style.position = "absolute";
21909
- this.anchor(wrap2, t.position, this.paneBounds(t.paneId));
21910
- const table = document.createElement("table");
21911
- Object.assign(table.style, {
21912
- borderCollapse: "collapse",
21913
- background: t.bgColor ?? "transparent",
21914
- fontFamily: this.theme.fontFamily || "sans-serif",
21915
- // Frame as an inset shadow (not a `border`) so it stays independent of the
21916
- // collapsed cell borders even when frame_width != border_width.
21917
- boxShadow: t.frameColor && t.frameWidth > 0 ? `inset 0 0 0 ${t.frameWidth}px ${t.frameColor}` : "none",
21918
- border: "none",
21919
- tableLayout: "auto",
21920
- // Re-enable pointer events on the table only (root is none) so cell tooltips work.
21921
- pointerEvents: "auto"
21922
- });
21923
- const span = /* @__PURE__ */ new Map();
21924
- const skip = /* @__PURE__ */ new Set();
21925
- for (const m of t.merges) {
21926
- span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
21927
- for (let r = m.startRow; r <= m.endRow; r += 1) {
21928
- for (let c = m.startCol; c <= m.endCol; c += 1) {
21929
- if (r !== m.startRow || c !== m.startCol) skip.add(`${r}:${c}`);
21930
- }
21931
- }
21932
- }
21933
- const cellBorder = t.borderColor && t.borderWidth > 0 ? `${t.borderWidth}px solid ${t.borderColor}` : "none";
21934
- for (let r = 0; r < t.rows; r += 1) {
21935
- const tr = document.createElement("tr");
21936
- for (let c = 0; c < t.columns; c += 1) {
21937
- const cell = t.cells[r]?.[c] ?? null;
21938
- if (skip.has(`${r}:${c}`) || cell?.merged) continue;
21939
- const td = document.createElement("td");
21940
- const sp = span.get(`${r}:${c}`);
21941
- if (sp) {
21942
- if (sp.cs > 1) td.colSpan = sp.cs;
21943
- if (sp.rs > 1) td.rowSpan = sp.rs;
21944
- }
21945
- Object.assign(td.style, {
21946
- border: cellBorder,
21947
- padding: "2px 6px",
21948
- background: cell?.bgColor ?? "transparent",
21949
- color: cell?.textColor ?? this.theme.textColor,
21950
- textAlign: cell?.hAlign ?? "center",
21951
- verticalAlign: cell?.vAlign === "top" ? "top" : cell?.vAlign === "bottom" ? "bottom" : "middle",
21952
- fontSize: `${SIZE_PX3[cell?.textSize ?? "normal"]}px`,
21953
- fontFamily: cell?.fontFamily === "monospace" ? "monospace" : "inherit",
21954
- fontWeight: cell?.bold ? "bold" : "normal",
21955
- fontStyle: cell?.italic ? "italic" : "normal",
21956
- whiteSpace: "pre-line"
21957
- });
21958
- if (cell?.tooltip) td.title = cell.tooltip;
21959
- td.textContent = cell?.text ?? "";
21960
- tr.appendChild(td);
21961
- }
21962
- table.appendChild(tr);
21963
- }
21964
- wrap2.appendChild(table);
21965
- return wrap2;
21966
- }
21967
- /**
21968
- * Position the wrapper at a Pine `position.*` corner/edge of the table's PANE
21969
- * (not the whole chart), inset past the right price axis so it never overlaps
21970
- * the Y-axis labels.
21971
- */
21972
- anchor(el, position, b) {
21973
- const m = 6;
21974
- const containerH = this.root.clientHeight || this.container.clientHeight;
21975
- const paneBottom = b.top + b.height;
21976
- if (position.startsWith("top")) el.style.top = `${b.top + m}px`;
21977
- else if (position.startsWith("bottom")) el.style.bottom = `${Math.max(0, containerH - paneBottom) + m}px`;
21978
- else el.style.top = `${b.top + b.height / 2}px`;
21979
- if (position.endsWith("left")) el.style.left = `${m}px`;
21980
- else if (position.endsWith("right")) el.style.right = `${b.rightAxis + m}px`;
21981
- else el.style.left = `calc(50% - ${b.rightAxis / 2}px)`;
21982
- const tx = position.endsWith("center") ? "-50%" : "0";
21983
- const ty = position.startsWith("middle") ? "-50%" : "0";
21984
- if (tx !== "0" || ty !== "0") el.style.transform = `translate(${tx}, ${ty})`;
21985
- }
21986
- };
21987
-
21988
22343
  // src/renderers/native/capabilities.ts
21989
22344
  var NATIVE_CAPABILITIES = {
21990
22345
  panes: true,
@@ -22005,7 +22360,7 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
22005
22360
  drawingDepth: true,
22006
22361
  // drawings share the series' z space (backend-composited interleave layers)
22007
22362
  tables: true,
22008
- // reuses the DOM TableOverlay
22363
+ // canvas-painted into the owning indicator's interleave slice
22009
22364
  trades: true,
22010
22365
  // strategy order-fill markers (arrows + labels + fill-price ticks)
22011
22366
  inputsUI: true
@@ -22036,6 +22391,9 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
22036
22391
  if (spacing < CANDLE_BODY_MIN_SPACING) return "wick";
22037
22392
  return "full";
22038
22393
  }
22394
+ function snapY(yCss, dpr) {
22395
+ return Math.round(yCss * dpr) / dpr;
22396
+ }
22039
22397
  function candleGeometry(xCss, spacing, dpr, bodyScale = 1) {
22040
22398
  const wickDev = Math.max(1, Math.round(wickWidth(spacing) * dpr));
22041
22399
  const wickLeftDev = Math.round(xCss * dpr - wickDev / 2);
@@ -22841,8 +23199,12 @@ void main() {
22841
23199
  return sc === pane.scale ? pane : { ...pane, scale: sc };
22842
23200
  };
22843
23201
  b.alpha = this.modelAlpha;
22844
- for (const m of models) for (const bgSpan of m.backgrounds) this.emitBackground(b, bgSpan, pane, coords);
22845
- for (const m of models) for (const f of m.fills) this.emitFill(b, m, f, effPane(m), coords, i0, i1, scene.offsetOf(m.id));
23202
+ for (const m of models) for (const bgSpan of m.backgrounds) if (bgSpan.overlay !== true) this.emitBackground(b, bgSpan, pane, coords);
23203
+ if (isPrice) {
23204
+ for (const m of scene.indicators.values()) {
23205
+ for (const bgSpan of m.backgrounds) if (bgSpan.overlay === true) this.emitBackground(b, bgSpan, pane, coords);
23206
+ }
23207
+ }
22846
23208
  const drawCandles = isPrice && !scene.candlesHidden;
22847
23209
  let candleDrawn = false;
22848
23210
  for (const m of models) {
@@ -22856,15 +23218,26 @@ void main() {
22856
23218
  b.alpha = this.modelAlpha;
22857
23219
  const off = scene.offsetOf(m.id);
22858
23220
  const mp = effPane(m);
22859
- for (const s of m.series) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
23221
+ for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, mp, coords, i0, i1, off);
23222
+ for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
22860
23223
  }
22861
23224
  if (drawCandles && !candleDrawn) {
22862
23225
  drawSlicesUpTo(scene.candleZ);
22863
23226
  b.alpha = this.candleStructureAlpha;
22864
23227
  this.emitPriceSeries(b, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
22865
23228
  }
22866
- drawSlicesUpTo(Infinity);
22867
23229
  b.alpha = this.modelAlpha;
23230
+ if (isPrice) {
23231
+ for (const m of scene.indicators.values()) {
23232
+ const off = scene.offsetOf(m.id);
23233
+ for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, i1, off);
23234
+ }
23235
+ for (const m of scene.indicators.values()) {
23236
+ const off = scene.offsetOf(m.id);
23237
+ for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0, i1, theme, off);
23238
+ }
23239
+ }
23240
+ drawSlicesUpTo(Infinity);
22868
23241
  for (const m of models) {
22869
23242
  const mp = effPane(m);
22870
23243
  for (const pl of m.priceLines) this.emitHline(b, pl, mp, coords, dataW, theme);
@@ -22911,7 +23284,7 @@ void main() {
22911
23284
  for (const pane of scene.orderedPanes()) {
22912
23285
  const b = this.glowBatch;
22913
23286
  b.reset();
22914
- this.emitGlowSources(b, scene.indicatorsForPane(pane.id), pane, coords, i0, i1, (id) => scene.offsetOf(id));
23287
+ this.emitGlowSources(b, scene, pane, coords, i0, i1);
22915
23288
  if (b.vertexCount === 0) continue;
22916
23289
  const topH = Math.round(pane.bounds.top * dpr * 0.5);
22917
23290
  const botH = Math.round((pane.bounds.top + pane.bounds.height) * dpr * 0.5);
@@ -22980,15 +23353,23 @@ void main() {
22980
23353
  gl.bindVertexArray(null);
22981
23354
  return true;
22982
23355
  }
22983
- /** The "neon" elements that glow: line/area/step lines + point markers (not candles/fills/bars). */
22984
- emitGlowSources(b, models, pane, coords, i0, i1, offsetOf = () => 0) {
22985
- for (const m of models) {
22986
- const off = offsetOf(m.id);
22987
- for (const s of m.series) {
22988
- if (!isLineLikeSeries(s) || s.visible === false) continue;
22989
- if (s.kind === "histogram" || s.kind === "columns") continue;
22990
- if (s.kind === "circles" || s.kind === "cross") this.emitPointMarkers(b, s, pane, coords, i0, i1, off);
22991
- else this.emitPolyline(b, s, pane, coords, i0, i1, s.kind === "step", off);
23356
+ /** The "neon" elements that glow: line/area/step lines + point markers (not candles/fills/bars).
23357
+ * Routing mirrors the main pass: own series per pane, force_overlay series on the price pane. */
23358
+ emitGlowSources(b, scene, pane, coords, i0, i1) {
23359
+ const emitOne = (s, off) => {
23360
+ if (!isLineLikeSeries(s) || s.visible === false) return;
23361
+ if (s.kind === "histogram" || s.kind === "columns") return;
23362
+ if (s.kind === "circles" || s.kind === "cross") this.emitPointMarkers(b, s, pane, coords, i0, i1, off);
23363
+ else this.emitPolyline(b, s, pane, coords, i0, i1, s.kind === "step", off);
23364
+ };
23365
+ for (const m of scene.indicatorsForPane(pane.id)) {
23366
+ const off = scene.offsetOf(m.id);
23367
+ for (const s of m.series) if (s.overlay !== true) emitOne(s, off);
23368
+ }
23369
+ if (pane.kind === "price") {
23370
+ for (const m of scene.indicators.values()) {
23371
+ const off = scene.offsetOf(m.id);
23372
+ for (const s of m.series) if (s.overlay === true) emitOne(s, off);
22992
23373
  }
22993
23374
  }
22994
23375
  }
@@ -23251,12 +23632,12 @@ void main() {
23251
23632
  if (drawBody) {
23252
23633
  const oY = coords.priceToY(bar.open, pane.scale, pane.bounds);
23253
23634
  const cY = coords.priceToY(bar.close, pane.scale, pane.bounds);
23254
- bodyTop = Math.min(oY, cY);
23255
- bodyH = Math.max(1, Math.abs(cY - oY));
23635
+ bodyTop = snapY(Math.min(oY, cY), coords.dpr);
23636
+ bodyH = Math.max(1 / coords.dpr, snapY(Math.max(oY, cY), coords.dpr) - bodyTop);
23256
23637
  }
23257
23638
  if (cs.wickVisible) {
23258
- const hY = coords.priceToY(bar.high, pane.scale, pane.bounds);
23259
- const lY = coords.priceToY(bar.low, pane.scale, pane.bounds);
23639
+ const hY = snapY(coords.priceToY(bar.high, pane.scale, pane.bounds), coords.dpr);
23640
+ const lY = snapY(coords.priceToY(bar.low, pane.scale, pane.bounds), coords.dpr);
23260
23641
  b.alpha = this.candleStructureAlpha;
23261
23642
  const wCol = parseColor((isUp ? cs.wickUpColor : cs.wickDownColor) ?? (drawBody ? dir : bodyColorStr));
23262
23643
  if (drawBody) {
@@ -23271,9 +23652,9 @@ void main() {
23271
23652
  b.alpha = this.candleBodyAlpha;
23272
23653
  b.rect(g.bodyX, bodyTop, g.bodyW, bodyH, c);
23273
23654
  }
23274
- if (cs.borderVisible || bc || fading && cs.bodyVisible) {
23655
+ if (cs.borderVisible || fading && cs.bodyVisible) {
23275
23656
  b.alpha = this.candleStructureAlpha;
23276
- const bord = cs.borderVisible || bc ? parseColor((isUp ? cs.borderUpColor : cs.borderDownColor) ?? dir) : c;
23657
+ const bord = cs.borderVisible ? parseColor((isUp ? cs.borderUpColor : cs.borderDownColor) ?? bodyColorStr) : c;
23277
23658
  const bw = Math.max(0, g.bodyW - 1);
23278
23659
  const bh = Math.max(0, bodyH - 1);
23279
23660
  b.rectStroke(g.bodyX + 0.5, bodyTop + 0.5, bw, bh, 1, bord);
@@ -24589,9 +24970,10 @@ void main() {
24589
24970
  * so each indicator arrives behind the candles (and behind older indicators);
24590
24971
  * `setIndicatorZ`/`bringToFront`/`sendToBack` change it. */
24591
24972
  this.seriesZ = /* @__PURE__ */ new Map();
24592
- /** Per-pane raster layers of user drawings interleaved into the series stack each is a
24973
+ /** Per-pane raster layers of drawings interleaved into the series stack (each
24974
+ * indicator's Pine drawings at its model's z, plus in-stack user drawings) — each a
24593
24975
  * prepainted canvas the backend composites just before the series carrying `beforeZ`.
24594
- * Rebuilt by the renderer per data frame; empty when every drawing sits over the stack. */
24976
+ * Rebuilt by the renderer per data frame. */
24595
24977
  this.drawingSlices = /* @__PURE__ */ new Map();
24596
24978
  /** Per-model index offset: the chart bar index of the model's `anchorTime` — its
24597
24979
  * index-aligned payloads (dense series arrays, `bar_index` drawings) count from that
@@ -24825,8 +25207,12 @@ void main() {
24825
25207
  return sc === pane.scale ? pane : { ...pane, scale: sc };
24826
25208
  };
24827
25209
  ctx.globalAlpha = this.modelAlpha;
24828
- for (const m of models) for (const bg of m.backgrounds) this.drawBackground(ctx, bg, pane, coords);
24829
- for (const m of models) for (const f of m.fills) this.drawFill(ctx, m, f, effPane(m), coords, i0, i1, scene.offsetOf(m.id));
25210
+ for (const m of models) for (const bg of m.backgrounds) if (bg.overlay !== true) this.drawBackground(ctx, bg, pane, coords);
25211
+ if (isPrice) {
25212
+ for (const m of scene.indicators.values()) {
25213
+ for (const bg of m.backgrounds) if (bg.overlay === true) this.drawBackground(ctx, bg, pane, coords);
25214
+ }
25215
+ }
24830
25216
  const slices = scene.drawingSlices.get(pane.id) ?? [];
24831
25217
  let si = 0;
24832
25218
  const drawSlicesUpTo = (z) => {
@@ -24848,13 +25234,25 @@ void main() {
24848
25234
  ctx.globalAlpha = this.modelAlpha;
24849
25235
  const off = scene.offsetOf(m.id);
24850
25236
  const mp = effPane(m);
24851
- for (const s of m.series) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
25237
+ for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, mp, coords, i0, i1, off);
25238
+ for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
24852
25239
  }
24853
25240
  if (drawCandles && !candleDrawn) {
24854
25241
  drawSlicesUpTo(scene.candleZ);
24855
25242
  ctx.globalAlpha = this.candleStructureAlpha;
24856
25243
  this.drawPriceSeries(ctx, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
24857
25244
  }
25245
+ if (isPrice) {
25246
+ ctx.globalAlpha = this.modelAlpha;
25247
+ for (const m of scene.indicators.values()) {
25248
+ const off = scene.offsetOf(m.id);
25249
+ for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, off);
25250
+ }
25251
+ for (const m of scene.indicators.values()) {
25252
+ const off = scene.offsetOf(m.id);
25253
+ for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0, i1, theme, off);
25254
+ }
25255
+ }
24858
25256
  drawSlicesUpTo(Infinity);
24859
25257
  ctx.globalAlpha = this.modelAlpha;
24860
25258
  for (const m of models) {
@@ -25099,13 +25497,13 @@ void main() {
25099
25497
  if (drawBody) {
25100
25498
  const oY = coords.priceToY(b.open, pane.scale, pane.bounds);
25101
25499
  const cY = coords.priceToY(b.close, pane.scale, pane.bounds);
25102
- top = Math.min(oY, cY);
25103
- bodyH = Math.max(1, Math.abs(cY - oY));
25500
+ top = snapY(Math.min(oY, cY), coords.dpr);
25501
+ bodyH = Math.max(1 / coords.dpr, snapY(Math.max(oY, cY), coords.dpr) - top);
25104
25502
  }
25105
25503
  if (cs.wickVisible) {
25106
25504
  const wick = (up ? cs.wickUpColor : cs.wickDownColor) ?? (drawBody ? dir : color);
25107
- const hY = coords.priceToY(b.high, pane.scale, pane.bounds);
25108
- const lY = coords.priceToY(b.low, pane.scale, pane.bounds);
25505
+ const hY = snapY(coords.priceToY(b.high, pane.scale, pane.bounds), coords.dpr);
25506
+ const lY = snapY(coords.priceToY(b.low, pane.scale, pane.bounds), coords.dpr);
25109
25507
  ctx.globalAlpha = this.candleStructureAlpha;
25110
25508
  ctx.strokeStyle = wick;
25111
25509
  ctx.lineWidth = g.wickW;
@@ -25127,9 +25525,9 @@ void main() {
25127
25525
  ctx.fillStyle = color;
25128
25526
  ctx.fillRect(g.bodyX, top, g.bodyW, bodyH);
25129
25527
  }
25130
- if (cs.borderVisible || bc || fading && cs.bodyVisible) {
25528
+ if (cs.borderVisible || fading && cs.bodyVisible) {
25131
25529
  ctx.globalAlpha = this.candleStructureAlpha;
25132
- ctx.strokeStyle = cs.borderVisible || bc ? (up ? cs.borderUpColor : cs.borderDownColor) ?? dir : color;
25530
+ ctx.strokeStyle = cs.borderVisible ? (up ? cs.borderUpColor : cs.borderDownColor) ?? color : color;
25133
25531
  ctx.lineWidth = 1;
25134
25532
  const bw = Math.max(0, g.bodyW - 1);
25135
25533
  const bh = Math.max(0, bodyH - 1);
@@ -25552,6 +25950,19 @@ void main() {
25552
25950
 
25553
25951
  // src/renderers/shared/DrawingSceneRenderer.ts
25554
25952
  var EMPTY_DRAWING_SET = { lines: [], boxes: [], labels: [], polylines: [], linefills: [] };
25953
+ function modelDrawingSet(m, overlay) {
25954
+ const want = (d) => Boolean(d.overlay) === overlay;
25955
+ return {
25956
+ lines: (m.lines ?? []).filter(want),
25957
+ boxes: (m.boxes ?? []).filter(want),
25958
+ labels: (m.labels ?? []).filter(want),
25959
+ polylines: (m.polylines ?? []).filter(want),
25960
+ linefills: (m.linefills ?? []).filter(want)
25961
+ };
25962
+ }
25963
+ function drawingSetEmpty(s) {
25964
+ return !s.lines.length && !s.boxes.length && !s.labels.length && !s.polylines.length && !s.linefills.length;
25965
+ }
25555
25966
  function fontSizePx(size3) {
25556
25967
  return size3 === "auto" ? 12 : namedFontSize(size3);
25557
25968
  }
@@ -25561,6 +25972,8 @@ void main() {
25561
25972
  this.set = set;
25562
25973
  /** measureText width cache, keyed by `${font} ${text}`, persists across frames. */
25563
25974
  this.widthCache = /* @__PURE__ */ new Map();
25975
+ /** Tooltip hit-rects captured while drawing the CURRENT set (rebuilt per render). */
25976
+ this.tipRegions = [];
25564
25977
  /**
25565
25978
  * Index offset of the CURRENT set's model: its `xloc:'bar_index'` coordinates count
25566
25979
  * from the model's anchor bar, so they shift by this to land on chart logical indices.
@@ -25581,8 +25994,13 @@ void main() {
25581
25994
  const s = this.set;
25582
25995
  return !s.lines.length && !s.boxes.length && !s.labels.length && !s.polylines.length && !s.linefills.length;
25583
25996
  }
25997
+ /** Tooltip hit-rects of the labels drawn by the LAST `render` call (same coords as `ctx`). */
25998
+ labelTipRegions() {
25999
+ return this.tipRegions;
26000
+ }
25584
26001
  /** Draw the whole set into `ctx` using the supplied coordinate closures. */
25585
26002
  render(ctx, W, H, xOf, yOf) {
26003
+ this.tipRegions = [];
25586
26004
  if (this.isEmpty()) return;
25587
26005
  ctx.save();
25588
26006
  this.drawLinefills(ctx, W, H, xOf, yOf);
@@ -25930,13 +26348,35 @@ void main() {
25930
26348
  if (this.isPointShape(lb.style)) {
25931
26349
  if (!lb.noFill) this.drawLabelShape(ctx, lb.style, px, py, fontPx, color);
25932
26350
  if (lb.text) this.drawLabelText(ctx, lb, px, py + fontPx, fontPx);
25933
- } else if (lb.style === "none" || lb.style === "text_outline" || lb.noFill) {
25934
- if (lb.text) this.drawLabelText(ctx, lb, px, py, fontPx, lb.style === "text_outline");
26351
+ if (lb.tooltip) {
26352
+ const r = Math.max(4, fontPx * 0.6) + 3;
26353
+ this.tipRegions.push({ left: px - r, top: py - r, right: px + r, bottom: py + r, text: lb.tooltip });
26354
+ }
26355
+ } else if (lb.style === "none" || lb.style === "text_outline") {
26356
+ if (lb.text) {
26357
+ this.drawLabelText(ctx, lb, px, py, fontPx, lb.style === "text_outline");
26358
+ if (lb.tooltip) this.tipRegions.push(this.textRegion(ctx, lb, px, py, fontPx, lb.tooltip));
26359
+ }
25935
26360
  } else {
25936
- this.drawBubble(ctx, lb, px, py, fontPx, color);
26361
+ const r = this.drawBubble(ctx, lb, px, py, fontPx, color);
26362
+ if (lb.tooltip) this.tipRegions.push({ left: r.x, top: r.y, right: r.x + r.w, bottom: r.y + r.h, text: lb.tooltip });
25937
26363
  }
25938
26364
  }
25939
26365
  }
26366
+ /** Canvas font for a label's text — family + Pine `text_formatting` (bold/italic). */
26367
+ labelFont(lb, fontPx) {
26368
+ const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
26369
+ return `${lb.italic ? "italic " : ""}${lb.bold ? "bold " : ""}${fontPx}px ${family}`;
26370
+ }
26371
+ /** Hover rect of a text-only label (style none/text_outline/noFill), centered like drawLabelText. */
26372
+ textRegion(ctx, lb, cx, cy, fontPx, text) {
26373
+ const font = this.labelFont(lb, fontPx);
26374
+ const lines = (lb.text ?? "").split("\n");
26375
+ const w = Math.max(1, ...lines.map((l) => this.measure(ctx, font, l)));
26376
+ const h = fontPx * 1.25 * lines.length;
26377
+ const left = lb.textAlign === "left" ? cx : lb.textAlign === "right" ? cx - w : cx - w / 2;
26378
+ return { left, top: cy - h / 2, right: left + w, bottom: cy + h / 2, text };
26379
+ }
25940
26380
  isPointShape(style) {
25941
26381
  switch (style) {
25942
26382
  case "circle":
@@ -25955,8 +26395,7 @@ void main() {
25955
26395
  }
25956
26396
  }
25957
26397
  drawLabelText(ctx, lb, cx, cy, fontPx, outline = false) {
25958
- const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
25959
- ctx.font = `${fontPx}px ${family}`;
26398
+ ctx.font = this.labelFont(lb, fontPx);
25960
26399
  ctx.textAlign = lb.textAlign === "left" ? "left" : lb.textAlign === "right" ? "right" : "center";
25961
26400
  ctx.textBaseline = "middle";
25962
26401
  const lines = lb.text.split("\n");
@@ -26036,9 +26475,9 @@ void main() {
26036
26475
  ctx.fill();
26037
26476
  }
26038
26477
  }
26478
+ /** Draw a bubble label; returns the bubble body rect (for tooltip hit-testing). */
26039
26479
  drawBubble(ctx, lb, px, py, fontPx, color) {
26040
- const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
26041
- ctx.font = `${fontPx}px ${family}`;
26480
+ ctx.font = this.labelFont(lb, fontPx);
26042
26481
  const lines = (lb.text ?? "").split("\n");
26043
26482
  const padX = 6;
26044
26483
  const padY = 4;
@@ -26096,38 +26535,51 @@ void main() {
26096
26535
  by = py - h - ptr;
26097
26536
  pointer = "down";
26098
26537
  }
26099
- ctx.fillStyle = color;
26100
- this.roundRect(ctx, bx, by, w, h, 4);
26101
- ctx.fill();
26102
- if (pointer !== "none") {
26103
- ctx.beginPath();
26104
- if (pointer === "down") {
26105
- ctx.moveTo(px - ptr, by + h);
26106
- ctx.lineTo(px + ptr, by + h);
26107
- ctx.lineTo(px, py);
26108
- } else if (pointer === "up") {
26109
- ctx.moveTo(px - ptr, by);
26110
- ctx.lineTo(px + ptr, by);
26111
- ctx.lineTo(px, py);
26112
- } else if (pointer === "left") {
26113
- ctx.moveTo(bx, py - ptr);
26114
- ctx.lineTo(bx, py + ptr);
26115
- ctx.lineTo(px, py);
26116
- } else {
26117
- ctx.moveTo(bx + w, py - ptr);
26118
- ctx.lineTo(bx + w, py + ptr);
26119
- ctx.lineTo(px, py);
26120
- }
26121
- ctx.closePath();
26538
+ if (!lb.noFill) {
26539
+ ctx.fillStyle = color;
26540
+ this.roundRect(ctx, bx, by, w, h, 4);
26122
26541
  ctx.fill();
26542
+ if (pointer !== "none") {
26543
+ ctx.beginPath();
26544
+ if (pointer === "down") {
26545
+ ctx.moveTo(px - ptr, by + h);
26546
+ ctx.lineTo(px + ptr, by + h);
26547
+ ctx.lineTo(px, py);
26548
+ } else if (pointer === "up") {
26549
+ ctx.moveTo(px - ptr, by);
26550
+ ctx.lineTo(px + ptr, by);
26551
+ ctx.lineTo(px, py);
26552
+ } else if (pointer === "left") {
26553
+ ctx.moveTo(bx, py - ptr);
26554
+ ctx.lineTo(bx, py + ptr);
26555
+ ctx.lineTo(px, py);
26556
+ } else {
26557
+ ctx.moveTo(bx + w, py - ptr);
26558
+ ctx.lineTo(bx + w, py + ptr);
26559
+ ctx.lineTo(px, py);
26560
+ }
26561
+ ctx.closePath();
26562
+ ctx.fill();
26563
+ }
26123
26564
  }
26124
26565
  if (lb.text) {
26125
- ctx.fillStyle = lb.textColor ?? contrastColor(color);
26126
- ctx.textAlign = "center";
26566
+ ctx.fillStyle = lb.textColor ?? (lb.noFill ? this.deps.theme.textColor : contrastColor(color));
26127
26567
  ctx.textBaseline = "middle";
26568
+ let tx;
26569
+ if (lb.textAlign === "left") {
26570
+ ctx.textAlign = "left";
26571
+ tx = bx + padX;
26572
+ } else if (lb.textAlign === "right") {
26573
+ ctx.textAlign = "right";
26574
+ tx = bx + w - padX;
26575
+ } else {
26576
+ ctx.textAlign = "center";
26577
+ tx = bx + w / 2;
26578
+ }
26128
26579
  const startY = by + padY + lineH / 2;
26129
- for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], bx + w / 2, startY + i * lineH);
26580
+ for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], tx, startY + i * lineH);
26130
26581
  }
26582
+ return { x: bx, y: by, w, h };
26131
26583
  }
26132
26584
  roundRect(ctx, x, y, w, h, r) {
26133
26585
  const rr = Math.min(r, w / 2, h / 2);
@@ -26363,7 +26815,7 @@ void main() {
26363
26815
  this.ctx = null;
26364
26816
  // The color for axis tick labels — the host-passed surface text, set each frame in render().
26365
26817
  this.axisTextColor = DARK_THEME.textColor;
26366
- // Shared Pine-drawing renderer (line/box/label/polyline/linefill); widthCache persists.
26818
+ // Shared Pine-drawing renderer, used here for autoscale geometry only; widthCache persists.
26367
26819
  this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
26368
26820
  }
26369
26821
  mount(canvas) {
@@ -26388,8 +26840,8 @@ void main() {
26388
26840
  */
26389
26841
  paneDrawingsRange(ownModels, scene, isPricePane, vr) {
26390
26842
  let dr = null;
26391
- for (const m of ownModels) dr = unionRange(dr, this.drawingsRange(this.ownDrawings(m), vr, scene.offsetOf(m.id)));
26392
- if (isPricePane) for (const m of scene.indicators.values()) dr = unionRange(dr, this.drawingsRange(this.overlayDrawings(m), vr, scene.offsetOf(m.id)));
26843
+ for (const m of ownModels) dr = unionRange(dr, this.drawingsRange(modelDrawingSet(m, false), vr, scene.offsetOf(m.id)));
26844
+ if (isPricePane) for (const m of scene.indicators.values()) dr = unionRange(dr, this.drawingsRange(modelDrawingSet(m, true), vr, scene.offsetOf(m.id)));
26393
26845
  return dr;
26394
26846
  }
26395
26847
  /** Clear the chrome canvas and draw drawings + axes + current-price line.
@@ -26421,17 +26873,6 @@ void main() {
26421
26873
  return;
26422
26874
  }
26423
26875
  const pricePane = panes.find((p) => p.kind === "price") ?? null;
26424
- for (const pane of panes) {
26425
- if (pane.collapsed) continue;
26426
- for (const m of scene.indicatorsForPane(pane.id)) {
26427
- const sc = scene.scaleFor(m, pane);
26428
- const mp = sc === pane.scale ? pane : { ...pane, scale: sc };
26429
- this.renderDrawings(ctx, coords, this.ownDrawings(m), mp, dataW, scene.offsetOf(m.id));
26430
- }
26431
- }
26432
- if (pricePane) {
26433
- for (const m of scene.indicators.values()) this.renderDrawings(ctx, coords, this.overlayDrawings(m), pricePane, dataW, scene.offsetOf(m.id));
26434
- }
26435
26876
  if (pricePane && !pricePane.collapsed && scene.tradeMarkers.visible) {
26436
26877
  for (const m of scene.indicators.values()) {
26437
26878
  if (m.trades?.length) this.renderTrades(ctx, coords, scene, theme, m.trades, pricePane, dataW);
@@ -26447,25 +26888,6 @@ void main() {
26447
26888
  this.canvas = null;
26448
26889
  this.ctx = null;
26449
26890
  }
26450
- // ── Pine-drawing helpers (own vs force_overlay routing) ──
26451
- ownDrawings(m) {
26452
- return {
26453
- lines: (m.lines ?? []).filter((d) => !d.overlay),
26454
- boxes: (m.boxes ?? []).filter((d) => !d.overlay),
26455
- labels: (m.labels ?? []).filter((d) => !d.overlay),
26456
- polylines: (m.polylines ?? []).filter((d) => !d.overlay),
26457
- linefills: (m.linefills ?? []).filter((d) => !d.overlay)
26458
- };
26459
- }
26460
- overlayDrawings(m) {
26461
- return {
26462
- lines: (m.lines ?? []).filter((d) => d.overlay),
26463
- boxes: (m.boxes ?? []).filter((d) => d.overlay),
26464
- labels: (m.labels ?? []).filter((d) => d.overlay),
26465
- polylines: (m.polylines ?? []).filter((d) => d.overlay),
26466
- linefills: (m.linefills ?? []).filter((d) => d.overlay)
26467
- };
26468
- }
26469
26891
  drawingsRange(set, vr, indexOffset = 0) {
26470
26892
  this.drawScene.setSet(set, indexOffset);
26471
26893
  if (this.drawScene.isEmpty()) return null;
@@ -26499,23 +26921,6 @@ void main() {
26499
26921
  );
26500
26922
  ctx.restore();
26501
26923
  }
26502
- renderDrawings(ctx, coords, set, pane, dataW, indexOffset = 0) {
26503
- this.drawScene.setSet(set, indexOffset);
26504
- if (this.drawScene.isEmpty()) return;
26505
- ctx.save();
26506
- ctx.translate(0, pane.bounds.top);
26507
- ctx.beginPath();
26508
- ctx.rect(0, 0, dataW, pane.bounds.height);
26509
- ctx.clip();
26510
- this.drawScene.render(
26511
- ctx,
26512
- dataW,
26513
- pane.bounds.height,
26514
- (l) => coords.logicalToX(l),
26515
- (price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
26516
- );
26517
- ctx.restore();
26518
- }
26519
26924
  // ── axes ──
26520
26925
  drawPriceAxes(ctx, scene, coords, theme, dataW, panes) {
26521
26926
  ctx.strokeStyle = scene.style.borderColor ?? theme.borderColor;
@@ -26731,6 +27136,68 @@ void main() {
26731
27136
  return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
26732
27137
  }
26733
27138
 
27139
+ // src/renderers/native/chrome/LabelTooltip.ts
27140
+ var HOVER_DELAY_MS = 350;
27141
+ var LabelTooltip = class {
27142
+ constructor(plot, deps) {
27143
+ this.plot = plot;
27144
+ this.deps = deps;
27145
+ this.tip = null;
27146
+ this.timer = null;
27147
+ /** Text of the currently open OR armed tip — dedupes moves inside one label. */
27148
+ this.current = null;
27149
+ this.onMove = (e) => {
27150
+ if (e.pointerType !== "mouse") return;
27151
+ const rect = this.plot.getBoundingClientRect();
27152
+ const x = e.clientX - rect.left;
27153
+ const y = e.clientY - rect.top;
27154
+ const text = this.deps.lookup(x, y);
27155
+ if (!text) {
27156
+ this.clear();
27157
+ return;
27158
+ }
27159
+ if (text === this.current) return;
27160
+ this.clear();
27161
+ this.current = text;
27162
+ this.timer = window.setTimeout(() => this.show(text, x, y), HOVER_DELAY_MS);
27163
+ };
27164
+ this.onLeave = () => {
27165
+ this.clear();
27166
+ };
27167
+ plot.addEventListener("pointermove", this.onMove);
27168
+ plot.addEventListener("pointerleave", this.onLeave);
27169
+ plot.addEventListener("pointerdown", this.onLeave);
27170
+ }
27171
+ destroy() {
27172
+ this.plot.removeEventListener("pointermove", this.onMove);
27173
+ this.plot.removeEventListener("pointerleave", this.onLeave);
27174
+ this.plot.removeEventListener("pointerdown", this.onLeave);
27175
+ this.clear();
27176
+ }
27177
+ show(text, x, y) {
27178
+ const doc = this.plot.ownerDocument;
27179
+ const tip = doc.createElement("div");
27180
+ tip.textContent = text;
27181
+ tip.style.cssText = "position:absolute;z-index:var(--vela-z-tooltip);pointer-events:none;background:var(--vela-bg);border:1px solid var(--vela-border);color:var(--vela-fg);border-radius:var(--vela-radius-md);padding:4px 9px;box-shadow:var(--vela-shadow);font:var(--vela-font-size-md) var(--vela-font);max-width:260px;";
27182
+ applyChromeTokens(tip, this.deps.theme());
27183
+ this.plot.appendChild(tip);
27184
+ const pw = this.plot.clientWidth;
27185
+ const ph = this.plot.clientHeight;
27186
+ tip.style.left = `${Math.max(0, Math.min(x + 12, pw - tip.offsetWidth - 4))}px`;
27187
+ tip.style.top = `${Math.max(0, Math.min(y + 16, ph - tip.offsetHeight - 4))}px`;
27188
+ this.tip = tip;
27189
+ }
27190
+ clear() {
27191
+ if (this.timer !== null) {
27192
+ clearTimeout(this.timer);
27193
+ this.timer = null;
27194
+ }
27195
+ this.tip?.remove();
27196
+ this.tip = null;
27197
+ this.current = null;
27198
+ }
27199
+ };
27200
+
26734
27201
  // src/renderers/native/chrome/CrosshairRenderer.ts
26735
27202
  var CrosshairRenderer = class {
26736
27203
  constructor() {
@@ -30862,17 +31329,17 @@ ${overlayScrollbarCss(".vela-sd-pane")}
30862
31329
  return textGlyph(String(glyph), 15);
30863
31330
  }
30864
31331
  function stampSizeIcon(size3) {
30865
- return textGlyph("\u25CF", (SIZE_PX4[String(size3)] ?? 13) + 4);
31332
+ return textGlyph("\u25CF", (SIZE_PX3[String(size3)] ?? 13) + 4);
30866
31333
  }
30867
31334
  function sizeIcon(size3) {
30868
31335
  return textGlyph(String(size3).charAt(0).toUpperCase(), 15);
30869
31336
  }
30870
- var SIZE_PX4 = { small: 10, normal: 13, large: 16, huge: 20 };
31337
+ var SIZE_PX3 = { small: 10, normal: 13, large: 16, huge: 20 };
30871
31338
  function numbersSizeIcon(size3) {
30872
- return textGlyph("12", (SIZE_PX4[String(size3)] ?? 13) - 1, 16.5, 'font-weight="600"');
31339
+ return textGlyph("12", (SIZE_PX3[String(size3)] ?? 13) - 1, 16.5, 'font-weight="600"');
30873
31340
  }
30874
31341
  function labelSizeIcon(size3) {
30875
- return textGlyph("T", (SIZE_PX4[String(size3)] ?? 13) + 2);
31342
+ return textGlyph("T", (SIZE_PX3[String(size3)] ?? 13) + 2);
30876
31343
  }
30877
31344
  function capitalize(s) {
30878
31345
  return s.charAt(0).toUpperCase() + s.slice(1);
@@ -31036,7 +31503,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
31036
31503
  this.root.replaceChildren();
31037
31504
  this.groupCells.clear();
31038
31505
  this.groupIcons.clear();
31039
- this.cursorBtn = this.makeButton(CURSOR_ICON, "Cursor", () => this.onArm(null));
31506
+ this.cursorBtn = this.makeButton(CURSOR_ICON, "Cursor", () => this.onCursorClick());
31040
31507
  this.root.appendChild(this.cursorBtn);
31041
31508
  if (this.def.groups.length > 0) this.root.appendChild(this.divider());
31042
31509
  for (const g of this.def.groups) {
@@ -31140,6 +31607,14 @@ ${overlayScrollbarCss(".vela-sd-pane")}
31140
31607
  this.magnetIcon = icon2;
31141
31608
  return cell;
31142
31609
  }
31610
+ /** Cursor returns to select/idle: an active measure/eraser mode exits through its own
31611
+ * toggle callback (disarming a tool via `onArm(null)` alone can't — the host treats a
31612
+ * null arm as a no-op side effect of entering those modes), then the tool disarms. */
31613
+ onCursorClick() {
31614
+ if (this.measureActive) this.onMeasure();
31615
+ if (this.eraserActive) this.onEraser();
31616
+ this.onArm(null);
31617
+ }
31143
31618
  /** Clicking the icon arms the group's last-used tool (it does NOT open the flyout). */
31144
31619
  onGroupIconClick(group) {
31145
31620
  const type = this.lastUsed.get(group.id) ?? group.tools[0]?.type;
@@ -32471,6 +32946,315 @@ ${overlayScrollbarCss(".vela-sd-pane")}
32471
32946
  }
32472
32947
  };
32473
32948
 
32949
+ // src/renderers/shared/TableOverlay.ts
32950
+ var SIZE_PX4 = {
32951
+ auto: 13,
32952
+ tiny: 10,
32953
+ small: 11,
32954
+ normal: 13,
32955
+ large: 16,
32956
+ huge: 20
32957
+ };
32958
+ function fontPxOf(size3) {
32959
+ if (typeof size3 === "number") return size3 > 0 ? size3 : SIZE_PX4.auto;
32960
+ return SIZE_PX4[size3] ?? SIZE_PX4.auto;
32961
+ }
32962
+ function tableHasContent(t) {
32963
+ return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
32964
+ }
32965
+ function mergeRenderPlan(t) {
32966
+ const span = /* @__PURE__ */ new Map();
32967
+ const omit = /* @__PURE__ */ new Set();
32968
+ for (const m of t.merges) {
32969
+ span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
32970
+ for (let r = m.startRow; r <= m.endRow; r += 1) {
32971
+ for (let c = m.startCol; c <= m.endCol; c += 1) {
32972
+ if (r !== m.startRow || c !== m.startCol) omit.add(`${r}:${c}`);
32973
+ }
32974
+ }
32975
+ }
32976
+ for (let r = 0; r < t.rows; r += 1) {
32977
+ for (let c = 0; c < t.columns; c += 1) {
32978
+ if (t.cells[r]?.[c]?.merged && !span.has(`${r}:${c}`)) omit.add(`${r}:${c}`);
32979
+ }
32980
+ }
32981
+ for (const key of span.keys()) omit.delete(key);
32982
+ return { span, omit };
32983
+ }
32984
+
32985
+ // src/renderers/shared/TableCanvasRenderer.ts
32986
+ var PAD_X = 6;
32987
+ var PAD_Y = 2;
32988
+ var MARGIN = 6;
32989
+ var LINE_HEIGHT = 1.2;
32990
+ function paintTable(ctx, t, args, tips) {
32991
+ if (!tableHasContent(t)) return;
32992
+ const layout = layoutTable(ctx, t, args);
32993
+ if (!layout || layout.w <= 0 || layout.h <= 0) return;
32994
+ const fw = t.frameColor && t.frameWidth > 0 ? t.frameWidth : 0;
32995
+ const { x, y } = anchorOrigin(t.position, layout.w + 2 * fw, layout.h + 2 * fw, args);
32996
+ const x0 = x + fw;
32997
+ const y0 = y + fw;
32998
+ if (t.bgColor) {
32999
+ ctx.fillStyle = t.bgColor;
33000
+ ctx.fillRect(x0, y0, layout.w, layout.h);
33001
+ }
33002
+ if (fw > 0 && t.frameColor) {
33003
+ ctx.strokeStyle = t.frameColor;
33004
+ ctx.lineWidth = fw;
33005
+ ctx.strokeRect(x + fw / 2, y + fw / 2, layout.w + fw, layout.h + fw);
33006
+ }
33007
+ const colX = [0];
33008
+ for (const w of layout.colW) colX.push(colX[colX.length - 1] + w);
33009
+ const rowY = [0];
33010
+ for (const h of layout.rowH) rowY.push(rowY[rowY.length - 1] + h);
33011
+ const prevBaseline = ctx.textBaseline;
33012
+ const prevAlign = ctx.textAlign;
33013
+ ctx.textBaseline = "middle";
33014
+ for (const box of layout.boxes) {
33015
+ const rx = x0 + colX[box.c];
33016
+ const ry = y0 + rowY[box.r];
33017
+ const rw = colX[box.c + box.cs] - colX[box.c];
33018
+ const rh = rowY[box.r + box.rs] - rowY[box.r];
33019
+ const cell = box.cell;
33020
+ if (cell.bgColor) {
33021
+ ctx.fillStyle = cell.bgColor;
33022
+ ctx.fillRect(rx, ry, rw, rh);
33023
+ }
33024
+ const text = cell.text ?? "";
33025
+ if (text.length > 0) {
33026
+ const px = fontPxOf(cell.textSize);
33027
+ ctx.font = cellFont(cell, px, args.theme);
33028
+ ctx.fillStyle = cell.textColor ?? args.theme.textColor;
33029
+ const lines = text.split("\n");
33030
+ const blockH = lines.length * px * LINE_HEIGHT;
33031
+ const blockTop = cell.vAlign === "top" ? ry + PAD_Y : cell.vAlign === "bottom" ? ry + rh - PAD_Y - blockH : ry + (rh - blockH) / 2;
33032
+ const tx = cell.hAlign === "left" ? rx + PAD_X : cell.hAlign === "right" ? rx + rw - PAD_X : rx + rw / 2;
33033
+ ctx.textAlign = cell.hAlign;
33034
+ lines.forEach((line, i) => ctx.fillText(line, tx, blockTop + (i + 0.5) * px * LINE_HEIGHT));
33035
+ }
33036
+ if (cell.tooltip) tips.push({ left: rx, top: ry, right: rx + rw, bottom: ry + rh, text: cell.tooltip });
33037
+ }
33038
+ ctx.textBaseline = prevBaseline;
33039
+ ctx.textAlign = prevAlign;
33040
+ if (t.borderColor && t.borderWidth > 0) {
33041
+ ctx.strokeStyle = t.borderColor;
33042
+ ctx.lineWidth = t.borderWidth;
33043
+ const seen = /* @__PURE__ */ new Set();
33044
+ ctx.beginPath();
33045
+ const edge = (ax, ay, bx, by) => {
33046
+ const key = `${ax},${ay},${bx},${by}`;
33047
+ if (seen.has(key)) return;
33048
+ seen.add(key);
33049
+ ctx.moveTo(ax, ay);
33050
+ ctx.lineTo(bx, by);
33051
+ };
33052
+ for (const box of layout.boxes) {
33053
+ const l = Math.round(x0 + colX[box.c]);
33054
+ const r = Math.round(x0 + colX[box.c + box.cs]);
33055
+ const tp = Math.round(y0 + rowY[box.r]);
33056
+ const bt = Math.round(y0 + rowY[box.r + box.rs]);
33057
+ edge(l, tp, r, tp);
33058
+ edge(l, bt, r, bt);
33059
+ edge(l, tp, l, bt);
33060
+ edge(r, tp, r, bt);
33061
+ }
33062
+ ctx.stroke();
33063
+ }
33064
+ }
33065
+ function layoutTable(ctx, t, args) {
33066
+ const { span, omit } = mergeRenderPlan(t);
33067
+ const colW = new Array(t.columns).fill(0);
33068
+ const rowH = new Array(t.rows).fill(0);
33069
+ const boxes = [];
33070
+ for (let r = 0; r < t.rows; r += 1) {
33071
+ for (let c = 0; c < t.columns; c += 1) {
33072
+ if (omit.has(`${r}:${c}`)) continue;
33073
+ const cell = t.cells[r]?.[c];
33074
+ if (cell == null) continue;
33075
+ const sp = span.get(`${r}:${c}`);
33076
+ boxes.push({ cell, r, c, cs: Math.min(sp?.cs ?? 1, t.columns - c), rs: Math.min(sp?.rs ?? 1, t.rows - r) });
33077
+ }
33078
+ }
33079
+ if (boxes.length === 0) return null;
33080
+ const sizeOf = (cell) => {
33081
+ const px = fontPxOf(cell.textSize);
33082
+ ctx.font = cellFont(cell, px, args.theme);
33083
+ const lines = (cell.text ?? "").split("\n");
33084
+ let maxW = 0;
33085
+ for (const line of lines) maxW = Math.max(maxW, ctx.measureText(line).width);
33086
+ let w2 = Math.ceil(maxW) + 2 * PAD_X;
33087
+ let h2 = Math.ceil(lines.length * px * LINE_HEIGHT) + 2 * PAD_Y;
33088
+ if (cell.width) w2 = Math.max(w2, cell.width / 100 * args.plotWidth);
33089
+ if (cell.height) h2 = Math.max(h2, cell.height / 100 * args.paneHeight);
33090
+ return { w: w2, h: h2 };
33091
+ };
33092
+ const spanning = [];
33093
+ for (const box of boxes) {
33094
+ const { w: w2, h: h2 } = sizeOf(box.cell);
33095
+ if (box.cs === 1) colW[box.c] = Math.max(colW[box.c], w2);
33096
+ if (box.rs === 1) rowH[box.r] = Math.max(rowH[box.r], h2);
33097
+ if (box.cs > 1 || box.rs > 1) spanning.push({ box, w: w2, h: h2 });
33098
+ }
33099
+ for (const { box, w: w2, h: h2 } of spanning) {
33100
+ if (box.cs > 1) {
33101
+ let sum = 0;
33102
+ for (let c = box.c; c < box.c + box.cs; c += 1) sum += colW[c];
33103
+ if (w2 > sum) for (let c = box.c; c < box.c + box.cs; c += 1) colW[c] += (w2 - sum) / box.cs;
33104
+ }
33105
+ if (box.rs > 1) {
33106
+ let sum = 0;
33107
+ for (let r = box.r; r < box.r + box.rs; r += 1) sum += rowH[r];
33108
+ if (h2 > sum) for (let r = box.r; r < box.r + box.rs; r += 1) rowH[r] += (h2 - sum) / box.rs;
33109
+ }
33110
+ }
33111
+ let w = 0;
33112
+ for (const cw of colW) w += cw;
33113
+ let h = 0;
33114
+ for (const rh of rowH) h += rh;
33115
+ return { colW, rowH, w, h, boxes };
33116
+ }
33117
+ function anchorOrigin(position, totalW, totalH, args) {
33118
+ let y;
33119
+ if (position.startsWith("top")) y = MARGIN;
33120
+ else if (position.startsWith("bottom")) y = args.paneHeight - MARGIN - totalH;
33121
+ else y = args.paneHeight / 2 - totalH / 2;
33122
+ let x;
33123
+ if (position.endsWith("left")) x = MARGIN;
33124
+ else if (position.endsWith("right")) x = args.plotWidth - MARGIN - totalW;
33125
+ else x = args.plotWidth / 2 - totalW / 2;
33126
+ return { x, y };
33127
+ }
33128
+ function cellFont(cell, px, theme) {
33129
+ const family = cell.fontFamily === "monospace" ? "monospace" : theme.fontFamily || "sans-serif";
33130
+ return `${cell.italic ? "italic " : ""}${cell.bold ? "bold " : ""}${px}px ${family}`;
33131
+ }
33132
+
33133
+ // src/renderers/native/drawings/IndicatorDrawingSlices.ts
33134
+ function indicatorSliceKey(z, boundaries) {
33135
+ return boundaries.find((b) => b > z) ?? Infinity;
33136
+ }
33137
+ var IndicatorDrawingSlices = class {
33138
+ constructor() {
33139
+ this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
33140
+ /** Slice canvas cache, keyed `paneId|beforeZ` — same lifecycle as the user-drawing cache. */
33141
+ this.sliceCache = /* @__PURE__ */ new Map();
33142
+ /** Tooltip hit-rects of every label drawn this frame, in plot coords (rebuilt per prepare). */
33143
+ this.tips = [];
33144
+ }
33145
+ /**
33146
+ * Rebuild the per-indicator drawing slices for this data frame. `ref` is the data
33147
+ * canvas the slices must match pixel-for-pixel (the backend composites them 1:1).
33148
+ * Runs from the renderer's data paint, just before the backend composites the scene.
33149
+ */
33150
+ prepare(scene, coords, theme, ref2) {
33151
+ this.tips = [];
33152
+ const out = /* @__PURE__ */ new Map();
33153
+ if (ref2.width === 0 || ref2.height === 0) {
33154
+ this.sliceCache.clear();
33155
+ return out;
33156
+ }
33157
+ this.drawScene.setDeps({
33158
+ timeToLogical: (ms) => coords.timeToLogical(ms),
33159
+ barAt: (logical) => {
33160
+ const b = scene.bars[Math.round(logical)];
33161
+ return b ? { high: b.high, low: b.low } : null;
33162
+ },
33163
+ theme
33164
+ });
33165
+ const dpr = coords.dpr;
33166
+ const dataW = coords.width;
33167
+ const buckets = /* @__PURE__ */ new Map();
33168
+ const add = (paneId, beforeZ, entry) => {
33169
+ const key = `${paneId}|${beforeZ}`;
33170
+ const bucket = buckets.get(key);
33171
+ if (bucket) bucket.entries.push(entry);
33172
+ else buckets.set(key, { paneId, beforeZ, entries: [entry] });
33173
+ };
33174
+ for (const pane of scene.orderedPanes()) {
33175
+ if (pane.collapsed) continue;
33176
+ const boundaries = scene.seriesBoundaries(pane.id);
33177
+ for (const m of scene.orderedIndicatorsForPane(pane.id)) {
33178
+ const set = modelDrawingSet(m, false);
33179
+ const tables = (m.tables ?? []).filter((t) => !t.overlay);
33180
+ if (drawingSetEmpty(set) && tables.length === 0) continue;
33181
+ const sc = scene.scaleFor(m, pane);
33182
+ const mp = sc === pane.scale ? pane : { ...pane, scale: sc };
33183
+ const beforeZ = indicatorSliceKey(scene.zOf(m.id), boundaries);
33184
+ add(pane.id, beforeZ, { set, tables, pane: mp, indexOffset: scene.offsetOf(m.id) });
33185
+ }
33186
+ if (pane.kind === "price") {
33187
+ for (const m of scene.indicators.values()) {
33188
+ const set = modelDrawingSet(m, true);
33189
+ const tables = (m.tables ?? []).filter((t) => t.overlay === true);
33190
+ if (drawingSetEmpty(set) && tables.length === 0) continue;
33191
+ add(pane.id, Infinity, { set, tables, pane, indexOffset: scene.offsetOf(m.id) });
33192
+ }
33193
+ }
33194
+ }
33195
+ for (const [key, { paneId, beforeZ, entries }] of buckets) {
33196
+ let canvas = this.sliceCache.get(key);
33197
+ if (!canvas) {
33198
+ canvas = document.createElement("canvas");
33199
+ this.sliceCache.set(key, canvas);
33200
+ }
33201
+ if (canvas.width !== ref2.width || canvas.height !== ref2.height) {
33202
+ canvas.width = ref2.width;
33203
+ canvas.height = ref2.height;
33204
+ }
33205
+ const ctx = canvas.getContext("2d");
33206
+ if (!ctx) continue;
33207
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
33208
+ ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
33209
+ for (const e of entries) this.paintEntry(ctx, e, coords, dataW, theme);
33210
+ const slices = out.get(paneId) ?? [];
33211
+ slices.push({ beforeZ, canvas });
33212
+ out.set(paneId, slices);
33213
+ }
33214
+ for (const key of [...this.sliceCache.keys()]) if (!buckets.has(key)) this.sliceCache.delete(key);
33215
+ for (const slices of out.values()) slices.sort((a, b) => a.beforeZ - b.beforeZ);
33216
+ return out;
33217
+ }
33218
+ paintEntry(ctx, e, coords, dataW, theme) {
33219
+ const { pane } = e;
33220
+ const paneTips = [];
33221
+ ctx.save();
33222
+ ctx.translate(0, pane.bounds.top);
33223
+ ctx.beginPath();
33224
+ ctx.rect(0, 0, dataW, pane.bounds.height);
33225
+ ctx.clip();
33226
+ this.drawScene.setSet(e.set, e.indexOffset);
33227
+ this.drawScene.render(
33228
+ ctx,
33229
+ dataW,
33230
+ pane.bounds.height,
33231
+ (l) => coords.logicalToX(l),
33232
+ (price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
33233
+ );
33234
+ paneTips.push(...this.drawScene.labelTipRegions());
33235
+ for (const t of e.tables) paintTable(ctx, t, { paneHeight: pane.bounds.height, plotWidth: dataW, theme }, paneTips);
33236
+ ctx.restore();
33237
+ for (const r of paneTips) {
33238
+ this.tips.push({ ...r, top: r.top + pane.bounds.top, bottom: r.bottom + pane.bounds.top });
33239
+ }
33240
+ }
33241
+ /** Tooltip of the topmost label or table cell under a plot-space point, or null. Fed by the last prepare. */
33242
+ labelTooltipAt(x, y) {
33243
+ for (let i = this.tips.length - 1; i >= 0; i -= 1) {
33244
+ const r = this.tips[i];
33245
+ if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return r.text;
33246
+ }
33247
+ return null;
33248
+ }
33249
+ };
33250
+ function mergeSlices(indicator, user) {
33251
+ const out = /* @__PURE__ */ new Map();
33252
+ for (const [paneId, slices] of indicator) out.set(paneId, [...slices]);
33253
+ for (const [paneId, slices] of user) out.set(paneId, [...out.get(paneId) ?? [], ...slices]);
33254
+ for (const slices of out.values()) slices.sort((a, b) => a.beforeZ - b.beforeZ);
33255
+ return out;
33256
+ }
33257
+
32474
33258
  // src/renderers/native/drawings/Projector.ts
32475
33259
  function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
32476
33260
  return {
@@ -32522,19 +33306,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
32522
33306
  for (const model of models) {
32523
33307
  const off = offsetOf(model.id);
32524
33308
  for (const s of model.series) {
32525
- if (s.kind === "candle" || s.kind === "bar") {
32526
- for (let i = i0; i <= i1; i += 1) {
32527
- const b = s.bars[i - off];
32528
- if (b) {
32529
- consider(b.high);
32530
- consider(b.low);
32531
- }
32532
- }
32533
- } else if (isLineLikeSeries(s)) {
32534
- for (let i = i0; i <= i1; i += 1) consider(s.points[i - off]?.value);
32535
- if (s.kind === "histogram" || s.kind === "columns") consider(s.style?.base ?? 0);
32536
- else if (s.style?.base != null) consider(s.style.base);
32537
- }
33309
+ if (s.overlay === true) continue;
33310
+ considerSeries(s, i0, i1, off, consider);
32538
33311
  }
32539
33312
  for (const pl of model.priceLines) consider(pl.price);
32540
33313
  }
@@ -32556,6 +33329,36 @@ ${overlayScrollbarCss(".vela-sd-pane")}
32556
33329
  const span = max2 - min2;
32557
33330
  return { min: min2 - span * MARGIN_BOTTOM, max: max2 + span * MARGIN_TOP };
32558
33331
  }
33332
+ function considerSeries(s, i0, i1, off, consider) {
33333
+ if (s.kind === "candle" || s.kind === "bar") {
33334
+ for (let i = i0; i <= i1; i += 1) {
33335
+ const b = s.bars[i - off];
33336
+ if (b) {
33337
+ consider(b.high);
33338
+ consider(b.low);
33339
+ }
33340
+ }
33341
+ } else if (isLineLikeSeries(s)) {
33342
+ for (let i = i0; i <= i1; i += 1) consider(s.points[i - off]?.value);
33343
+ if (s.kind === "histogram" || s.kind === "columns") consider(s.style?.base ?? 0);
33344
+ else if (s.style?.base != null) consider(s.style.base);
33345
+ }
33346
+ }
33347
+ function overlaySeriesRange(models, i0, i1, offsetOf = () => 0) {
33348
+ let min2 = Infinity;
33349
+ let max2 = -Infinity;
33350
+ const consider = (v) => {
33351
+ if (v != null && Number.isFinite(v)) {
33352
+ if (v < min2) min2 = v;
33353
+ if (v > max2) max2 = v;
33354
+ }
33355
+ };
33356
+ for (const model of models) {
33357
+ const off = offsetOf(model.id);
33358
+ for (const s of model.series) if (s.overlay === true) considerSeries(s, i0, i1, off, consider);
33359
+ }
33360
+ return min2 === Infinity ? null : { min: min2, max: max2 };
33361
+ }
32559
33362
  function expandScaleByPixels(scale, heightPx, abovePx, belowPx) {
32560
33363
  if (abovePx <= 0 && belowPx <= 0) return scale;
32561
33364
  const content = heightPx - abovePx - belowPx;
@@ -33331,6 +34134,11 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33331
34134
  this.vpvrRenderer = new VpvrRenderer();
33332
34135
  this.resizeObserver = null;
33333
34136
  this.dprMedia = null;
34137
+ /** Plot size in INTEGER device px, as last reported by the resize observer's
34138
+ * device-pixel-content-box — the browser's own statement of how many device pixels
34139
+ * it paints the plot into. `null` until the first report or where the box type is
34140
+ * unsupported (WebKit); syncSize then falls back to rounding the client rect. */
34141
+ this.plotDeviceSize = null;
33334
34142
  this.coords = new CoordinateSystem();
33335
34143
  this.scene = new SceneGraph();
33336
34144
  // chosen at mount (WebGL2 if available, else canvas2d)
@@ -33338,6 +34146,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33338
34146
  this.glowAmount = 0;
33339
34147
  // WebGL2 neon-glow intensity (canvas2d ignores it)
33340
34148
  this.chrome = new ChromeRenderer();
34149
+ /** Prepaints each indicator's Pine drawings into interleave slices at the model's z. */
34150
+ this.indicatorSlices = new IndicatorDrawingSlices();
34151
+ /** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
34152
+ this.labelTooltip = null;
33341
34153
  this.crosshairLayer = new CrosshairRenderer();
33342
34154
  /** 1 Hz repaint pump so the price-axis countdown-to-bar-close ticks; null when off. */
33343
34155
  this.countdownTimer = null;
@@ -33348,6 +34160,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33348
34160
  this.indicatorValuesOn = true;
33349
34161
  /** Host-contributed legend actions — held here so a rebuild of the legend re-wires them. */
33350
34162
  this.legendActionsProvider = null;
34163
+ /** Host-contributed legend callouts — held here so a rebuild of the legend re-wires them. */
34164
+ this.legendCalloutsProvider = null;
33351
34165
  /** Host override of the legend's fold toggle — held here so a remount re-applies it. */
33352
34166
  this.legendOverviewAction = null;
33353
34167
  // ── keyboard navigation / accessibility (item 11) ──
@@ -33482,7 +34296,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33482
34296
  this.toggleVisibleCbs = /* @__PURE__ */ new Set();
33483
34297
  this.moveIndicatorCbs = /* @__PURE__ */ new Set();
33484
34298
  this.priceStyleCbs = /* @__PURE__ */ new Set();
33485
- this.tableOverlays = /* @__PURE__ */ new Map();
33486
34299
  this.name = "native";
33487
34300
  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"];
33488
34301
  /** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
@@ -33796,7 +34609,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33796
34609
  * hidden) on clear so a re-show picks up the current theme.
33797
34610
  */
33798
34611
  setLoading(loading) {
33799
- for (const overlay of this.tableOverlays.values()) overlay.setVisible(!loading);
33800
34612
  if (!loading || !this.wrapper) {
33801
34613
  this.loadingEl?.remove();
33802
34614
  this.loadingEl = null;
@@ -34456,6 +35268,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34456
35268
  this.plot.appendChild(this.scrollButton);
34457
35269
  this.plot.addEventListener("pointermove", this.onScrollProximityMove);
34458
35270
  this.plot.addEventListener("pointerleave", this.onScrollProximityLeave);
35271
+ this.labelTooltip = new LabelTooltip(this.plot, {
35272
+ theme: () => this.chromeTheme(),
35273
+ lookup: (x, y) => this.indicatorSlices.labelTooltipAt(x, y)
35274
+ });
34459
35275
  this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
34460
35276
  projector: () => this.drawingProjector(),
34461
35277
  dpr: () => this.coords.dpr,
@@ -34495,9 +35311,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34495
35311
  this.inputsUI.setDialogHost(this.dialogHost);
34496
35312
  this.inputsUI.setSymbolPicker(this.symbolPicker);
34497
35313
  this.inputsUI.setLegendActions(this.legendActionsProvider);
35314
+ this.inputsUI.setLegendCallouts(this.legendCalloutsProvider);
34498
35315
  this.inputsUI.setLegendOverviewAction(this.legendOverviewAction);
34499
35316
  this.inputsUI.setOnChange((c) => {
34500
- for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value });
35317
+ for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value, ...c.kind ? { kind: c.kind } : {} });
34501
35318
  });
34502
35319
  this.inputsUI.setOnRemove((id) => {
34503
35320
  for (const cb of this.removeIndicatorCbs) cb(id);
@@ -34538,6 +35355,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34538
35355
  this.emitPaneAction({ type: "maximize", paneId, maximized });
34539
35356
  }
34540
35357
  });
35358
+ this.paneControls.setSuspended(this.layoutMode === "mobile");
34541
35359
  this.axisScaleButtons = new AxisScaleButtons(this.plot, theme, {
34542
35360
  panes: () => this.axisScaleViews(),
34543
35361
  rightAxis: () => this.rightAxisW,
@@ -34547,8 +35365,19 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34547
35365
  if (pane) this.setPaneLog(paneId, !paneLogScale(this.scene, pane));
34548
35366
  }
34549
35367
  });
34550
- this.resizeObserver = new ResizeObserver(() => this.resize());
35368
+ this.resizeObserver = new ResizeObserver((entries) => {
35369
+ for (const e of entries) {
35370
+ if (e.target !== this.plot) continue;
35371
+ const s = e.devicePixelContentBoxSize?.[0];
35372
+ if (s) this.plotDeviceSize = { width: s.inlineSize, height: s.blockSize };
35373
+ }
35374
+ this.resize();
35375
+ });
34551
35376
  this.resizeObserver.observe(this.wrapper);
35377
+ try {
35378
+ this.resizeObserver.observe(this.plot, { box: "device-pixel-content-box" });
35379
+ } catch {
35380
+ }
34552
35381
  this.watchDpr();
34553
35382
  this.syncSize();
34554
35383
  }
@@ -34673,8 +35502,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34673
35502
  this.inputsUI?.destroy();
34674
35503
  this.paneControls?.destroy();
34675
35504
  this.axisScaleButtons?.destroy();
34676
- for (const overlay of this.tableOverlays.values()) overlay.destroy();
34677
- this.tableOverlays.clear();
34678
35505
  this.resizeObserver?.disconnect();
34679
35506
  this.resizeObserver = null;
34680
35507
  this.dprMedia?.removeEventListener("change", this.onDprChange);
@@ -34687,6 +35514,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34687
35514
  this.settingsButton = null;
34688
35515
  this.plot?.removeEventListener("pointermove", this.onScrollProximityMove);
34689
35516
  this.plot?.removeEventListener("pointerleave", this.onScrollProximityLeave);
35517
+ this.labelTooltip?.destroy();
35518
+ this.labelTooltip = null;
34690
35519
  this.scrollButton?.remove();
34691
35520
  this.scrollButton = null;
34692
35521
  for (const l of this.extLayers) l.instance.destroy?.();
@@ -34701,6 +35530,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34701
35530
  this.attributionEl = null;
34702
35531
  this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
34703
35532
  this.mountContainer?.style.removeProperty("--vela-scale-gutter");
35533
+ this.mountContainer?.style.removeProperty("--vela-bottom-gutter");
34704
35534
  this.mountContainer?.style.removeProperty("--vela-price-pane-top");
34705
35535
  this.mountContainer?.style.removeProperty("--vela-price-pane-bottom");
34706
35536
  this.mountContainer = null;
@@ -34793,7 +35623,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34793
35623
  ensurePane(pane) {
34794
35624
  this.scene.ensurePane(pane.id, pane.kind, pane.order, pane.heightWeight ?? (pane.kind === "price" ? 3 : 1));
34795
35625
  this.layoutPanes();
34796
- this.repositionTables();
34797
35626
  this.paneControls?.refresh();
34798
35627
  this.scheduler.invalidate(4 /* Full */);
34799
35628
  }
@@ -34815,17 +35644,14 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34815
35644
  if (!model.ownScale) this.scene.dropIndicatorScale(handle.id);
34816
35645
  this.inputsUI.setPane(handle.id, paneId);
34817
35646
  this.refreshAnchorOffset(model);
34818
- this.syncTables(model);
34819
35647
  this.refreshAxisWidth();
34820
35648
  this.layoutPanes();
34821
- this.repositionTables();
34822
35649
  this.paneControls?.refresh();
34823
35650
  this.scheduler.invalidate(4 /* Full */);
34824
35651
  }
34825
35652
  orderPanes(orderedIds) {
34826
35653
  this.scene.orderPanes(orderedIds);
34827
35654
  this.layoutPanes();
34828
- this.repositionTables();
34829
35655
  this.paneControls?.refresh();
34830
35656
  this.scheduler.invalidate(4 /* Full */);
34831
35657
  }
@@ -34834,7 +35660,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34834
35660
  if (!pane || pane.collapsed === collapsed) return;
34835
35661
  pane.collapsed = collapsed;
34836
35662
  this.layoutPanes();
34837
- this.repositionTables();
34838
35663
  this.paneControls?.refresh();
34839
35664
  this.scheduler.invalidate(4 /* Full */);
34840
35665
  }
@@ -34842,7 +35667,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34842
35667
  if (paneId !== null && !this.scene.panes.has(paneId)) paneId = null;
34843
35668
  this.maximizedPaneId = paneId;
34844
35669
  this.layoutPanes();
34845
- this.repositionTables();
34846
35670
  this.paneControls?.refresh();
34847
35671
  this.scheduler.invalidate(4 /* Full */);
34848
35672
  }
@@ -34927,9 +35751,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34927
35751
  else this.scene.assignIndicatorZ(model.id);
34928
35752
  this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
34929
35753
  native: !!model.native,
34930
- ...model.shorttitle ? { settingsTitle: model.title } : {}
35754
+ ...model.props ? { props: model.props, propValues: model.propValues ?? {} } : {}
34931
35755
  });
34932
- this.syncTables(model);
34933
35756
  if (model.native?.type === "volume") {
34934
35757
  this.volumeActive = true;
34935
35758
  this.volumeHidden = false;
@@ -34953,7 +35776,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34953
35776
  }
34954
35777
  }
34955
35778
  applyPatch(model, patch);
34956
- this.syncTables(model);
34957
35779
  this.scheduler.invalidate(3 /* Light */);
34958
35780
  }
34959
35781
  removeIndicator(handle) {
@@ -34972,14 +35794,12 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34972
35794
  this.scene.forgetAnchorOffset(handle.id);
34973
35795
  this.scene.dropIndicatorScale(handle.id);
34974
35796
  this.inputsUI.remove(handle.id);
34975
- this.tableOverlays.get(handle.id)?.destroy();
34976
- this.tableOverlays.delete(handle.id);
34977
35797
  this.refreshAxisWidth();
34978
35798
  this.paneControls?.refresh();
34979
35799
  this.scheduler.invalidate(4 /* Full */);
34980
35800
  }
34981
- setIndicatorInputs(handle, values) {
34982
- this.inputsUI.setValues(handle.id, values);
35801
+ setIndicatorInputs(handle, values, props) {
35802
+ this.inputsUI.setValues(handle.id, values, props);
34983
35803
  }
34984
35804
  setSymbolPicker(picker) {
34985
35805
  this.symbolPicker = picker;
@@ -34989,6 +35809,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34989
35809
  this.legendActionsProvider = provider;
34990
35810
  this.inputsUI?.setLegendActions(provider);
34991
35811
  }
35812
+ setLegendCallouts(provider) {
35813
+ this.legendCalloutsProvider = provider;
35814
+ this.inputsUI?.setLegendCallouts(provider);
35815
+ }
34992
35816
  setLegendOverviewAction(action) {
34993
35817
  this.legendOverviewAction = action;
34994
35818
  this.inputsUI?.setLegendOverviewAction(action);
@@ -35013,8 +35837,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35013
35837
  }
35014
35838
  if (!visible) {
35015
35839
  this.scene.indicators.delete(handle.id);
35016
- this.tableOverlays.get(handle.id)?.destroy();
35017
- this.tableOverlays.delete(handle.id);
35018
35840
  }
35019
35841
  this.inputsUI.setVisible(handle.id, visible);
35020
35842
  this.scheduler.invalidate(4 /* Full */);
@@ -35060,6 +35882,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35060
35882
  this.userDrawings?.setLayoutMode(mode);
35061
35883
  this.settingsDialog?.setLayoutMode(mode);
35062
35884
  this.inputsUI?.setLayoutMode(mode);
35885
+ this.paneControls?.setSuspended(mode === "mobile");
35063
35886
  if (this.scrollButton) {
35064
35887
  const px = mode === "mobile" ? SCROLL_BTN_SIZE_TOUCH : SCROLL_BTN_SIZE;
35065
35888
  this.scrollButton.style.width = `${px}px`;
@@ -35455,7 +36278,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35455
36278
  /** Relayout + repaint + refresh the hover buttons after a collapse/maximize/order change. */
35456
36279
  afterPaneLayoutChange() {
35457
36280
  this.layoutPanes();
35458
- this.repositionTables();
35459
36281
  this.paneControls?.refresh();
35460
36282
  this.scheduler.invalidate(4 /* Full */);
35461
36283
  }
@@ -35536,7 +36358,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35536
36358
  above.heightWeight = next2.above;
35537
36359
  below.heightWeight = next2.below;
35538
36360
  this.layoutPanes();
35539
- this.repositionTables();
35540
36361
  this.scheduler.invalidate(4 /* Full */);
35541
36362
  }
35542
36363
  /** Double-click a separator → split the two adjacent panes evenly (each gets half of
@@ -35551,7 +36372,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35551
36372
  above.heightWeight = half;
35552
36373
  below.heightWeight = half;
35553
36374
  this.layoutPanes();
35554
- this.repositionTables();
35555
36375
  this.scheduler.invalidate(4 /* Full */);
35556
36376
  }
35557
36377
  // ── keyboard navigation / accessibility (item 11) ──
@@ -35830,7 +36650,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35830
36650
  const liveActual = li >= 0 ? this.bars[li] : void 0;
35831
36651
  const easeLive = !!liveActual && this.liveEaseTime === liveActual.time && (liveActual.high !== this.liveEaseHigh || liveActual.low !== this.liveEaseLow || liveActual.close !== this.liveEaseClose);
35832
36652
  if (easeLive && liveActual) this.bars[li] = { ...liveActual, high: this.liveEaseHigh, low: this.liveEaseLow, close: this.liveEaseClose };
35833
- this.scene.drawingSlices = this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map();
36653
+ this.scene.drawingSlices = mergeSlices(
36654
+ this.indicatorSlices.prepare(this.scene, this.coords, this.theme, this.dataCanvas),
36655
+ this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map()
36656
+ );
35834
36657
  this.backdropRenderer.render(this.scene, this.coords, this.theme, gridAlpha);
35835
36658
  this.backend.render(this.scene, this.coords, this.theme);
35836
36659
  this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
@@ -35969,7 +36792,11 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35969
36792
  }
35970
36793
  const models = this.scene.indicatorsForPane(pane.id);
35971
36794
  const masterModels = models.filter((m) => m.ownScale !== true);
35972
- const dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
36795
+ let dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
36796
+ if (pane === pricePane) {
36797
+ const or2 = overlaySeriesRange(this.scene.indicators.values(), i0, i1, (id) => this.scene.offsetOf(id));
36798
+ if (or2) dr = dr ? { min: Math.min(dr.min, or2.min), max: Math.max(dr.max, or2.max) } : or2;
36799
+ }
35973
36800
  const includeCandles = pane.kind === "price" && !this.scene.candlesHidden;
35974
36801
  pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
35975
36802
  pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
@@ -36243,6 +37070,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36243
37070
  for (const m of models) {
36244
37071
  const off = this.scene.offsetOf(m.id);
36245
37072
  for (const s of m.series) {
37073
+ if (s.overlay === true) continue;
36246
37074
  if (isLineLikeSeries(s)) {
36247
37075
  const v = s.points[i0 - off]?.value;
36248
37076
  if (v != null && Number.isFinite(v)) return v;
@@ -36263,27 +37091,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36263
37091
  }
36264
37092
  return maxVol;
36265
37093
  }
36266
- /** Create/update/destroy an indicator's DOM table overlay (anchored off real pane geometry). */
36267
- syncTables(model) {
36268
- const tables = model.tables ?? [];
36269
- let overlay = this.tableOverlays.get(model.id);
36270
- if (tables.length === 0) {
36271
- if (overlay) {
36272
- overlay.destroy();
36273
- this.tableOverlays.delete(model.id);
36274
- }
36275
- return;
36276
- }
36277
- if (!overlay) {
36278
- overlay = new TableOverlay(this.plot, this.theme, (id) => this.paneBoundsFor(id));
36279
- overlay.setVisible(this.loadingEl === null);
36280
- this.tableOverlays.set(model.id, overlay);
36281
- }
36282
- overlay.update(tables);
36283
- }
36284
- repositionTables() {
36285
- for (const overlay of this.tableOverlays.values()) overlay.reposition();
36286
- }
36287
37094
  layoutPanes() {
36288
37095
  const panes = this.scene.orderedPanes();
36289
37096
  const dataHeight = this.coords.height;
@@ -36334,6 +37141,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36334
37141
  const visible = maxPane ? [maxPane] : this.scene.orderedPanes().filter((p) => !p.collapsed);
36335
37142
  const paneBottom = visible.length ? Math.max(...visible.map((p) => p.bounds.top + p.bounds.height)) : dataHeight;
36336
37143
  this.scrollBtnBottomPx = SCROLL_BTN_BOTTOM + Math.max(0, dataHeight - paneBottom);
37144
+ this.mountContainer?.style.setProperty("--vela-bottom-gutter", `${TIME_AXIS_H + Math.max(0, dataHeight - paneBottom)}px`);
36337
37145
  this.scrollBtnRightPx = this.rightAxisW + SCROLL_BTN_RIGHT_INSET;
36338
37146
  if (this.scrollButton) {
36339
37147
  this.scrollButton.style.bottom = `${this.scrollBtnBottomPx}px`;
@@ -36426,30 +37234,33 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36426
37234
  if (w <= 0 || h <= 0) return;
36427
37235
  const dpr = window.devicePixelRatio || 1;
36428
37236
  this.plot.style.left = `${this.toolbarGutter}px`;
36429
- const pw = Math.max(1, w - this.toolbarGutter);
36430
- const ph = h;
36431
- this.dataCanvas.width = Math.round(pw * dpr);
36432
- this.dataCanvas.height = Math.round(ph * dpr);
36433
- this.backdropCanvas.width = this.dataCanvas.width;
36434
- this.backdropCanvas.height = this.dataCanvas.height;
36435
- this.volumeCanvas.width = this.dataCanvas.width;
36436
- this.volumeCanvas.height = this.dataCanvas.height;
36437
- for (const l of this.extLayers) {
36438
- l.canvas.width = this.dataCanvas.width;
36439
- l.canvas.height = this.dataCanvas.height;
36440
- }
36441
- this.vpvrCanvas.width = this.dataCanvas.width;
36442
- this.vpvrCanvas.height = this.dataCanvas.height;
36443
- this.chromeCanvas.width = this.dataCanvas.width;
36444
- this.chromeCanvas.height = this.dataCanvas.height;
36445
- this.drawingsCanvas.width = this.dataCanvas.width;
36446
- this.drawingsCanvas.height = this.dataCanvas.height;
36447
- this.cursorCanvas.width = this.dataCanvas.width;
36448
- this.cursorCanvas.height = this.dataCanvas.height;
37237
+ const rect = this.plot.getBoundingClientRect();
37238
+ let bw = Math.max(1, Math.round(rect.width * dpr));
37239
+ let bh = Math.max(1, Math.round(rect.height * dpr));
37240
+ const dev = this.plotDeviceSize;
37241
+ if (dev && Math.abs(dev.width - rect.width * dpr) <= 1 && Math.abs(dev.height - rect.height * dpr) <= 1) {
37242
+ bw = Math.max(1, dev.width);
37243
+ bh = Math.max(1, dev.height);
37244
+ }
37245
+ const pw = bw / dpr;
37246
+ const ph = bh / dpr;
37247
+ const size3 = (canvas) => {
37248
+ canvas.width = bw;
37249
+ canvas.height = bh;
37250
+ canvas.style.width = `${pw}px`;
37251
+ canvas.style.height = `${ph}px`;
37252
+ };
37253
+ size3(this.dataCanvas);
37254
+ size3(this.backdropCanvas);
37255
+ size3(this.volumeCanvas);
37256
+ for (const l of this.extLayers) size3(l.canvas);
37257
+ size3(this.vpvrCanvas);
37258
+ size3(this.chromeCanvas);
37259
+ size3(this.drawingsCanvas);
37260
+ size3(this.cursorCanvas);
36449
37261
  this.coords.setSize(Math.max(1, pw - this.rightAxisW), Math.max(1, ph - TIME_AXIS_H), dpr);
36450
37262
  this.scene.crosshair = null;
36451
37263
  this.layoutPanes();
36452
- this.repositionTables();
36453
37264
  this.userDrawings?.onResize();
36454
37265
  if (!this.didInitialFit && this.coords.barCount > 0) {
36455
37266
  this.fitContent();
@@ -37148,6 +37959,19 @@ ${overlayScrollbarCss(".vela-sd-pane")}
37148
37959
  function legendActions() {
37149
37960
  return [...legendRegistry.values()].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
37150
37961
  }
37962
+ var calloutRegistry = /* @__PURE__ */ new Map();
37963
+ function registerLegendCallout(desc) {
37964
+ calloutRegistry.set(desc.id, desc);
37965
+ return () => {
37966
+ if (calloutRegistry.get(desc.id) === desc) calloutRegistry.delete(desc.id);
37967
+ };
37968
+ }
37969
+ function unregisterLegendCallout(id) {
37970
+ calloutRegistry.delete(id);
37971
+ }
37972
+ function legendCallouts() {
37973
+ return [...calloutRegistry.values()].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
37974
+ }
37151
37975
  var stateHandlers = /* @__PURE__ */ new Map();
37152
37976
  function registerStatePersistence(handler) {
37153
37977
  stateHandlers.set(handler.key, handler);
@@ -38491,6 +39315,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
38491
39315
  exports.iconMarkup = iconMarkup;
38492
39316
  exports.inputVisible = inputVisible;
38493
39317
  exports.legendActions = legendActions;
39318
+ exports.legendCallouts = legendCallouts;
38494
39319
  exports.nativeIndicatorDescriptors = nativeIndicatorDescriptors;
38495
39320
  exports.nativeIndicatorTypes = nativeIndicatorTypes;
38496
39321
  exports.normalizeSettingsRow = normalizeSettingsRow;
@@ -38499,6 +39324,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
38499
39324
  exports.registerDrawingType = registerDrawingType;
38500
39325
  exports.registerIcon = registerIcon;
38501
39326
  exports.registerLegendAction = registerLegendAction;
39327
+ exports.registerLegendCallout = registerLegendCallout;
38502
39328
  exports.registerNativeIndicator = registerNativeIndicator;
38503
39329
  exports.registerRendererDefaults = registerRendererDefaults;
38504
39330
  exports.registerRendererLayer = registerRendererLayer;
@@ -38524,6 +39350,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
38524
39350
  exports.unregisterChartType = unregisterChartType;
38525
39351
  exports.unregisterDefaultEngine = unregisterDefaultEngine;
38526
39352
  exports.unregisterLegendAction = unregisterLegendAction;
39353
+ exports.unregisterLegendCallout = unregisterLegendCallout;
38527
39354
  exports.unregisterNativeIndicator = unregisterNativeIndicator;
38528
39355
  exports.unregisterRendererDefaults = unregisterRendererDefaults;
38529
39356
  exports.unregisterRendererLayer = unregisterRendererLayer;