@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
package/dist/index.cjs CHANGED
@@ -138,6 +138,7 @@ var IndicatorHandleImpl = class {
138
138
  constructor(id, title, controller, source) {
139
139
  this.controller = controller;
140
140
  this.schema = [];
141
+ this.propsSchema = [];
141
142
  this.visibleState = true;
142
143
  this.bus = new TypedEventBus();
143
144
  this.id = id;
@@ -147,6 +148,9 @@ var IndicatorHandleImpl = class {
147
148
  get inputs() {
148
149
  return this.schema;
149
150
  }
151
+ get props() {
152
+ return this.propsSchema;
153
+ }
150
154
  get visible() {
151
155
  return this.visibleState;
152
156
  }
@@ -156,6 +160,12 @@ var IndicatorHandleImpl = class {
156
160
  setInputs(values) {
157
161
  this.controller.applyInputs(this.id, values);
158
162
  }
163
+ setProp(key, value) {
164
+ this.controller.applyProps(this.id, { [key]: value });
165
+ }
166
+ setProps(values) {
167
+ this.controller.applyProps(this.id, values);
168
+ }
159
169
  setVisible(visible) {
160
170
  this.controller.setVisible(this.id, visible);
161
171
  }
@@ -175,6 +185,9 @@ var IndicatorHandleImpl = class {
175
185
  setSchema(schema) {
176
186
  this.schema = schema;
177
187
  }
188
+ setPropsSchema(schema) {
189
+ this.propsSchema = schema;
190
+ }
178
191
  /** Sync the public `visible` getter when the orchestrator changes visibility (API or legend eye). */
179
192
  setVisibleState(visible) {
180
193
  this.visibleState = visible;
@@ -188,6 +201,8 @@ var IndicatorHandleImpl = class {
188
201
  function summarizeModel(model) {
189
202
  const series = {};
190
203
  for (const s of model.series) series[s.kind] = (series[s.kind] ?? 0) + 1;
204
+ const countOverlay = (items) => items?.filter((i) => i.overlay === true).length ?? 0;
205
+ 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);
191
206
  return {
192
207
  id: model.id,
193
208
  title: model.title,
@@ -207,7 +222,8 @@ function summarizeModel(model) {
207
222
  tables: model.tables?.length ?? 0,
208
223
  barColors: model.barColors?.length ?? 0,
209
224
  trades: model.trades?.length ?? 0,
210
- inputs: model.inputs.length
225
+ inputs: model.inputs.length,
226
+ forcedOverlay
211
227
  };
212
228
  }
213
229
  function inspectModels(models) {
@@ -228,7 +244,8 @@ function inspectModels(models) {
228
244
  linefills: sum((s) => s.linefills),
229
245
  tables: sum((s) => s.tables),
230
246
  barColors: sum((s) => s.barColors),
231
- trades: sum((s) => s.trades)
247
+ trades: sum((s) => s.trades),
248
+ forcedOverlay: sum((s) => s.forcedOverlay)
232
249
  }
233
250
  };
234
251
  }
@@ -498,11 +515,11 @@ registerIcon(
498
515
  );
499
516
  registerIcon(
500
517
  "market-pre",
501
- 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"/>')
518
+ 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"/>')
502
519
  );
503
520
  registerIcon(
504
521
  "market-post",
505
- 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"/>')
522
+ 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"/>')
506
523
  );
507
524
  registerIcon("market-closed", svg24('<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>'));
508
525
  registerIcon(
@@ -535,6 +552,8 @@ registerIcon("folder-plus", S('<path d="M1.6 4.4a1 1 0 0 1 1-1h2.7l1.3 1.7h6.8a1
535
552
  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"/>'));
536
553
  registerIcon("collapse", S('<path d="M3 8h10"/>'));
537
554
  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"/>'));
555
+ registerIcon("plus", S('<path d="M8 2.8v10.4M2.8 8h10.4"/>'));
556
+ registerIcon("minus", S('<path d="M2.8 8h10.4"/>'));
538
557
  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"/>'));
539
558
  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"/>'));
540
559
  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"/>'));
@@ -542,7 +561,7 @@ registerIcon("star-filled", S('<path d="M8 2.2l1.75 3.55 3.9.55-2.8 2.75.65 3.9L
542
561
  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"'));
543
562
  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"'));
544
563
  registerIcon("burger", S('<path d="M2.5 4.5h11M2.5 8h11M2.5 11.5h11"/>'));
545
- 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"/>'));
564
+ 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"/>'));
546
565
  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"/>'));
547
566
  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"/>'));
548
567
  registerIcon("cursor", svg24('<path d="M5 3l6 16 2-6 6-2z"/>', 'fill="currentColor" stroke="none"'));
@@ -6913,6 +6932,23 @@ function timeframeToMs(timeframe) {
6913
6932
  return 36e5;
6914
6933
  }
6915
6934
 
6935
+ // src/data/symbol-groups.ts
6936
+ function isGroupRow(d) {
6937
+ return d.group != null && d.ticker === d.group;
6938
+ }
6939
+ function groupKeyOf(d) {
6940
+ return `${(d.prefix ?? d.provider ?? "").toLowerCase()}:${(d.group ?? "").toUpperCase()}`;
6941
+ }
6942
+ function groupMembers(pool, groupRow) {
6943
+ const key = groupKeyOf(groupRow);
6944
+ return pool.filter((s) => s.group != null && !isGroupRow(s) && groupKeyOf(s) === key);
6945
+ }
6946
+ function defaultMemberOf(pool, groupRow) {
6947
+ const members = groupMembers(pool, groupRow);
6948
+ const defaults2 = members.filter((m) => m.default);
6949
+ return (defaults2.length === 1 ? defaults2[0] : members[0]) ?? null;
6950
+ }
6951
+
6916
6952
  // src/data/ProviderRegistry.ts
6917
6953
  var normName = (n) => n.trim().toLowerCase();
6918
6954
  var normTicker = (t) => t.trim().toUpperCase();
@@ -7002,17 +7038,24 @@ var ProviderRegistry = class {
7002
7038
  resolve(raw, opts = {}) {
7003
7039
  const { provider, ticker } = this.parse(raw);
7004
7040
  if (provider) {
7005
- if (this.entries.has(provider)) return { provider, ticker };
7041
+ if (this.entries.has(provider)) {
7042
+ const d = this.entries.get(provider).byTicker?.get(normTicker(ticker));
7043
+ return { provider, ticker: d != null && isGroupRow(d) ? this.loadableTicker(provider, d) : ticker };
7044
+ }
7006
7045
  const key = `${provider}:${normTicker(ticker)}`;
7007
7046
  for (const name of this.candidateOrder(opts.default)) {
7008
7047
  const d = this.entries.get(name).prefixIndex?.get(key);
7009
- if (d) return { provider: name, ticker: d.ticker };
7048
+ if (d) return { provider: name, ticker: this.loadableTicker(name, d) };
7010
7049
  }
7011
7050
  return null;
7012
7051
  }
7013
7052
  const norm = normTicker(ticker);
7014
7053
  for (const name of this.candidateOrder(opts.default)) {
7015
- if (this.entries.get(name).index?.has(norm)) return { provider: name, ticker };
7054
+ const e = this.entries.get(name);
7055
+ if (e.index?.has(norm)) {
7056
+ const d = e.byTicker?.get(norm);
7057
+ return { provider: name, ticker: d != null && isGroupRow(d) ? this.loadableTicker(name, d) : ticker };
7058
+ }
7016
7059
  }
7017
7060
  if (opts.lenient && this.entries.size === 1) {
7018
7061
  const only = this.names()[0];
@@ -7021,6 +7064,13 @@ var ProviderRegistry = class {
7021
7064
  }
7022
7065
  return null;
7023
7066
  }
7067
+ /** What a resolved descriptor LOADS: itself — unless it is a GROUP row (listed, never
7068
+ * served), which loads its default member (single `default`, else first listed). A
7069
+ * memberless group keeps its own ticker and fails downstream like any unknown symbol. */
7070
+ loadableTicker(name, d) {
7071
+ if (!isGroupRow(d)) return d.ticker;
7072
+ return defaultMemberOf(this.entries.get(name)?.descriptors ?? [], d)?.ticker ?? d.ticker;
7073
+ }
7024
7074
  /**
7025
7075
  * Resolve now if possible, else resolve later — re-attempting after each provider
7026
7076
  * settles. The promise stays pending until some registered provider can serve the
@@ -7309,6 +7359,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
7309
7359
  /** Invalidates detached async work (backfill loops, in-flight loads, gap heals):
7310
7360
  * bumped by init(), setMarket() and destroy(). */
7311
7361
  this.generation = 0;
7362
+ /** Aborts the in-flight PROGRESSIVE load's source polling on supersession — an
7363
+ * abandoned stream left polling to its own budget starves the browser's per-host
7364
+ * connection pool, and the NEXT symbol's very first fetch with it (measured). */
7365
+ this.progressiveAbort = null;
7312
7366
  /** Awaiters racing a superseded load (setMarket callers) — released on every bump so they never hang. */
7313
7367
  this.supersedeWaiters = [];
7314
7368
  /** `history:complete` fired for the CURRENT load. Each market load re-arms the cycle
@@ -7342,7 +7396,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
7342
7396
  for (const engine of engines) this.registerEngine(engine.language, engine);
7343
7397
  this.defaultLanguage = config.defaultLanguage;
7344
7398
  this.renderer.mount(container, config.theme);
7345
- this.renderer.onInputChange((e) => this.applyInputs(e.indicatorId, { [e.key]: e.value }));
7399
+ this.renderer.onInputChange(
7400
+ (e) => e.kind === "prop" ? this.applyProps(e.indicatorId, { [e.key]: e.value }) : this.applyInputs(e.indicatorId, { [e.key]: e.value })
7401
+ );
7346
7402
  this.renderer.onRemoveIndicator((id) => this.removeIndicator(id));
7347
7403
  this.renderer.onToggleIndicatorVisible?.((id, visible) => this.setVisible(id, visible));
7348
7404
  this.paneActionUnsub = this.renderer.onPaneAction?.((a) => this.handlePaneAction(a)) ?? null;
@@ -7449,6 +7505,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
7449
7505
  * superseded setMarket awaiters so their promises resolve instead of hanging. */
7450
7506
  bumpGeneration() {
7451
7507
  const gen = ++this.generation;
7508
+ this.progressiveAbort?.abort();
7509
+ this.progressiveAbort = null;
7452
7510
  for (const w of this.supersedeWaiters.splice(0)) w();
7453
7511
  return gen;
7454
7512
  }
@@ -7502,7 +7560,47 @@ var EngineOrchestrator = class _EngineOrchestrator {
7502
7560
  const requested = market.bars ?? 500;
7503
7561
  const initialRange = market.visibleRange;
7504
7562
  const deep = !market.data?.length && initialRange == null && requested > SINGLE_LOAD_BARS;
7505
- if (deep && this.feed.loadRange) {
7563
+ let progressiveServed = false;
7564
+ if (!market.data?.length && initialRange == null && this.feed.loadProgressive) {
7565
+ let painted = false;
7566
+ const paint = (bars, final) => {
7567
+ if (this.generation !== gen || !final && bars.length === 0) return;
7568
+ this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
7569
+ if (!painted && bars.length > 0) {
7570
+ painted = true;
7571
+ if (opts.firstLoad) this.activateBarLayers();
7572
+ if (!final) this.historyState = "backfill";
7573
+ }
7574
+ };
7575
+ const abort = new AbortController();
7576
+ this.progressiveAbort = abort;
7577
+ progressiveServed = await new Promise((firstPaint) => {
7578
+ let signaled = false;
7579
+ const signal = (served) => {
7580
+ if (!signaled) {
7581
+ signaled = true;
7582
+ firstPaint(served);
7583
+ }
7584
+ };
7585
+ abort.signal.addEventListener("abort", () => signal(true), { once: true });
7586
+ this.feed.loadProgressive(market, (bars) => {
7587
+ paint(bars, false);
7588
+ if (painted) signal(true);
7589
+ }, { signal: abort.signal }).then((full) => {
7590
+ if (this.progressiveAbort === abort) this.progressiveAbort = null;
7591
+ if (full == null) return signal(false);
7592
+ if (this.generation !== gen) return signal(true);
7593
+ paint(full, true);
7594
+ this.completeHistory(full.length >= requested ? "depth" : "genesis");
7595
+ signal(true);
7596
+ }).catch(() => {
7597
+ if (this.progressiveAbort === abort) this.progressiveAbort = null;
7598
+ if (this.generation === gen) this.completeHistory("aborted");
7599
+ signal(true);
7600
+ });
7601
+ });
7602
+ }
7603
+ if (progressiveServed) ; else if (deep && this.feed.loadRange) {
7506
7604
  const head = await this.feed.load({ ...market, bars: Math.min(requested, CHUNK_BARS) });
7507
7605
  if (this.generation !== gen) return;
7508
7606
  this.setBarSeries(head);
@@ -8022,7 +8120,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
8022
8120
  const title = options.title ?? "Indicator";
8023
8121
  const handle = new IndicatorHandleImpl(id, title, this, source);
8024
8122
  this.handles.set(id, handle);
8025
- this.registry.add({ id, title, source, options, inputValues: { ...options.inputs ?? {} } });
8123
+ this.registry.add({ id, title, source, options, inputValues: { ...options.inputs ?? {} }, propValues: { ...options.props ?? {} } });
8026
8124
  void this.startIndicator(id, source, options, handle);
8027
8125
  return handle;
8028
8126
  }
@@ -8045,7 +8143,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
8045
8143
  return handle;
8046
8144
  }
8047
8145
  const inputValues = { ...descriptor.defaultInputs(), ...options.inputs ?? {} };
8048
- this.registry.add({ id, title, source: type, inputValues, native: { type, instance: descriptor.create(), descriptor } });
8146
+ this.registry.add({ id, title, source: type, inputValues, propValues: {}, native: { type, instance: descriptor.create(), descriptor } });
8049
8147
  handle.setSchema(descriptor.inputsSchema());
8050
8148
  void this.startNativeIndicator(id, handle);
8051
8149
  return handle;
@@ -8098,6 +8196,19 @@ var EngineOrchestrator = class _EngineOrchestrator {
8098
8196
  if (record.session) record.session.update(record.inputValues);
8099
8197
  else if (record.native && !record.hidden) record.native.instance.setInputs(record.inputValues);
8100
8198
  }
8199
+ /** IndicatorController: re-run an indicator with merged declaration-prop overrides.
8200
+ * Same lifecycle as {@link applyInputs} — a prop change replays the whole script.
8201
+ * Script indicators only: natives have no declaration props. */
8202
+ applyProps(id, values) {
8203
+ const record = this.registry.get(id);
8204
+ if (!record?.session) return;
8205
+ record.propValues = { ...record.propValues, ...values };
8206
+ if (record.renderHandle) this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
8207
+ record.pendingStructural = true;
8208
+ if (!record.hidden) this.setLoading(record, true);
8209
+ record.pendingCause = "inputs";
8210
+ record.session.update(record.inputValues, record.propValues);
8211
+ }
8101
8212
  /** IndicatorController: tear down an indicator and (if now empty) its pane. */
8102
8213
  /** Live handles of every indicator on the chart (script + native), insertion order. */
8103
8214
  listIndicators() {
@@ -8246,6 +8357,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
8246
8357
  for (const input of prepared.inputs) defaults2[input.key] = input.defval;
8247
8358
  record.inputValues = { ...defaults2, ...record.inputValues };
8248
8359
  handle.setSchema(prepared.inputs);
8360
+ const propDefaults = {};
8361
+ for (const prop of prepared.props ?? []) propDefaults[prop.key] = prop.defval;
8362
+ record.propValues = { ...propDefaults, ...record.propValues };
8363
+ handle.setPropsSchema(prepared.props ?? []);
8249
8364
  this.mountLoadingPlaceholder(id, record);
8250
8365
  this.executeIndicator(id, handle);
8251
8366
  } catch (err) {
@@ -8269,6 +8384,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
8269
8384
  getBars: () => this.bars,
8270
8385
  fetchSeries: (sym, tf, range) => this.fetchSeries(sym, tf, range),
8271
8386
  inputs: record.inputValues,
8387
+ props: record.propValues,
8272
8388
  visibleRange: this.currentVisibleRange(),
8273
8389
  mode,
8274
8390
  // Mid-backfill session starts (add / re-show / price-style re-execute) let the
@@ -8362,6 +8478,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
8362
8478
  const meta = record.prepared.meta;
8363
8479
  const model = {
8364
8480
  id,
8481
+ // Deliberately the FULL title, never a shorttitle: while the script loads the
8482
+ // legend identifies it by its full name; the compact shorttitle arrives with
8483
+ // the first computed model and takes over from there.
8365
8484
  title: record.options?.title ?? meta.title,
8366
8485
  overlay: meta.overlay,
8367
8486
  paneHint: meta.overlay ? "price" : "new",
@@ -8370,7 +8489,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
8370
8489
  backgrounds: [],
8371
8490
  priceLines: [],
8372
8491
  inputs: record.prepared.inputs,
8373
- inputValues: record.inputValues
8492
+ inputValues: record.inputValues,
8493
+ ...record.prepared.props ? { props: record.prepared.props, propValues: record.propValues } : {}
8374
8494
  };
8375
8495
  const paneId = this.routePane(id, model, record.options ?? {});
8376
8496
  this.placeModel(model, id, paneId);
@@ -8603,7 +8723,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
8603
8723
  record.model = model;
8604
8724
  if (record.pendingStructural) {
8605
8725
  record.renderHandle = this.renderer.mountIndicator(model);
8606
- this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues);
8726
+ this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
8607
8727
  record.pendingStructural = false;
8608
8728
  } else {
8609
8729
  this.renderer.updateIndicator(record.renderHandle, modelToValuePatch(model));
@@ -8630,16 +8750,17 @@ var EngineOrchestrator = class _EngineOrchestrator {
8630
8750
  placeModel(model, id, paneId) {
8631
8751
  model.id = id;
8632
8752
  model.paneId = paneId;
8633
- for (const series of model.series) series.paneId = paneId;
8634
- for (const fill of model.fills) fill.paneId = paneId;
8635
- for (const bg of model.backgrounds) bg.paneId = paneId;
8753
+ const paneOf = (item) => item.overlay === true ? "price" : paneId;
8754
+ for (const series of model.series) series.paneId = paneOf(series);
8755
+ for (const fill of model.fills) fill.paneId = paneOf(fill);
8756
+ for (const bg of model.backgrounds) bg.paneId = paneOf(bg);
8636
8757
  for (const line of model.priceLines) line.paneId = paneId;
8637
- if (model.lines) for (const ln of model.lines) ln.paneId = paneId;
8638
- if (model.boxes) for (const bx of model.boxes) bx.paneId = paneId;
8639
- if (model.labels) for (const lb of model.labels) lb.paneId = paneId;
8640
- if (model.polylines) for (const pl of model.polylines) pl.paneId = paneId;
8641
- if (model.linefills) for (const lf of model.linefills) lf.paneId = paneId;
8642
- if (model.tables) for (const tb of model.tables) tb.paneId = paneId;
8758
+ if (model.lines) for (const ln of model.lines) ln.paneId = paneOf(ln);
8759
+ if (model.boxes) for (const bx of model.boxes) bx.paneId = paneOf(bx);
8760
+ if (model.labels) for (const lb of model.labels) lb.paneId = paneOf(lb);
8761
+ if (model.polylines) for (const pl of model.polylines) pl.paneId = paneOf(pl);
8762
+ if (model.linefills) for (const lf of model.linefills) lf.paneId = paneOf(lf);
8763
+ if (model.tables) for (const tb of model.tables) tb.paneId = paneOf(tb);
8643
8764
  }
8644
8765
  emitContextChanged(id) {
8645
8766
  if (!this.registry.get(id)?.session?.getContext) return;
@@ -8792,6 +8913,15 @@ var RendererControl = class {
8792
8913
  setLegendActions(provider) {
8793
8914
  this.renderer.setLegendActions?.(provider);
8794
8915
  }
8916
+ /**
8917
+ * Wire the legend rows' HOST-CONTRIBUTED callout bubbles (the shells route the
8918
+ * plugin registry through this; see `registerLegendCallout`). Silent on a renderer
8919
+ * without the seam — contributed callouts simply never show there, same graceful
8920
+ * degradation as {@link setLegendActions}.
8921
+ */
8922
+ setLegendCallouts(provider) {
8923
+ this.renderer.setLegendCallouts?.(provider);
8924
+ }
8795
8925
  /**
8796
8926
  * Replace the indicator legend's fold toggle with a host action (or restore it with
8797
8927
  * `null`) — multi-chart shells point the chip at their indicator overview instead of
@@ -9142,6 +9272,28 @@ var CachingDataFeed = class {
9142
9272
  this.store.merge(key, dropForming(bars));
9143
9273
  return bars;
9144
9274
  }
9275
+ /**
9276
+ * Progressive twin of {@link load}: a COLD load streams through the inner feed's
9277
+ * progressive path — batches forwarded verbatim, the FINAL answer cached exactly as
9278
+ * `load` caches — while a cache-covered load answers once through `load` itself (a
9279
+ * warm chart has nothing to stream). Falls back to `load` wholesale when the inner
9280
+ * feed lacks the capability, so callers may prefer this method unconditionally.
9281
+ */
9282
+ async loadProgressive(cfg, onBatch, opts) {
9283
+ if (cfg.data && cfg.data.length > 0) return this.inner.load(cfg);
9284
+ if (!this.inner.loadProgressive) return null;
9285
+ const symbol = cfg.symbol ?? "TEST";
9286
+ const key = cacheKey(symbol, cfg.timeframe ?? "60", cfg.session);
9287
+ const cached = this.store.get(key);
9288
+ const n = cfg.bars ?? 500;
9289
+ const lastCached = cached?.[cached.length - 1];
9290
+ if (cached && lastCached && cached.length >= n - 1 && this.inner.loadRange) return this.load(cfg);
9291
+ this.store.retainSymbol(symbol);
9292
+ const bars = await this.inner.loadProgressive(cfg, onBatch, opts);
9293
+ if (bars == null) return null;
9294
+ this.store.merge(key, dropForming(bars));
9295
+ return bars;
9296
+ }
9145
9297
  /**
9146
9298
  * Cache-backed ranged fetch — the gateway engines use for secondary series
9147
9299
  * (`request.security` HTF/LTF/cross-symbol) and the orchestrator's backward
@@ -9387,6 +9539,17 @@ var MultiProviderFeed = class {
9387
9539
  this.prefetchSymbolInfo(resolved);
9388
9540
  return this.cache.load(canonical(cfg, resolved));
9389
9541
  }
9542
+ /** Progressive twin of {@link load} — same resolution, the cache streams the batches. */
9543
+ async loadProgressive(cfg, onBatch, opts) {
9544
+ if (cfg.data && cfg.data.length > 0) {
9545
+ this.liveBars = cfg.data.map((b) => ({ ...b }));
9546
+ return cfg.data;
9547
+ }
9548
+ const resolved = await this.registry.whenResolvable(rawSymbol(cfg), { default: this.primaryProvider });
9549
+ this.primaryProvider = resolved.provider;
9550
+ this.prefetchSymbolInfo(resolved);
9551
+ return this.cache.loadProgressive(canonical(cfg, resolved), onBatch, opts);
9552
+ }
9390
9553
  /**
9391
9554
  * Synchronous per-symbol metadata for engines (Pine `syminfo.*`), served from the cache
9392
9555
  * warmed by load(). Undefined until the prefetch lands (the engine then synthesizes a
@@ -9474,6 +9637,18 @@ var RegistryFetchFeed = class {
9474
9637
  if (!provider) return Promise.resolve([]);
9475
9638
  return safeBars(provider, ticker, cfg.timeframe ?? "60", { limit: cfg.bars ?? 500, session: cfg.session });
9476
9639
  }
9640
+ async loadProgressive(cfg, onBatch, opts) {
9641
+ const { provider: name, ticker } = parseSymbol(cfg.symbol ?? "");
9642
+ const provider = this.registry.get(name ?? "");
9643
+ if (!provider) return [];
9644
+ if (!provider.getBarsProgressive) return null;
9645
+ try {
9646
+ return await provider.getBarsProgressive(ticker, cfg.timeframe ?? "60", { limit: cfg.bars ?? 500, session: cfg.session }, onBatch, opts);
9647
+ } catch (e) {
9648
+ console.warn(`[vela] progressive fetch failed for ${ticker} ${cfg.timeframe ?? "60"} \u2014 ${e instanceof Error ? e.message : String(e)}`);
9649
+ return [];
9650
+ }
9651
+ }
9477
9652
  loadRange(cfg, range) {
9478
9653
  const { provider: name, ticker } = parseSymbol(cfg.symbol ?? "");
9479
9654
  const provider = this.registry.get(name ?? "");
@@ -10379,13 +10554,6 @@ var Menu = class {
10379
10554
  }
10380
10555
  };
10381
10556
 
10382
- // src/core/model/inputs.ts
10383
- function inputVisible(when, values) {
10384
- if (!when) return true;
10385
- const conds = Array.isArray(when) ? when : [when];
10386
- return conds.every((c) => c.anyOf ? c.anyOf.some((x) => x === values[c.key]) : values[c.key] === c.equals);
10387
- }
10388
-
10389
10557
  // src/ui/tokens.ts
10390
10558
  var STATIC_ID = "vela-ui-tokens";
10391
10559
  var STATIC_DECLS = Object.entries(STATIC_TOKENS).map(([k, v]) => ` ${k}: ${v};`).join("\n");
@@ -10631,6 +10799,203 @@ var Popover = class {
10631
10799
  }
10632
10800
  };
10633
10801
 
10802
+ // src/ui/components/callout-bubble/controller.ts
10803
+ function closesPanel(item) {
10804
+ return item.close !== false;
10805
+ }
10806
+ function calloutPanelRows(items) {
10807
+ const rows = [];
10808
+ for (const item of items) {
10809
+ if (item.type === "text") {
10810
+ rows.push({ type: "text", text: item.text });
10811
+ continue;
10812
+ }
10813
+ const last = rows[rows.length - 1];
10814
+ if (last && last.type === "buttons") last.buttons.push(item);
10815
+ else rows.push({ type: "buttons", buttons: [item] });
10816
+ }
10817
+ return rows;
10818
+ }
10819
+
10820
+ // src/ui/components/callout-bubble/styles.ts
10821
+ var CALLOUT_STYLE_ID = "vela-ui-callout-bubble";
10822
+ var CALLOUT_CSS = `
10823
+ .vela-callout {
10824
+ display: inline-grid;
10825
+ place-items: center;
10826
+ border-radius: 50%;
10827
+ flex: none;
10828
+ line-height: 0;
10829
+ box-sizing: border-box;
10830
+ cursor: default;
10831
+ user-select: none;
10832
+ -webkit-user-select: none;
10833
+ }
10834
+ .vela-callout[role='button'] { cursor: pointer; }
10835
+ .vela-callout svg { display: block; }
10836
+ /* The deployed panel \u2014 carries the kit's elevated-card look itself (the popover
10837
+ shell is bare positioning chrome). */
10838
+ .vela-callout-panel {
10839
+ display: flex;
10840
+ flex-direction: column;
10841
+ gap: 8px;
10842
+ padding: 10px 12px;
10843
+ max-width: 280px;
10844
+ box-sizing: border-box;
10845
+ background: var(--vela-surface-elev);
10846
+ border: 1px solid var(--vela-border-strong);
10847
+ border-radius: 6px;
10848
+ box-shadow: var(--vela-shadow);
10849
+ color: var(--vela-fg);
10850
+ font: var(--vela-font-size-md) var(--vela-font);
10851
+ }
10852
+ .vela-callout-title { font-weight: 600; color: var(--vela-fg-bright); }
10853
+ .vela-callout-text { color: var(--vela-fg-muted); line-height: 1.45; white-space: pre-line; }
10854
+ .vela-callout-actions { display: flex; flex-wrap: wrap; gap: 8px; }
10855
+ .vela-callout-btn {
10856
+ cursor: pointer;
10857
+ height: 26px;
10858
+ padding: 0 10px;
10859
+ border-radius: 5px;
10860
+ border: 1px solid var(--vela-border);
10861
+ background: transparent;
10862
+ color: var(--vela-fg);
10863
+ font-size: var(--vela-font-size-md);
10864
+ font-family: inherit;
10865
+ 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;
10866
+ }
10867
+ .vela-callout-btn:hover { background: var(--vela-hover); color: var(--vela-fg-bright); border-color: var(--vela-fg-muted); }
10868
+ .vela-callout-btn-primary { border-color: var(--vela-selected-bg); background: var(--vela-selected-bg); color: var(--vela-selected-fg); }
10869
+ .vela-callout-btn-primary:hover { background: var(--vela-selected-bg); color: var(--vela-selected-fg); opacity: 0.85; border-color: var(--vela-selected-bg); }
10870
+ `;
10871
+
10872
+ // src/ui/components/callout-bubble/view.ts
10873
+ var CalloutBubble = class {
10874
+ constructor(opts) {
10875
+ this.pop = null;
10876
+ this.spec = { icon: opts.icon, background: opts.background, label: opts.label };
10877
+ if (opts.color !== void 0) this.spec.color = opts.color;
10878
+ if (opts.panel !== void 0) this.spec.panel = opts.panel;
10879
+ this.size = opts.size ?? 16;
10880
+ this.host = opts.host;
10881
+ this.theme = opts.theme;
10882
+ this.boundary = opts.boundary;
10883
+ const doc = (opts.host ?? document.body).ownerDocument;
10884
+ injectStyles(CALLOUT_STYLE_ID, CALLOUT_CSS, doc);
10885
+ this.el = doc.createElement("span");
10886
+ this.el.className = "vela-callout";
10887
+ this.el.style.width = `${this.size}px`;
10888
+ this.el.style.height = `${this.size}px`;
10889
+ this.el.addEventListener("click", (e) => {
10890
+ if (!this.spec.panel) return;
10891
+ e.stopPropagation();
10892
+ this.toggle();
10893
+ });
10894
+ this.el.addEventListener("dblclick", (e) => {
10895
+ if (this.spec.panel) e.stopPropagation();
10896
+ });
10897
+ this.el.addEventListener("keydown", (e) => {
10898
+ if (!this.spec.panel || e.key !== "Enter" && e.key !== " ") return;
10899
+ e.preventDefault();
10900
+ this.toggle();
10901
+ });
10902
+ this.dress();
10903
+ }
10904
+ /** Re-dress the bubble (a status change: new icon, tint, label, panel). */
10905
+ set(spec) {
10906
+ this.spec = { ...this.spec, ...spec };
10907
+ if ("panel" in spec) this.pop?.hide();
10908
+ this.dress();
10909
+ }
10910
+ /** Whether the deployed panel is currently open. */
10911
+ get open() {
10912
+ return this.pop?.open ?? false;
10913
+ }
10914
+ /** Close the deployed panel, if any. The bubble itself stays. */
10915
+ hidePanel() {
10916
+ this.pop?.hide();
10917
+ }
10918
+ destroy() {
10919
+ this.pop?.destroy();
10920
+ this.pop = null;
10921
+ this.el.remove();
10922
+ }
10923
+ dress() {
10924
+ const clickable = this.spec.panel !== void 0;
10925
+ this.el.style.background = this.spec.background;
10926
+ this.el.style.color = this.spec.color ?? "";
10927
+ this.el.innerHTML = iconAt(this.spec.icon, this.size - 4);
10928
+ this.el.setAttribute("aria-label", this.spec.label);
10929
+ if (clickable) {
10930
+ this.el.setAttribute("role", "button");
10931
+ this.el.tabIndex = 0;
10932
+ } else {
10933
+ this.el.removeAttribute("role");
10934
+ this.el.removeAttribute("tabindex");
10935
+ }
10936
+ }
10937
+ toggle() {
10938
+ if (this.pop?.open) {
10939
+ this.pop.hide();
10940
+ return;
10941
+ }
10942
+ const panel = this.spec.panel;
10943
+ if (!panel) return;
10944
+ this.pop?.destroy();
10945
+ this.pop = new Popover({
10946
+ trigger: this.el,
10947
+ gap: 6,
10948
+ content: (body) => this.buildPanel(body, panel),
10949
+ ...this.host ? { host: this.host } : {},
10950
+ ...this.theme ? { theme: this.theme() } : {},
10951
+ ...this.boundary ? { boundary: this.boundary } : {}
10952
+ });
10953
+ this.pop.show();
10954
+ }
10955
+ buildPanel(body, panel) {
10956
+ const doc = body.ownerDocument;
10957
+ const root = doc.createElement("div");
10958
+ root.className = "vela-callout-panel";
10959
+ if (panel.title) {
10960
+ const title = doc.createElement("div");
10961
+ title.className = "vela-callout-title";
10962
+ title.textContent = panel.title;
10963
+ root.appendChild(title);
10964
+ }
10965
+ for (const row of calloutPanelRows(panel.items)) {
10966
+ if (row.type === "text") {
10967
+ const text = doc.createElement("div");
10968
+ text.className = "vela-callout-text";
10969
+ text.textContent = row.text;
10970
+ root.appendChild(text);
10971
+ continue;
10972
+ }
10973
+ const actions = doc.createElement("div");
10974
+ actions.className = "vela-callout-actions";
10975
+ for (const item of row.buttons) {
10976
+ const btn2 = doc.createElement("button");
10977
+ btn2.type = "button";
10978
+ btn2.className = "vela-callout-btn" + (item.primary ? " vela-callout-btn-primary" : "");
10979
+ btn2.textContent = item.label;
10980
+ btn2.addEventListener("click", () => {
10981
+ item.run();
10982
+ if (closesPanel(item)) this.pop?.hide();
10983
+ });
10984
+ actions.appendChild(btn2);
10985
+ }
10986
+ root.appendChild(actions);
10987
+ }
10988
+ body.appendChild(root);
10989
+ }
10990
+ };
10991
+
10992
+ // src/core/model/inputs.ts
10993
+ function inputVisible(when, values) {
10994
+ if (!when) return true;
10995
+ const conds = Array.isArray(when) ? when : [when];
10996
+ return conds.every((c) => c.anyOf ? c.anyOf.some((x) => x === values[c.key]) : values[c.key] === c.equals);
10997
+ }
10998
+
10634
10999
  // src/ui/components/select/controller.ts
10635
11000
  function selectController(opts) {
10636
11001
  const options = opts.options;
@@ -12545,6 +12910,10 @@ function buildFieldControl(desc) {
12545
12910
  }
12546
12911
 
12547
12912
  // src/renderers/shared/IndicatorInputsDialog.ts
12913
+ var PROPS_TAB = "Properties";
12914
+ function bagOf(row, decl) {
12915
+ return decl.prop ? row.propValues : row.values;
12916
+ }
12548
12917
  var SOURCES = ["close", "open", "high", "low", "hl2", "hlc3", "ohlc4", "volume"];
12549
12918
  var IndicatorInputsDialog = class {
12550
12919
  constructor(host) {
@@ -12555,6 +12924,7 @@ var IndicatorInputsDialog = class {
12555
12924
  this.openId = null;
12556
12925
  this.row = null;
12557
12926
  this.snapshot = null;
12927
+ this.propSnapshot = null;
12558
12928
  this.dialogTips = [];
12559
12929
  /** Re-applies every `when` gate against current values; set while the dialog is open. */
12560
12930
  this.refreshVisibility = null;
@@ -12577,10 +12947,11 @@ var IndicatorInputsDialog = class {
12577
12947
  }
12578
12948
  open(row) {
12579
12949
  this.close();
12580
- if (row.inputs.length === 0) return;
12950
+ if (row.inputs.length === 0 && row.props.length === 0) return;
12581
12951
  this.row = row;
12582
12952
  this.openId = row.id;
12583
12953
  this.snapshot = { ...row.values };
12954
+ this.propSnapshot = { ...row.propValues };
12584
12955
  ensureDialogStyles();
12585
12956
  const t = this.host.theme();
12586
12957
  const border = "var(--vela-border)";
@@ -12588,7 +12959,7 @@ var IndicatorInputsDialog = class {
12588
12959
  const host = this.host.dialogHost() ?? this.host.container;
12589
12960
  const ui = new Dialog({
12590
12961
  host,
12591
- title: row.settingsTitle,
12962
+ title: row.title,
12592
12963
  // Non-modal: live-edit dialog — a modal machine locks pointer events on the
12593
12964
  // whole body, killing the chart and the body-portaled popovers.
12594
12965
  modal: false,
@@ -12620,6 +12991,16 @@ var IndicatorInputsDialog = class {
12620
12991
  text: () => "Close"
12621
12992
  }));
12622
12993
  const tabDefs = tabInputs(row.inputs);
12994
+ if (row.props.length > 0) {
12995
+ const propDecls = row.props.map((p) => {
12996
+ const d = { ...p, prop: true };
12997
+ delete d.tab;
12998
+ return d;
12999
+ });
13000
+ const existing = tabDefs.find((t2) => t2.name === PROPS_TAB);
13001
+ if (existing) existing.inputs.push(...propDecls);
13002
+ else tabDefs.push({ name: PROPS_TAB, inputs: propDecls });
13003
+ }
12623
13004
  const tabs = document.createElement("div");
12624
13005
  tabs.style.cssText = `display:flex;gap:12px;padding:0 20px;border-bottom:1px solid ${border};flex:0 0 auto;`;
12625
13006
  const tabEls = [];
@@ -12722,9 +13103,10 @@ var IndicatorInputsDialog = class {
12722
13103
  this.openId = null;
12723
13104
  this.row = null;
12724
13105
  this.snapshot = null;
13106
+ this.propSnapshot = null;
12725
13107
  ui?.destroy();
12726
13108
  }
12727
- /** Restore every input to its open-time value (re-running the indicator), then close. */
13109
+ /** Restore every input and prop to its open-time value (re-running the indicator), then close. */
12728
13110
  revertAndClose() {
12729
13111
  const row = this.row;
12730
13112
  const snap = this.snapshot;
@@ -12738,6 +13120,17 @@ var IndicatorInputsDialog = class {
12738
13120
  }
12739
13121
  }
12740
13122
  }
13123
+ const propSnap = this.propSnapshot;
13124
+ if (row && propSnap) {
13125
+ for (const p of row.props) {
13126
+ const snapped = propSnap[p.key];
13127
+ const before = snapped !== void 0 ? snapped : p.defval;
13128
+ if (row.propValues[p.key] !== before) {
13129
+ row.propValues[p.key] = before;
13130
+ this.host.onChange?.({ indicatorId: row.id, key: p.key, value: before, kind: "prop" });
13131
+ }
13132
+ }
13133
+ }
12741
13134
  this.close();
12742
13135
  }
12743
13136
  /** The footer's reset button — same chip as Cancel, pinned to the LEFT edge
@@ -12755,19 +13148,27 @@ var IndicatorInputsDialog = class {
12755
13148
  const row = this.row;
12756
13149
  if (!row) return;
12757
13150
  const snap = this.snapshot;
13151
+ const propSnap = this.propSnapshot;
12758
13152
  for (const inp of row.inputs) {
12759
13153
  if (row.values[inp.key] !== inp.defval) {
12760
13154
  row.values[inp.key] = inp.defval;
12761
13155
  this.host.onChange?.({ indicatorId: row.id, key: inp.key, value: inp.defval });
12762
13156
  }
12763
13157
  }
13158
+ for (const p of row.props) {
13159
+ if (row.propValues[p.key] !== p.defval) {
13160
+ row.propValues[p.key] = p.defval;
13161
+ this.host.onChange?.({ indicatorId: row.id, key: p.key, value: p.defval, kind: "prop" });
13162
+ }
13163
+ }
12764
13164
  this.open(row);
12765
13165
  if (snap) this.snapshot = snap;
13166
+ if (propSnap) this.propSnapshot = propSnap;
12766
13167
  }
12767
13168
  /** Write one edit through: store it, notify the host, and re-apply the `when` gates. */
12768
- commit(row, key, value) {
12769
- row.values[key] = value;
12770
- this.host.onChange?.({ indicatorId: row.id, key, value });
13169
+ commit(row, decl, value) {
13170
+ bagOf(row, decl)[decl.key] = value;
13171
+ this.host.onChange?.({ indicatorId: row.id, key: decl.key, value, ...decl.prop ? { kind: "prop" } : {} });
12771
13172
  this.refreshVisibility?.();
12772
13173
  }
12773
13174
  /** One settings row (or several `inline=` inputs) placed into a section's grid. */
@@ -12794,7 +13195,7 @@ var IndicatorInputsDialog = class {
12794
13195
  const id = idOf(lead);
12795
13196
  const info = lead.tooltip ? this.infoButton(lead.tooltip) : void 0;
12796
13197
  if (lead.type === "bool") {
12797
- const current = Boolean(row.values[lead.key] ?? lead.defval);
13198
+ const current = Boolean(bagOf(row, lead)[lead.key] ?? lead.defval);
12798
13199
  return append(fieldRow({
12799
13200
  label: nameOf(lead),
12800
13201
  id,
@@ -12803,7 +13204,7 @@ var IndicatorInputsDialog = class {
12803
13204
  toggle: {
12804
13205
  id,
12805
13206
  checked: current,
12806
- onChange: (v) => this.commit(row, lead.key, v)
13207
+ onChange: (v) => this.commit(row, lead, v)
12807
13208
  }
12808
13209
  }));
12809
13210
  }
@@ -12828,8 +13229,8 @@ var IndicatorInputsDialog = class {
12828
13229
  }
12829
13230
  /** Build the typed control for one input, committing edits live via `onChange`. */
12830
13231
  buildControl(row, inp, id) {
12831
- const current = row.values[inp.key] ?? inp.defval;
12832
- const emit = (value) => this.commit(row, inp.key, value);
13232
+ const current = bagOf(row, inp)[inp.key] ?? inp.defval;
13233
+ const emit = (value) => this.commit(row, inp, value);
12833
13234
  if (inp.type === "bool") {
12834
13235
  return buildFieldControl({ kind: "switch", id, checked: Boolean(current), onChange: (v) => emit(v) }).el;
12835
13236
  }
@@ -12840,7 +13241,7 @@ var IndicatorInputsDialog = class {
12840
13241
  kind: "color",
12841
13242
  id,
12842
13243
  theme: this.host.theme(),
12843
- get: () => String(row.values[inp.key] ?? inp.defval),
13244
+ get: () => String(bagOf(row, inp)[inp.key] ?? inp.defval),
12844
13245
  onChange: (v) => emit(v)
12845
13246
  }).el;
12846
13247
  }
@@ -13403,6 +13804,7 @@ function timeParts(ts) {
13403
13804
  }
13404
13805
 
13405
13806
  // src/renderers/shared/InputsUI.ts
13807
+ var LEGEND_AT_TOP_ATTR = "data-vela-pane-at-top";
13406
13808
  var STATUS_KEYFRAMES = "@keyframes vela-ind-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.3;transform:scale(.6)}}";
13407
13809
  var LEGEND_ICON_PX2 = 16;
13408
13810
  var LEGEND_CTL_PX = 18;
@@ -13420,6 +13822,9 @@ var CLOSE_SVG = iconAt("close", LEGEND_ICON_PX2);
13420
13822
  var FOLD_SVG = iconAt("chevron-up", LEGEND_ICON_PX2);
13421
13823
  var UNFOLD_SVG = iconAt("chevron-down", LEGEND_ICON_PX2);
13422
13824
  var OVERVIEW_SVG = iconAt("objects", LEGEND_ICON_PX2);
13825
+ function legendCalloutsDisplay(open2, hasCallouts) {
13826
+ return !open2 && hasCallouts ? "inline-flex" : "none";
13827
+ }
13423
13828
  var InputsUI = class {
13424
13829
  constructor(container, theme, paneBoundsOf) {
13425
13830
  this.container = container;
@@ -13441,10 +13846,14 @@ var InputsUI = class {
13441
13846
  this.moveApi = null;
13442
13847
  /** Host-contributed legend actions, resolved PER ROW at render time (see setLegendActions). */
13443
13848
  this.legendActions = null;
13849
+ /** Host-contributed callout bubbles, resolved PER ROW at render time (see setLegendCallouts). */
13850
+ this.legendCallouts = null;
13444
13851
  /** Chrome-tooltip disposers, per row id — a tip open at removal must not outlive its row. */
13445
13852
  this.rowTips = /* @__PURE__ */ new Map();
13446
13853
  /** Same, for the contributed extras only (rebuilt independently by setLegendActions). */
13447
13854
  this.extrasTips = /* @__PURE__ */ new Map();
13855
+ /** Same, for the contributed callouts only (rebuilt independently by setLegendCallouts). */
13856
+ this.calloutTips = /* @__PURE__ */ new Map();
13448
13857
  /** Open "Move to" menu (kept so it can be torn down). */
13449
13858
  this.moveMenu = null;
13450
13859
  this.moveTargets = [];
@@ -13602,6 +14011,42 @@ var InputsUI = class {
13602
14011
  extrasEl.appendChild(btn2);
13603
14012
  }
13604
14013
  }
14014
+ /**
14015
+ * Wire the host-contributed callout bubbles. Same contract as
14016
+ * {@link setLegendActions}: re-calling replaces the provider and re-projects the
14017
+ * rows already on screen.
14018
+ */
14019
+ setLegendCallouts(provider) {
14020
+ this.legendCallouts = provider;
14021
+ for (const row of this.rows.values()) this.renderCallouts(row);
14022
+ }
14023
+ /** (Re)build one row's callout bubbles — tinted icon circles beside the title whose
14024
+ * click (when the view carries content) deploys a panel of text and actions. */
14025
+ renderCallouts(row) {
14026
+ this.disposeTips(this.calloutTips, row.id);
14027
+ for (const bubble of row.callouts) bubble.destroy();
14028
+ row.callouts = [];
14029
+ row.calloutsEl.replaceChildren();
14030
+ const views = this.legendCallouts?.(row.id) ?? [];
14031
+ row.calloutsEl.style.display = legendCalloutsDisplay(row.highlighted, views.length > 0);
14032
+ for (const view of views) {
14033
+ const bubble = new CalloutBubble({
14034
+ icon: view.icon,
14035
+ background: view.background,
14036
+ ...view.color !== void 0 ? { color: view.color } : {},
14037
+ label: view.tooltip,
14038
+ ...view.content !== void 0 ? { panel: view.content } : {},
14039
+ // The panel portals into the plot host and self-tokens: it must work on
14040
+ // a bare chart, where no `.vela-ui` token ancestor exists.
14041
+ host: this.container,
14042
+ theme: () => this.theme
14043
+ });
14044
+ bubble.el.dataset.legendCallout = view.id;
14045
+ this.tip(this.calloutTips, row.id, bubble.el, view.tooltip);
14046
+ row.callouts.push(bubble);
14047
+ row.calloutsEl.appendChild(bubble.el);
14048
+ }
14049
+ }
13605
14050
  /** Reposition the per-pane legend containers after a layout change. */
13606
14051
  reposition() {
13607
14052
  for (const [paneId, lg] of this.legends) this.positionLegend(lg, paneId);
@@ -13769,6 +14214,7 @@ var InputsUI = class {
13769
14214
  }
13770
14215
  positionLegend(lg, paneId) {
13771
14216
  const bounds = this.paneBoundsOf ? this.paneBoundsOf(paneId) : { top: 0, height: Infinity };
14217
+ lg.toggleAttribute(LEGEND_AT_TOP_ATTR, bounds.top === 0);
13772
14218
  lg.style.display = bounds.height < 4 || !this.titlesVisible ? "none" : "flex";
13773
14219
  const collapsed = this.paneCollapse.has(paneId);
13774
14220
  const masterId = this.paneCollapse.get(paneId) ?? null;
@@ -13844,13 +14290,13 @@ var InputsUI = class {
13844
14290
  }
13845
14291
  /** Create or update an indicator's legend row (in the legend for its pane). */
13846
14292
  upsert(id, title, inputs, values, paneId = "price", opts = {}) {
13847
- const settingsTitle = opts.settingsTitle ?? title;
13848
14293
  const existing = this.rows.get(id);
13849
14294
  if (existing) {
13850
14295
  existing.title = title;
13851
- existing.settingsTitle = settingsTitle;
13852
14296
  existing.inputs = inputs;
13853
14297
  existing.values = { ...values };
14298
+ existing.props = opts.props ?? [];
14299
+ existing.propValues = { ...opts.propValues ?? {} };
13854
14300
  existing.titleEl.textContent = title;
13855
14301
  if (existing.paneId !== paneId) {
13856
14302
  existing.paneId = paneId;
@@ -13902,6 +14348,9 @@ var InputsUI = class {
13902
14348
  titleWrap.appendChild(beta);
13903
14349
  }
13904
14350
  el.appendChild(titleWrap);
14351
+ const calloutsEl = document.createElement("span");
14352
+ calloutsEl.style.cssText = `display:none;align-items:center;gap:4px;flex:none;margin-left:${LEGEND_TITLE_STATUS_GAP_PX}px;`;
14353
+ el.appendChild(calloutsEl);
13905
14354
  el.appendChild(statusEl);
13906
14355
  const valuesEl = document.createElement("span");
13907
14356
  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;`;
@@ -13918,13 +14367,13 @@ var InputsUI = class {
13918
14367
  eye.className = "vela-ind-ctl";
13919
14368
  eye.style.cssText = LEGEND_CTL_CSS;
13920
14369
  eye.addEventListener("click", () => {
13921
- const row = this.rows.get(id);
13922
- this.onToggleVisible?.(id, Boolean(row?.hidden));
14370
+ const row2 = this.rows.get(id);
14371
+ this.onToggleVisible?.(id, Boolean(row2?.hidden));
13923
14372
  });
13924
14373
  controlsEl.appendChild(eye);
13925
14374
  eyeEl = eye;
13926
14375
  }
13927
- if (inputs.length > 0) {
14376
+ if (inputs.length > 0 || (opts.props ?? []).length > 0) {
13928
14377
  const gear = document.createElement("button");
13929
14378
  gear.type = "button";
13930
14379
  gear.setAttribute("aria-label", "Settings");
@@ -13964,7 +14413,9 @@ var InputsUI = class {
13964
14413
  controlsEl.appendChild(close);
13965
14414
  el.appendChild(controlsEl);
13966
14415
  this.attach(this.legendFor(paneId), el, !!opts.native);
13967
- 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 });
14416
+ 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 };
14417
+ this.rows.set(id, row);
14418
+ this.renderCallouts(row);
13968
14419
  this.syncFoldToggle();
13969
14420
  }
13970
14421
  /** Place a row in its pane's legend — native rows PREPEND (pinned to the top), Pine rows append. */
@@ -13973,9 +14424,11 @@ var InputsUI = class {
13973
14424
  else container.appendChild(el);
13974
14425
  }
13975
14426
  /** Reflect programmatic input changes (so a re-opened dialog shows current values). */
13976
- setValues(id, values) {
14427
+ setValues(id, values, props) {
13977
14428
  const row = this.rows.get(id);
13978
- if (row) row.values = { ...row.values, ...values };
14429
+ if (!row) return;
14430
+ row.values = { ...row.values, ...values };
14431
+ if (props) row.propValues = { ...row.propValues, ...props };
13979
14432
  }
13980
14433
  /**
13981
14434
  * Reflect an indicator's live status in its legend row: `'loading'` shows three pulsing
@@ -14057,8 +14510,13 @@ var InputsUI = class {
14057
14510
  syncRowActions(row) {
14058
14511
  const open2 = row.highlighted;
14059
14512
  row.controlsEl.style.display = open2 || row.hidden ? "inline-flex" : "none";
14060
- if (open2) row.el.appendChild(row.statusEl);
14061
- else row.el.insertBefore(row.statusEl, row.valuesEl);
14513
+ if (open2) {
14514
+ row.el.appendChild(row.statusEl);
14515
+ for (const bubble of row.callouts) bubble.hidePanel();
14516
+ } else {
14517
+ row.el.insertBefore(row.statusEl, row.valuesEl);
14518
+ }
14519
+ row.calloutsEl.style.display = legendCalloutsDisplay(open2, row.callouts.length > 0);
14062
14520
  for (const child of Array.from(row.controlsEl.children)) {
14063
14521
  if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
14064
14522
  if (child === row.extrasEl) {
@@ -14076,10 +14534,12 @@ var InputsUI = class {
14076
14534
  }
14077
14535
  remove(id) {
14078
14536
  const row = this.rows.get(id);
14537
+ for (const bubble of row?.callouts ?? []) bubble.destroy();
14079
14538
  row?.el.remove();
14080
14539
  this.rows.delete(id);
14081
14540
  this.disposeTips(this.rowTips, id);
14082
14541
  this.disposeTips(this.extrasTips, id);
14542
+ this.disposeTips(this.calloutTips, id);
14083
14543
  if (this.selectedId === id) this.selectedId = null;
14084
14544
  if (this.inputsDialog.openId === id) this.inputsDialog.close();
14085
14545
  this.syncFoldToggle();
@@ -14099,6 +14559,8 @@ var InputsUI = class {
14099
14559
  this.rowMenu = null;
14100
14560
  for (const id of [...this.rowTips.keys()]) this.disposeTips(this.rowTips, id);
14101
14561
  for (const id of [...this.extrasTips.keys()]) this.disposeTips(this.extrasTips, id);
14562
+ for (const id of [...this.calloutTips.keys()]) this.disposeTips(this.calloutTips, id);
14563
+ for (const row of this.rows.values()) for (const bubble of row.callouts) bubble.destroy();
14102
14564
  for (const lg of this.legends.values()) lg.remove();
14103
14565
  this.legends.clear();
14104
14566
  this.rows.clear();
@@ -14144,7 +14606,7 @@ var ICONS = {
14144
14606
  };
14145
14607
  var STYLE_ID = "vela-pane-controls";
14146
14608
  var ICON_PX = 12;
14147
- var CLUSTER_PILL = "rgba(0,0,0,0.28)";
14609
+ var CLUSTER_PILL = "rgba(0,0,0,0.65)";
14148
14610
  function ensureStyles() {
14149
14611
  if (typeof document === "undefined" || document.getElementById(STYLE_ID)) return;
14150
14612
  const st = document.createElement("style");
@@ -14164,8 +14626,12 @@ var PaneControls = class {
14164
14626
  this.deps = deps;
14165
14627
  this.clusters = /* @__PURE__ */ new Map();
14166
14628
  this.hoverPaneId = null;
14629
+ /** Mobile: hover clusters are meaningless without a cursor — suppressed; a
14630
+ * collapsed pane's standalone expand chip stays (the only way back up). */
14631
+ this.suspended = false;
14167
14632
  /** Reveal the cluster for the pane under the cursor, resolved from the pointer's y in the plot. */
14168
14633
  this.onPlotMove = (e) => {
14634
+ if (this.suspended) return;
14169
14635
  const rect = this.plot.getBoundingClientRect();
14170
14636
  const y = e.clientY - rect.top;
14171
14637
  let hit = null;
@@ -14237,7 +14703,12 @@ var PaneControls = class {
14237
14703
  }
14238
14704
  if (p.count > 1) {
14239
14705
  cluster.appendChild(
14240
- this.button(p.maximized ? ICONS.restore : ICONS.maximize, p.maximized ? "Restore pane" : "Maximize pane", false, () => this.deps.onToggleMaximize(p.id), { role: "maximize" })
14706
+ this.button(p.maximized ? ICONS.restore : ICONS.maximize, p.maximized ? "Restore pane" : "Maximize pane", false, () => this.deps.onToggleMaximize(p.id), {
14707
+ role: "maximize",
14708
+ // Same inverse-chip treatment as the collapsed pane's expand toggle: the
14709
+ // maximized state must read as an active state, not just a swapped glyph.
14710
+ selected: p.maximized
14711
+ })
14241
14712
  );
14242
14713
  }
14243
14714
  }
@@ -14274,17 +14745,18 @@ var PaneControls = class {
14274
14745
  }
14275
14746
  const hovered = id === this.hoverPaneId;
14276
14747
  const hasButtons = cluster.children.length > 0;
14277
- const visible = hasButtons && (hovered || p.collapsed) && p.height > 8;
14748
+ const stateChipRole = p.collapsed ? "collapse" : !this.suspended && p.maximized ? "maximize" : null;
14749
+ const visible = hasButtons && (hovered || stateChipRole != null) && p.height > 8;
14278
14750
  cluster.style.right = `${rightPx}px`;
14279
14751
  cluster.style.top = p.collapsed ? `${p.top + Math.max(1, Math.round((p.height - 24) / 2))}px` : `${p.top + 4}px`;
14280
14752
  cluster.style.display = visible ? "flex" : "none";
14281
14753
  if (!visible) continue;
14282
- const soloExpand = p.collapsed && !hovered;
14283
- cluster.style.background = soloExpand ? "transparent" : CLUSTER_PILL;
14754
+ const soloChip = stateChipRole != null && !hovered;
14755
+ cluster.style.background = soloChip ? "transparent" : CLUSTER_PILL;
14284
14756
  for (const child of cluster.children) {
14285
14757
  const btn2 = child;
14286
14758
  btn2.style.display = "inline-flex";
14287
- btn2.style.visibility = soloExpand && btn2.dataset.role !== "collapse" ? "hidden" : "visible";
14759
+ btn2.style.visibility = soloChip && btn2.dataset.role !== stateChipRole ? "hidden" : "visible";
14288
14760
  }
14289
14761
  }
14290
14762
  }
@@ -14294,6 +14766,14 @@ var PaneControls = class {
14294
14766
  this.hoverPaneId = paneId;
14295
14767
  this.reposition();
14296
14768
  }
14769
+ /** Mobile suppression: no hover clusters (touch has no cursor; the shell's own
14770
+ * chrome covers maximize), while collapsed panes keep their expand chips. */
14771
+ setSuspended(on) {
14772
+ if (on === this.suspended) return;
14773
+ this.suspended = on;
14774
+ if (on) this.hoverPaneId = null;
14775
+ this.reposition();
14776
+ }
14297
14777
  destroy() {
14298
14778
  this.plot.removeEventListener("pointermove", this.onPlotMove);
14299
14779
  this.plot.removeEventListener("pointerleave", this.onPlotLeave);
@@ -14444,131 +14924,6 @@ var AxisScaleButtons = class {
14444
14924
  }
14445
14925
  };
14446
14926
 
14447
- // src/renderers/shared/TableOverlay.ts
14448
- var SIZE_PX3 = {
14449
- auto: 13,
14450
- tiny: 10,
14451
- small: 11,
14452
- normal: 13,
14453
- large: 16,
14454
- huge: 20
14455
- };
14456
- var TableOverlay = class {
14457
- constructor(container, theme, paneBounds) {
14458
- this.container = container;
14459
- this.theme = theme;
14460
- this.paneBounds = paneBounds;
14461
- this.lastTables = [];
14462
- if (getComputedStyle(container).position === "static") container.style.position = "relative";
14463
- this.root = document.createElement("div");
14464
- Object.assign(this.root.style, {
14465
- position: "absolute",
14466
- inset: "0",
14467
- pointerEvents: "none",
14468
- overflow: "hidden",
14469
- zIndex: "3"
14470
- });
14471
- container.appendChild(this.root);
14472
- }
14473
- update(tables) {
14474
- this.lastTables = tables;
14475
- this.root.replaceChildren();
14476
- for (const t of tables) this.root.appendChild(this.renderTable(t));
14477
- }
14478
- /** Re-render at the current pane geometry — after layout settles or on resize. */
14479
- reposition() {
14480
- if (this.root.isConnected) this.update(this.lastTables);
14481
- }
14482
- /** Show/hide the whole overlay. Tables anchor to pane corners, not to bars, so unlike the
14483
- * series content they DON'T vanish with an emptied chart — the loading state hides them. */
14484
- setVisible(visible) {
14485
- this.root.style.display = visible ? "" : "none";
14486
- }
14487
- destroy() {
14488
- this.root.remove();
14489
- }
14490
- renderTable(t) {
14491
- const wrap = document.createElement("div");
14492
- wrap.style.position = "absolute";
14493
- this.anchor(wrap, t.position, this.paneBounds(t.paneId));
14494
- const table = document.createElement("table");
14495
- Object.assign(table.style, {
14496
- borderCollapse: "collapse",
14497
- background: t.bgColor ?? "transparent",
14498
- fontFamily: this.theme.fontFamily || "sans-serif",
14499
- // Frame as an inset shadow (not a `border`) so it stays independent of the
14500
- // collapsed cell borders even when frame_width != border_width.
14501
- boxShadow: t.frameColor && t.frameWidth > 0 ? `inset 0 0 0 ${t.frameWidth}px ${t.frameColor}` : "none",
14502
- border: "none",
14503
- tableLayout: "auto",
14504
- // Re-enable pointer events on the table only (root is none) so cell tooltips work.
14505
- pointerEvents: "auto"
14506
- });
14507
- const span = /* @__PURE__ */ new Map();
14508
- const skip = /* @__PURE__ */ new Set();
14509
- for (const m of t.merges) {
14510
- span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
14511
- for (let r = m.startRow; r <= m.endRow; r += 1) {
14512
- for (let c = m.startCol; c <= m.endCol; c += 1) {
14513
- if (r !== m.startRow || c !== m.startCol) skip.add(`${r}:${c}`);
14514
- }
14515
- }
14516
- }
14517
- const cellBorder = t.borderColor && t.borderWidth > 0 ? `${t.borderWidth}px solid ${t.borderColor}` : "none";
14518
- for (let r = 0; r < t.rows; r += 1) {
14519
- const tr = document.createElement("tr");
14520
- for (let c = 0; c < t.columns; c += 1) {
14521
- const cell = t.cells[r]?.[c] ?? null;
14522
- if (skip.has(`${r}:${c}`) || cell?.merged) continue;
14523
- const td = document.createElement("td");
14524
- const sp = span.get(`${r}:${c}`);
14525
- if (sp) {
14526
- if (sp.cs > 1) td.colSpan = sp.cs;
14527
- if (sp.rs > 1) td.rowSpan = sp.rs;
14528
- }
14529
- Object.assign(td.style, {
14530
- border: cellBorder,
14531
- padding: "2px 6px",
14532
- background: cell?.bgColor ?? "transparent",
14533
- color: cell?.textColor ?? this.theme.textColor,
14534
- textAlign: cell?.hAlign ?? "center",
14535
- verticalAlign: cell?.vAlign === "top" ? "top" : cell?.vAlign === "bottom" ? "bottom" : "middle",
14536
- fontSize: `${SIZE_PX3[cell?.textSize ?? "normal"]}px`,
14537
- fontFamily: cell?.fontFamily === "monospace" ? "monospace" : "inherit",
14538
- fontWeight: cell?.bold ? "bold" : "normal",
14539
- fontStyle: cell?.italic ? "italic" : "normal",
14540
- whiteSpace: "pre-line"
14541
- });
14542
- if (cell?.tooltip) td.title = cell.tooltip;
14543
- td.textContent = cell?.text ?? "";
14544
- tr.appendChild(td);
14545
- }
14546
- table.appendChild(tr);
14547
- }
14548
- wrap.appendChild(table);
14549
- return wrap;
14550
- }
14551
- /**
14552
- * Position the wrapper at a Pine `position.*` corner/edge of the table's PANE
14553
- * (not the whole chart), inset past the right price axis so it never overlaps
14554
- * the Y-axis labels.
14555
- */
14556
- anchor(el, position, b) {
14557
- const m = 6;
14558
- const containerH = this.root.clientHeight || this.container.clientHeight;
14559
- const paneBottom = b.top + b.height;
14560
- if (position.startsWith("top")) el.style.top = `${b.top + m}px`;
14561
- else if (position.startsWith("bottom")) el.style.bottom = `${Math.max(0, containerH - paneBottom) + m}px`;
14562
- else el.style.top = `${b.top + b.height / 2}px`;
14563
- if (position.endsWith("left")) el.style.left = `${m}px`;
14564
- else if (position.endsWith("right")) el.style.right = `${b.rightAxis + m}px`;
14565
- else el.style.left = `calc(50% - ${b.rightAxis / 2}px)`;
14566
- const tx = position.endsWith("center") ? "-50%" : "0";
14567
- const ty = position.startsWith("middle") ? "-50%" : "0";
14568
- if (tx !== "0" || ty !== "0") el.style.transform = `translate(${tx}, ${ty})`;
14569
- }
14570
- };
14571
-
14572
14927
  // src/renderers/native/capabilities.ts
14573
14928
  var NATIVE_CAPABILITIES = {
14574
14929
  panes: true,
@@ -14589,7 +14944,7 @@ var NATIVE_CAPABILITIES = {
14589
14944
  drawingDepth: true,
14590
14945
  // drawings share the series' z space (backend-composited interleave layers)
14591
14946
  tables: true,
14592
- // reuses the DOM TableOverlay
14947
+ // canvas-painted into the owning indicator's interleave slice
14593
14948
  trades: true,
14594
14949
  // strategy order-fill markers (arrows + labels + fill-price ticks)
14595
14950
  inputsUI: true
@@ -14620,6 +14975,9 @@ function candleTier(spacing) {
14620
14975
  if (spacing < CANDLE_BODY_MIN_SPACING) return "wick";
14621
14976
  return "full";
14622
14977
  }
14978
+ function snapY(yCss, dpr) {
14979
+ return Math.round(yCss * dpr) / dpr;
14980
+ }
14623
14981
  function candleGeometry(xCss, spacing, dpr, bodyScale = 1) {
14624
14982
  const wickDev = Math.max(1, Math.round(wickWidth(spacing) * dpr));
14625
14983
  const wickLeftDev = Math.round(xCss * dpr - wickDev / 2);
@@ -15425,8 +15783,12 @@ var WebGL2Backend = class {
15425
15783
  return sc === pane.scale ? pane : { ...pane, scale: sc };
15426
15784
  };
15427
15785
  b.alpha = this.modelAlpha;
15428
- for (const m of models) for (const bgSpan of m.backgrounds) this.emitBackground(b, bgSpan, pane, coords);
15429
- 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));
15786
+ for (const m of models) for (const bgSpan of m.backgrounds) if (bgSpan.overlay !== true) this.emitBackground(b, bgSpan, pane, coords);
15787
+ if (isPrice) {
15788
+ for (const m of scene.indicators.values()) {
15789
+ for (const bgSpan of m.backgrounds) if (bgSpan.overlay === true) this.emitBackground(b, bgSpan, pane, coords);
15790
+ }
15791
+ }
15430
15792
  const drawCandles = isPrice && !scene.candlesHidden;
15431
15793
  let candleDrawn = false;
15432
15794
  for (const m of models) {
@@ -15440,15 +15802,26 @@ var WebGL2Backend = class {
15440
15802
  b.alpha = this.modelAlpha;
15441
15803
  const off = scene.offsetOf(m.id);
15442
15804
  const mp = effPane(m);
15443
- for (const s of m.series) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
15805
+ for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, mp, coords, i0, i1, off);
15806
+ for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
15444
15807
  }
15445
15808
  if (drawCandles && !candleDrawn) {
15446
15809
  drawSlicesUpTo(scene.candleZ);
15447
15810
  b.alpha = this.candleStructureAlpha;
15448
15811
  this.emitPriceSeries(b, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
15449
15812
  }
15450
- drawSlicesUpTo(Infinity);
15451
15813
  b.alpha = this.modelAlpha;
15814
+ if (isPrice) {
15815
+ for (const m of scene.indicators.values()) {
15816
+ const off = scene.offsetOf(m.id);
15817
+ for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, i1, off);
15818
+ }
15819
+ for (const m of scene.indicators.values()) {
15820
+ const off = scene.offsetOf(m.id);
15821
+ for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0, i1, theme, off);
15822
+ }
15823
+ }
15824
+ drawSlicesUpTo(Infinity);
15452
15825
  for (const m of models) {
15453
15826
  const mp = effPane(m);
15454
15827
  for (const pl of m.priceLines) this.emitHline(b, pl, mp, coords, dataW, theme);
@@ -15495,7 +15868,7 @@ var WebGL2Backend = class {
15495
15868
  for (const pane of scene.orderedPanes()) {
15496
15869
  const b = this.glowBatch;
15497
15870
  b.reset();
15498
- this.emitGlowSources(b, scene.indicatorsForPane(pane.id), pane, coords, i0, i1, (id) => scene.offsetOf(id));
15871
+ this.emitGlowSources(b, scene, pane, coords, i0, i1);
15499
15872
  if (b.vertexCount === 0) continue;
15500
15873
  const topH = Math.round(pane.bounds.top * dpr * 0.5);
15501
15874
  const botH = Math.round((pane.bounds.top + pane.bounds.height) * dpr * 0.5);
@@ -15564,15 +15937,23 @@ var WebGL2Backend = class {
15564
15937
  gl.bindVertexArray(null);
15565
15938
  return true;
15566
15939
  }
15567
- /** The "neon" elements that glow: line/area/step lines + point markers (not candles/fills/bars). */
15568
- emitGlowSources(b, models, pane, coords, i0, i1, offsetOf = () => 0) {
15569
- for (const m of models) {
15570
- const off = offsetOf(m.id);
15571
- for (const s of m.series) {
15572
- if (!isLineLikeSeries(s) || s.visible === false) continue;
15573
- if (s.kind === "histogram" || s.kind === "columns") continue;
15574
- if (s.kind === "circles" || s.kind === "cross") this.emitPointMarkers(b, s, pane, coords, i0, i1, off);
15575
- else this.emitPolyline(b, s, pane, coords, i0, i1, s.kind === "step", off);
15940
+ /** The "neon" elements that glow: line/area/step lines + point markers (not candles/fills/bars).
15941
+ * Routing mirrors the main pass: own series per pane, force_overlay series on the price pane. */
15942
+ emitGlowSources(b, scene, pane, coords, i0, i1) {
15943
+ const emitOne = (s, off) => {
15944
+ if (!isLineLikeSeries(s) || s.visible === false) return;
15945
+ if (s.kind === "histogram" || s.kind === "columns") return;
15946
+ if (s.kind === "circles" || s.kind === "cross") this.emitPointMarkers(b, s, pane, coords, i0, i1, off);
15947
+ else this.emitPolyline(b, s, pane, coords, i0, i1, s.kind === "step", off);
15948
+ };
15949
+ for (const m of scene.indicatorsForPane(pane.id)) {
15950
+ const off = scene.offsetOf(m.id);
15951
+ for (const s of m.series) if (s.overlay !== true) emitOne(s, off);
15952
+ }
15953
+ if (pane.kind === "price") {
15954
+ for (const m of scene.indicators.values()) {
15955
+ const off = scene.offsetOf(m.id);
15956
+ for (const s of m.series) if (s.overlay === true) emitOne(s, off);
15576
15957
  }
15577
15958
  }
15578
15959
  }
@@ -15835,12 +16216,12 @@ var WebGL2Backend = class {
15835
16216
  if (drawBody) {
15836
16217
  const oY = coords.priceToY(bar.open, pane.scale, pane.bounds);
15837
16218
  const cY = coords.priceToY(bar.close, pane.scale, pane.bounds);
15838
- bodyTop = Math.min(oY, cY);
15839
- bodyH = Math.max(1, Math.abs(cY - oY));
16219
+ bodyTop = snapY(Math.min(oY, cY), coords.dpr);
16220
+ bodyH = Math.max(1 / coords.dpr, snapY(Math.max(oY, cY), coords.dpr) - bodyTop);
15840
16221
  }
15841
16222
  if (cs.wickVisible) {
15842
- const hY = coords.priceToY(bar.high, pane.scale, pane.bounds);
15843
- const lY = coords.priceToY(bar.low, pane.scale, pane.bounds);
16223
+ const hY = snapY(coords.priceToY(bar.high, pane.scale, pane.bounds), coords.dpr);
16224
+ const lY = snapY(coords.priceToY(bar.low, pane.scale, pane.bounds), coords.dpr);
15844
16225
  b.alpha = this.candleStructureAlpha;
15845
16226
  const wCol = parseColor((isUp ? cs.wickUpColor : cs.wickDownColor) ?? (drawBody ? dir : bodyColorStr));
15846
16227
  if (drawBody) {
@@ -15855,9 +16236,9 @@ var WebGL2Backend = class {
15855
16236
  b.alpha = this.candleBodyAlpha;
15856
16237
  b.rect(g.bodyX, bodyTop, g.bodyW, bodyH, c);
15857
16238
  }
15858
- if (cs.borderVisible || bc || fading && cs.bodyVisible) {
16239
+ if (cs.borderVisible || fading && cs.bodyVisible) {
15859
16240
  b.alpha = this.candleStructureAlpha;
15860
- const bord = cs.borderVisible || bc ? parseColor((isUp ? cs.borderUpColor : cs.borderDownColor) ?? dir) : c;
16241
+ const bord = cs.borderVisible ? parseColor((isUp ? cs.borderUpColor : cs.borderDownColor) ?? bodyColorStr) : c;
15861
16242
  const bw = Math.max(0, g.bodyW - 1);
15862
16243
  const bh = Math.max(0, bodyH - 1);
15863
16244
  b.rectStroke(g.bodyX + 0.5, bodyTop + 0.5, bw, bh, 1, bord);
@@ -17173,9 +17554,10 @@ var SceneGraph = class {
17173
17554
  * so each indicator arrives behind the candles (and behind older indicators);
17174
17555
  * `setIndicatorZ`/`bringToFront`/`sendToBack` change it. */
17175
17556
  this.seriesZ = /* @__PURE__ */ new Map();
17176
- /** Per-pane raster layers of user drawings interleaved into the series stack each is a
17557
+ /** Per-pane raster layers of drawings interleaved into the series stack (each
17558
+ * indicator's Pine drawings at its model's z, plus in-stack user drawings) — each a
17177
17559
  * prepainted canvas the backend composites just before the series carrying `beforeZ`.
17178
- * Rebuilt by the renderer per data frame; empty when every drawing sits over the stack. */
17560
+ * Rebuilt by the renderer per data frame. */
17179
17561
  this.drawingSlices = /* @__PURE__ */ new Map();
17180
17562
  /** Per-model index offset: the chart bar index of the model's `anchorTime` — its
17181
17563
  * index-aligned payloads (dense series arrays, `bar_index` drawings) count from that
@@ -17409,8 +17791,12 @@ var Canvas2dBackend = class {
17409
17791
  return sc === pane.scale ? pane : { ...pane, scale: sc };
17410
17792
  };
17411
17793
  ctx.globalAlpha = this.modelAlpha;
17412
- for (const m of models) for (const bg of m.backgrounds) this.drawBackground(ctx, bg, pane, coords);
17413
- 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));
17794
+ for (const m of models) for (const bg of m.backgrounds) if (bg.overlay !== true) this.drawBackground(ctx, bg, pane, coords);
17795
+ if (isPrice) {
17796
+ for (const m of scene.indicators.values()) {
17797
+ for (const bg of m.backgrounds) if (bg.overlay === true) this.drawBackground(ctx, bg, pane, coords);
17798
+ }
17799
+ }
17414
17800
  const slices = scene.drawingSlices.get(pane.id) ?? [];
17415
17801
  let si = 0;
17416
17802
  const drawSlicesUpTo = (z) => {
@@ -17432,13 +17818,25 @@ var Canvas2dBackend = class {
17432
17818
  ctx.globalAlpha = this.modelAlpha;
17433
17819
  const off = scene.offsetOf(m.id);
17434
17820
  const mp = effPane(m);
17435
- for (const s of m.series) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
17821
+ for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, mp, coords, i0, i1, off);
17822
+ for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
17436
17823
  }
17437
17824
  if (drawCandles && !candleDrawn) {
17438
17825
  drawSlicesUpTo(scene.candleZ);
17439
17826
  ctx.globalAlpha = this.candleStructureAlpha;
17440
17827
  this.drawPriceSeries(ctx, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
17441
17828
  }
17829
+ if (isPrice) {
17830
+ ctx.globalAlpha = this.modelAlpha;
17831
+ for (const m of scene.indicators.values()) {
17832
+ const off = scene.offsetOf(m.id);
17833
+ for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, off);
17834
+ }
17835
+ for (const m of scene.indicators.values()) {
17836
+ const off = scene.offsetOf(m.id);
17837
+ for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0, i1, theme, off);
17838
+ }
17839
+ }
17442
17840
  drawSlicesUpTo(Infinity);
17443
17841
  ctx.globalAlpha = this.modelAlpha;
17444
17842
  for (const m of models) {
@@ -17683,13 +18081,13 @@ var Canvas2dBackend = class {
17683
18081
  if (drawBody) {
17684
18082
  const oY = coords.priceToY(b.open, pane.scale, pane.bounds);
17685
18083
  const cY = coords.priceToY(b.close, pane.scale, pane.bounds);
17686
- top = Math.min(oY, cY);
17687
- bodyH = Math.max(1, Math.abs(cY - oY));
18084
+ top = snapY(Math.min(oY, cY), coords.dpr);
18085
+ bodyH = Math.max(1 / coords.dpr, snapY(Math.max(oY, cY), coords.dpr) - top);
17688
18086
  }
17689
18087
  if (cs.wickVisible) {
17690
18088
  const wick = (up ? cs.wickUpColor : cs.wickDownColor) ?? (drawBody ? dir : color);
17691
- const hY = coords.priceToY(b.high, pane.scale, pane.bounds);
17692
- const lY = coords.priceToY(b.low, pane.scale, pane.bounds);
18089
+ const hY = snapY(coords.priceToY(b.high, pane.scale, pane.bounds), coords.dpr);
18090
+ const lY = snapY(coords.priceToY(b.low, pane.scale, pane.bounds), coords.dpr);
17693
18091
  ctx.globalAlpha = this.candleStructureAlpha;
17694
18092
  ctx.strokeStyle = wick;
17695
18093
  ctx.lineWidth = g.wickW;
@@ -17711,9 +18109,9 @@ var Canvas2dBackend = class {
17711
18109
  ctx.fillStyle = color;
17712
18110
  ctx.fillRect(g.bodyX, top, g.bodyW, bodyH);
17713
18111
  }
17714
- if (cs.borderVisible || bc || fading && cs.bodyVisible) {
18112
+ if (cs.borderVisible || fading && cs.bodyVisible) {
17715
18113
  ctx.globalAlpha = this.candleStructureAlpha;
17716
- ctx.strokeStyle = cs.borderVisible || bc ? (up ? cs.borderUpColor : cs.borderDownColor) ?? dir : color;
18114
+ ctx.strokeStyle = cs.borderVisible ? (up ? cs.borderUpColor : cs.borderDownColor) ?? color : color;
17717
18115
  ctx.lineWidth = 1;
17718
18116
  const bw = Math.max(0, g.bodyW - 1);
17719
18117
  const bh = Math.max(0, bodyH - 1);
@@ -18136,6 +18534,19 @@ function autoFontSize(lines, boxW, boxH, bold) {
18136
18534
 
18137
18535
  // src/renderers/shared/DrawingSceneRenderer.ts
18138
18536
  var EMPTY_DRAWING_SET = { lines: [], boxes: [], labels: [], polylines: [], linefills: [] };
18537
+ function modelDrawingSet(m, overlay) {
18538
+ const want = (d) => Boolean(d.overlay) === overlay;
18539
+ return {
18540
+ lines: (m.lines ?? []).filter(want),
18541
+ boxes: (m.boxes ?? []).filter(want),
18542
+ labels: (m.labels ?? []).filter(want),
18543
+ polylines: (m.polylines ?? []).filter(want),
18544
+ linefills: (m.linefills ?? []).filter(want)
18545
+ };
18546
+ }
18547
+ function drawingSetEmpty(s) {
18548
+ return !s.lines.length && !s.boxes.length && !s.labels.length && !s.polylines.length && !s.linefills.length;
18549
+ }
18139
18550
  function fontSizePx(size) {
18140
18551
  return size === "auto" ? 12 : namedFontSize(size);
18141
18552
  }
@@ -18145,6 +18556,8 @@ var DrawingSceneRenderer = class {
18145
18556
  this.set = set;
18146
18557
  /** measureText width cache, keyed by `${font} ${text}`, persists across frames. */
18147
18558
  this.widthCache = /* @__PURE__ */ new Map();
18559
+ /** Tooltip hit-rects captured while drawing the CURRENT set (rebuilt per render). */
18560
+ this.tipRegions = [];
18148
18561
  /**
18149
18562
  * Index offset of the CURRENT set's model: its `xloc:'bar_index'` coordinates count
18150
18563
  * from the model's anchor bar, so they shift by this to land on chart logical indices.
@@ -18165,8 +18578,13 @@ var DrawingSceneRenderer = class {
18165
18578
  const s = this.set;
18166
18579
  return !s.lines.length && !s.boxes.length && !s.labels.length && !s.polylines.length && !s.linefills.length;
18167
18580
  }
18581
+ /** Tooltip hit-rects of the labels drawn by the LAST `render` call (same coords as `ctx`). */
18582
+ labelTipRegions() {
18583
+ return this.tipRegions;
18584
+ }
18168
18585
  /** Draw the whole set into `ctx` using the supplied coordinate closures. */
18169
18586
  render(ctx, W, H, xOf, yOf) {
18587
+ this.tipRegions = [];
18170
18588
  if (this.isEmpty()) return;
18171
18589
  ctx.save();
18172
18590
  this.drawLinefills(ctx, W, H, xOf, yOf);
@@ -18514,13 +18932,35 @@ var DrawingSceneRenderer = class {
18514
18932
  if (this.isPointShape(lb.style)) {
18515
18933
  if (!lb.noFill) this.drawLabelShape(ctx, lb.style, px, py, fontPx, color);
18516
18934
  if (lb.text) this.drawLabelText(ctx, lb, px, py + fontPx, fontPx);
18517
- } else if (lb.style === "none" || lb.style === "text_outline" || lb.noFill) {
18518
- if (lb.text) this.drawLabelText(ctx, lb, px, py, fontPx, lb.style === "text_outline");
18935
+ if (lb.tooltip) {
18936
+ const r = Math.max(4, fontPx * 0.6) + 3;
18937
+ this.tipRegions.push({ left: px - r, top: py - r, right: px + r, bottom: py + r, text: lb.tooltip });
18938
+ }
18939
+ } else if (lb.style === "none" || lb.style === "text_outline") {
18940
+ if (lb.text) {
18941
+ this.drawLabelText(ctx, lb, px, py, fontPx, lb.style === "text_outline");
18942
+ if (lb.tooltip) this.tipRegions.push(this.textRegion(ctx, lb, px, py, fontPx, lb.tooltip));
18943
+ }
18519
18944
  } else {
18520
- this.drawBubble(ctx, lb, px, py, fontPx, color);
18945
+ const r = this.drawBubble(ctx, lb, px, py, fontPx, color);
18946
+ 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 });
18521
18947
  }
18522
18948
  }
18523
18949
  }
18950
+ /** Canvas font for a label's text — family + Pine `text_formatting` (bold/italic). */
18951
+ labelFont(lb, fontPx) {
18952
+ const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
18953
+ return `${lb.italic ? "italic " : ""}${lb.bold ? "bold " : ""}${fontPx}px ${family}`;
18954
+ }
18955
+ /** Hover rect of a text-only label (style none/text_outline/noFill), centered like drawLabelText. */
18956
+ textRegion(ctx, lb, cx, cy, fontPx, text) {
18957
+ const font = this.labelFont(lb, fontPx);
18958
+ const lines = (lb.text ?? "").split("\n");
18959
+ const w = Math.max(1, ...lines.map((l) => this.measure(ctx, font, l)));
18960
+ const h = fontPx * 1.25 * lines.length;
18961
+ const left = lb.textAlign === "left" ? cx : lb.textAlign === "right" ? cx - w : cx - w / 2;
18962
+ return { left, top: cy - h / 2, right: left + w, bottom: cy + h / 2, text };
18963
+ }
18524
18964
  isPointShape(style) {
18525
18965
  switch (style) {
18526
18966
  case "circle":
@@ -18539,8 +18979,7 @@ var DrawingSceneRenderer = class {
18539
18979
  }
18540
18980
  }
18541
18981
  drawLabelText(ctx, lb, cx, cy, fontPx, outline = false) {
18542
- const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
18543
- ctx.font = `${fontPx}px ${family}`;
18982
+ ctx.font = this.labelFont(lb, fontPx);
18544
18983
  ctx.textAlign = lb.textAlign === "left" ? "left" : lb.textAlign === "right" ? "right" : "center";
18545
18984
  ctx.textBaseline = "middle";
18546
18985
  const lines = lb.text.split("\n");
@@ -18620,9 +19059,9 @@ var DrawingSceneRenderer = class {
18620
19059
  ctx.fill();
18621
19060
  }
18622
19061
  }
19062
+ /** Draw a bubble label; returns the bubble body rect (for tooltip hit-testing). */
18623
19063
  drawBubble(ctx, lb, px, py, fontPx, color) {
18624
- const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
18625
- ctx.font = `${fontPx}px ${family}`;
19064
+ ctx.font = this.labelFont(lb, fontPx);
18626
19065
  const lines = (lb.text ?? "").split("\n");
18627
19066
  const padX = 6;
18628
19067
  const padY = 4;
@@ -18680,38 +19119,51 @@ var DrawingSceneRenderer = class {
18680
19119
  by = py - h - ptr;
18681
19120
  pointer = "down";
18682
19121
  }
18683
- ctx.fillStyle = color;
18684
- this.roundRect(ctx, bx, by, w, h, 4);
18685
- ctx.fill();
18686
- if (pointer !== "none") {
18687
- ctx.beginPath();
18688
- if (pointer === "down") {
18689
- ctx.moveTo(px - ptr, by + h);
18690
- ctx.lineTo(px + ptr, by + h);
18691
- ctx.lineTo(px, py);
18692
- } else if (pointer === "up") {
18693
- ctx.moveTo(px - ptr, by);
18694
- ctx.lineTo(px + ptr, by);
18695
- ctx.lineTo(px, py);
18696
- } else if (pointer === "left") {
18697
- ctx.moveTo(bx, py - ptr);
18698
- ctx.lineTo(bx, py + ptr);
18699
- ctx.lineTo(px, py);
18700
- } else {
18701
- ctx.moveTo(bx + w, py - ptr);
18702
- ctx.lineTo(bx + w, py + ptr);
18703
- ctx.lineTo(px, py);
18704
- }
18705
- ctx.closePath();
19122
+ if (!lb.noFill) {
19123
+ ctx.fillStyle = color;
19124
+ this.roundRect(ctx, bx, by, w, h, 4);
18706
19125
  ctx.fill();
19126
+ if (pointer !== "none") {
19127
+ ctx.beginPath();
19128
+ if (pointer === "down") {
19129
+ ctx.moveTo(px - ptr, by + h);
19130
+ ctx.lineTo(px + ptr, by + h);
19131
+ ctx.lineTo(px, py);
19132
+ } else if (pointer === "up") {
19133
+ ctx.moveTo(px - ptr, by);
19134
+ ctx.lineTo(px + ptr, by);
19135
+ ctx.lineTo(px, py);
19136
+ } else if (pointer === "left") {
19137
+ ctx.moveTo(bx, py - ptr);
19138
+ ctx.lineTo(bx, py + ptr);
19139
+ ctx.lineTo(px, py);
19140
+ } else {
19141
+ ctx.moveTo(bx + w, py - ptr);
19142
+ ctx.lineTo(bx + w, py + ptr);
19143
+ ctx.lineTo(px, py);
19144
+ }
19145
+ ctx.closePath();
19146
+ ctx.fill();
19147
+ }
18707
19148
  }
18708
19149
  if (lb.text) {
18709
- ctx.fillStyle = lb.textColor ?? contrastColor(color);
18710
- ctx.textAlign = "center";
19150
+ ctx.fillStyle = lb.textColor ?? (lb.noFill ? this.deps.theme.textColor : contrastColor(color));
18711
19151
  ctx.textBaseline = "middle";
19152
+ let tx;
19153
+ if (lb.textAlign === "left") {
19154
+ ctx.textAlign = "left";
19155
+ tx = bx + padX;
19156
+ } else if (lb.textAlign === "right") {
19157
+ ctx.textAlign = "right";
19158
+ tx = bx + w - padX;
19159
+ } else {
19160
+ ctx.textAlign = "center";
19161
+ tx = bx + w / 2;
19162
+ }
18712
19163
  const startY = by + padY + lineH / 2;
18713
- for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], bx + w / 2, startY + i * lineH);
19164
+ for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], tx, startY + i * lineH);
18714
19165
  }
19166
+ return { x: bx, y: by, w, h };
18715
19167
  }
18716
19168
  roundRect(ctx, x, y, w, h, r) {
18717
19169
  const rr = Math.min(r, w / 2, h / 2);
@@ -18947,7 +19399,7 @@ var ChromeRenderer = class {
18947
19399
  this.ctx = null;
18948
19400
  // The color for axis tick labels — the host-passed surface text, set each frame in render().
18949
19401
  this.axisTextColor = DARK_THEME.textColor;
18950
- // Shared Pine-drawing renderer (line/box/label/polyline/linefill); widthCache persists.
19402
+ // Shared Pine-drawing renderer, used here for autoscale geometry only; widthCache persists.
18951
19403
  this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
18952
19404
  }
18953
19405
  mount(canvas) {
@@ -18972,8 +19424,8 @@ var ChromeRenderer = class {
18972
19424
  */
18973
19425
  paneDrawingsRange(ownModels, scene, isPricePane, vr) {
18974
19426
  let dr = null;
18975
- for (const m of ownModels) dr = unionRange(dr, this.drawingsRange(this.ownDrawings(m), vr, scene.offsetOf(m.id)));
18976
- if (isPricePane) for (const m of scene.indicators.values()) dr = unionRange(dr, this.drawingsRange(this.overlayDrawings(m), vr, scene.offsetOf(m.id)));
19427
+ for (const m of ownModels) dr = unionRange(dr, this.drawingsRange(modelDrawingSet(m, false), vr, scene.offsetOf(m.id)));
19428
+ if (isPricePane) for (const m of scene.indicators.values()) dr = unionRange(dr, this.drawingsRange(modelDrawingSet(m, true), vr, scene.offsetOf(m.id)));
18977
19429
  return dr;
18978
19430
  }
18979
19431
  /** Clear the chrome canvas and draw drawings + axes + current-price line.
@@ -19005,17 +19457,6 @@ var ChromeRenderer = class {
19005
19457
  return;
19006
19458
  }
19007
19459
  const pricePane = panes.find((p) => p.kind === "price") ?? null;
19008
- for (const pane of panes) {
19009
- if (pane.collapsed) continue;
19010
- for (const m of scene.indicatorsForPane(pane.id)) {
19011
- const sc = scene.scaleFor(m, pane);
19012
- const mp = sc === pane.scale ? pane : { ...pane, scale: sc };
19013
- this.renderDrawings(ctx, coords, this.ownDrawings(m), mp, dataW, scene.offsetOf(m.id));
19014
- }
19015
- }
19016
- if (pricePane) {
19017
- for (const m of scene.indicators.values()) this.renderDrawings(ctx, coords, this.overlayDrawings(m), pricePane, dataW, scene.offsetOf(m.id));
19018
- }
19019
19460
  if (pricePane && !pricePane.collapsed && scene.tradeMarkers.visible) {
19020
19461
  for (const m of scene.indicators.values()) {
19021
19462
  if (m.trades?.length) this.renderTrades(ctx, coords, scene, theme, m.trades, pricePane, dataW);
@@ -19031,25 +19472,6 @@ var ChromeRenderer = class {
19031
19472
  this.canvas = null;
19032
19473
  this.ctx = null;
19033
19474
  }
19034
- // ── Pine-drawing helpers (own vs force_overlay routing) ──
19035
- ownDrawings(m) {
19036
- return {
19037
- lines: (m.lines ?? []).filter((d) => !d.overlay),
19038
- boxes: (m.boxes ?? []).filter((d) => !d.overlay),
19039
- labels: (m.labels ?? []).filter((d) => !d.overlay),
19040
- polylines: (m.polylines ?? []).filter((d) => !d.overlay),
19041
- linefills: (m.linefills ?? []).filter((d) => !d.overlay)
19042
- };
19043
- }
19044
- overlayDrawings(m) {
19045
- return {
19046
- lines: (m.lines ?? []).filter((d) => d.overlay),
19047
- boxes: (m.boxes ?? []).filter((d) => d.overlay),
19048
- labels: (m.labels ?? []).filter((d) => d.overlay),
19049
- polylines: (m.polylines ?? []).filter((d) => d.overlay),
19050
- linefills: (m.linefills ?? []).filter((d) => d.overlay)
19051
- };
19052
- }
19053
19475
  drawingsRange(set, vr, indexOffset = 0) {
19054
19476
  this.drawScene.setSet(set, indexOffset);
19055
19477
  if (this.drawScene.isEmpty()) return null;
@@ -19083,23 +19505,6 @@ var ChromeRenderer = class {
19083
19505
  );
19084
19506
  ctx.restore();
19085
19507
  }
19086
- renderDrawings(ctx, coords, set, pane, dataW, indexOffset = 0) {
19087
- this.drawScene.setSet(set, indexOffset);
19088
- if (this.drawScene.isEmpty()) return;
19089
- ctx.save();
19090
- ctx.translate(0, pane.bounds.top);
19091
- ctx.beginPath();
19092
- ctx.rect(0, 0, dataW, pane.bounds.height);
19093
- ctx.clip();
19094
- this.drawScene.render(
19095
- ctx,
19096
- dataW,
19097
- pane.bounds.height,
19098
- (l) => coords.logicalToX(l),
19099
- (price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
19100
- );
19101
- ctx.restore();
19102
- }
19103
19508
  // ── axes ──
19104
19509
  drawPriceAxes(ctx, scene, coords, theme, dataW, panes) {
19105
19510
  ctx.strokeStyle = scene.style.borderColor ?? theme.borderColor;
@@ -19315,6 +19720,68 @@ function formatCountdown(ms) {
19315
19720
  return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
19316
19721
  }
19317
19722
 
19723
+ // src/renderers/native/chrome/LabelTooltip.ts
19724
+ var HOVER_DELAY_MS = 350;
19725
+ var LabelTooltip = class {
19726
+ constructor(plot, deps) {
19727
+ this.plot = plot;
19728
+ this.deps = deps;
19729
+ this.tip = null;
19730
+ this.timer = null;
19731
+ /** Text of the currently open OR armed tip — dedupes moves inside one label. */
19732
+ this.current = null;
19733
+ this.onMove = (e) => {
19734
+ if (e.pointerType !== "mouse") return;
19735
+ const rect = this.plot.getBoundingClientRect();
19736
+ const x = e.clientX - rect.left;
19737
+ const y = e.clientY - rect.top;
19738
+ const text = this.deps.lookup(x, y);
19739
+ if (!text) {
19740
+ this.clear();
19741
+ return;
19742
+ }
19743
+ if (text === this.current) return;
19744
+ this.clear();
19745
+ this.current = text;
19746
+ this.timer = window.setTimeout(() => this.show(text, x, y), HOVER_DELAY_MS);
19747
+ };
19748
+ this.onLeave = () => {
19749
+ this.clear();
19750
+ };
19751
+ plot.addEventListener("pointermove", this.onMove);
19752
+ plot.addEventListener("pointerleave", this.onLeave);
19753
+ plot.addEventListener("pointerdown", this.onLeave);
19754
+ }
19755
+ destroy() {
19756
+ this.plot.removeEventListener("pointermove", this.onMove);
19757
+ this.plot.removeEventListener("pointerleave", this.onLeave);
19758
+ this.plot.removeEventListener("pointerdown", this.onLeave);
19759
+ this.clear();
19760
+ }
19761
+ show(text, x, y) {
19762
+ const doc = this.plot.ownerDocument;
19763
+ const tip = doc.createElement("div");
19764
+ tip.textContent = text;
19765
+ 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;";
19766
+ applyChromeTokens(tip, this.deps.theme());
19767
+ this.plot.appendChild(tip);
19768
+ const pw = this.plot.clientWidth;
19769
+ const ph = this.plot.clientHeight;
19770
+ tip.style.left = `${Math.max(0, Math.min(x + 12, pw - tip.offsetWidth - 4))}px`;
19771
+ tip.style.top = `${Math.max(0, Math.min(y + 16, ph - tip.offsetHeight - 4))}px`;
19772
+ this.tip = tip;
19773
+ }
19774
+ clear() {
19775
+ if (this.timer !== null) {
19776
+ clearTimeout(this.timer);
19777
+ this.timer = null;
19778
+ }
19779
+ this.tip?.remove();
19780
+ this.tip = null;
19781
+ this.current = null;
19782
+ }
19783
+ };
19784
+
19318
19785
  // src/renderers/native/chrome/CrosshairRenderer.ts
19319
19786
  var CrosshairRenderer = class {
19320
19787
  constructor() {
@@ -23446,17 +23913,17 @@ function glyphIcon(glyph) {
23446
23913
  return textGlyph(String(glyph), 15);
23447
23914
  }
23448
23915
  function stampSizeIcon(size) {
23449
- return textGlyph("\u25CF", (SIZE_PX4[String(size)] ?? 13) + 4);
23916
+ return textGlyph("\u25CF", (SIZE_PX3[String(size)] ?? 13) + 4);
23450
23917
  }
23451
23918
  function sizeIcon(size) {
23452
23919
  return textGlyph(String(size).charAt(0).toUpperCase(), 15);
23453
23920
  }
23454
- var SIZE_PX4 = { small: 10, normal: 13, large: 16, huge: 20 };
23921
+ var SIZE_PX3 = { small: 10, normal: 13, large: 16, huge: 20 };
23455
23922
  function numbersSizeIcon(size) {
23456
- return textGlyph("12", (SIZE_PX4[String(size)] ?? 13) - 1, 16.5, 'font-weight="600"');
23923
+ return textGlyph("12", (SIZE_PX3[String(size)] ?? 13) - 1, 16.5, 'font-weight="600"');
23457
23924
  }
23458
23925
  function labelSizeIcon(size) {
23459
- return textGlyph("T", (SIZE_PX4[String(size)] ?? 13) + 2);
23926
+ return textGlyph("T", (SIZE_PX3[String(size)] ?? 13) + 2);
23460
23927
  }
23461
23928
  function capitalize(s) {
23462
23929
  return s.charAt(0).toUpperCase() + s.slice(1);
@@ -23620,7 +24087,7 @@ var DrawingToolbar = class {
23620
24087
  this.root.replaceChildren();
23621
24088
  this.groupCells.clear();
23622
24089
  this.groupIcons.clear();
23623
- this.cursorBtn = this.makeButton(CURSOR_ICON, "Cursor", () => this.onArm(null));
24090
+ this.cursorBtn = this.makeButton(CURSOR_ICON, "Cursor", () => this.onCursorClick());
23624
24091
  this.root.appendChild(this.cursorBtn);
23625
24092
  if (this.def.groups.length > 0) this.root.appendChild(this.divider());
23626
24093
  for (const g of this.def.groups) {
@@ -23724,6 +24191,14 @@ var DrawingToolbar = class {
23724
24191
  this.magnetIcon = icon2;
23725
24192
  return cell;
23726
24193
  }
24194
+ /** Cursor returns to select/idle: an active measure/eraser mode exits through its own
24195
+ * toggle callback (disarming a tool via `onArm(null)` alone can't — the host treats a
24196
+ * null arm as a no-op side effect of entering those modes), then the tool disarms. */
24197
+ onCursorClick() {
24198
+ if (this.measureActive) this.onMeasure();
24199
+ if (this.eraserActive) this.onEraser();
24200
+ this.onArm(null);
24201
+ }
23727
24202
  /** Clicking the icon arms the group's last-used tool (it does NOT open the flyout). */
23728
24203
  onGroupIconClick(group) {
23729
24204
  const type = this.lastUsed.get(group.id) ?? group.tools[0]?.type;
@@ -25055,6 +25530,315 @@ var UserDrawingController = class {
25055
25530
  }
25056
25531
  };
25057
25532
 
25533
+ // src/renderers/shared/TableOverlay.ts
25534
+ var SIZE_PX4 = {
25535
+ auto: 13,
25536
+ tiny: 10,
25537
+ small: 11,
25538
+ normal: 13,
25539
+ large: 16,
25540
+ huge: 20
25541
+ };
25542
+ function fontPxOf(size) {
25543
+ if (typeof size === "number") return size > 0 ? size : SIZE_PX4.auto;
25544
+ return SIZE_PX4[size] ?? SIZE_PX4.auto;
25545
+ }
25546
+ function tableHasContent(t) {
25547
+ return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
25548
+ }
25549
+ function mergeRenderPlan(t) {
25550
+ const span = /* @__PURE__ */ new Map();
25551
+ const omit = /* @__PURE__ */ new Set();
25552
+ for (const m of t.merges) {
25553
+ span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
25554
+ for (let r = m.startRow; r <= m.endRow; r += 1) {
25555
+ for (let c = m.startCol; c <= m.endCol; c += 1) {
25556
+ if (r !== m.startRow || c !== m.startCol) omit.add(`${r}:${c}`);
25557
+ }
25558
+ }
25559
+ }
25560
+ for (let r = 0; r < t.rows; r += 1) {
25561
+ for (let c = 0; c < t.columns; c += 1) {
25562
+ if (t.cells[r]?.[c]?.merged && !span.has(`${r}:${c}`)) omit.add(`${r}:${c}`);
25563
+ }
25564
+ }
25565
+ for (const key of span.keys()) omit.delete(key);
25566
+ return { span, omit };
25567
+ }
25568
+
25569
+ // src/renderers/shared/TableCanvasRenderer.ts
25570
+ var PAD_X = 6;
25571
+ var PAD_Y = 2;
25572
+ var MARGIN = 6;
25573
+ var LINE_HEIGHT = 1.2;
25574
+ function paintTable(ctx, t, args, tips) {
25575
+ if (!tableHasContent(t)) return;
25576
+ const layout = layoutTable(ctx, t, args);
25577
+ if (!layout || layout.w <= 0 || layout.h <= 0) return;
25578
+ const fw = t.frameColor && t.frameWidth > 0 ? t.frameWidth : 0;
25579
+ const { x, y } = anchorOrigin(t.position, layout.w + 2 * fw, layout.h + 2 * fw, args);
25580
+ const x0 = x + fw;
25581
+ const y0 = y + fw;
25582
+ if (t.bgColor) {
25583
+ ctx.fillStyle = t.bgColor;
25584
+ ctx.fillRect(x0, y0, layout.w, layout.h);
25585
+ }
25586
+ if (fw > 0 && t.frameColor) {
25587
+ ctx.strokeStyle = t.frameColor;
25588
+ ctx.lineWidth = fw;
25589
+ ctx.strokeRect(x + fw / 2, y + fw / 2, layout.w + fw, layout.h + fw);
25590
+ }
25591
+ const colX = [0];
25592
+ for (const w of layout.colW) colX.push(colX[colX.length - 1] + w);
25593
+ const rowY = [0];
25594
+ for (const h of layout.rowH) rowY.push(rowY[rowY.length - 1] + h);
25595
+ const prevBaseline = ctx.textBaseline;
25596
+ const prevAlign = ctx.textAlign;
25597
+ ctx.textBaseline = "middle";
25598
+ for (const box of layout.boxes) {
25599
+ const rx = x0 + colX[box.c];
25600
+ const ry = y0 + rowY[box.r];
25601
+ const rw = colX[box.c + box.cs] - colX[box.c];
25602
+ const rh = rowY[box.r + box.rs] - rowY[box.r];
25603
+ const cell = box.cell;
25604
+ if (cell.bgColor) {
25605
+ ctx.fillStyle = cell.bgColor;
25606
+ ctx.fillRect(rx, ry, rw, rh);
25607
+ }
25608
+ const text = cell.text ?? "";
25609
+ if (text.length > 0) {
25610
+ const px = fontPxOf(cell.textSize);
25611
+ ctx.font = cellFont(cell, px, args.theme);
25612
+ ctx.fillStyle = cell.textColor ?? args.theme.textColor;
25613
+ const lines = text.split("\n");
25614
+ const blockH = lines.length * px * LINE_HEIGHT;
25615
+ const blockTop = cell.vAlign === "top" ? ry + PAD_Y : cell.vAlign === "bottom" ? ry + rh - PAD_Y - blockH : ry + (rh - blockH) / 2;
25616
+ const tx = cell.hAlign === "left" ? rx + PAD_X : cell.hAlign === "right" ? rx + rw - PAD_X : rx + rw / 2;
25617
+ ctx.textAlign = cell.hAlign;
25618
+ lines.forEach((line, i) => ctx.fillText(line, tx, blockTop + (i + 0.5) * px * LINE_HEIGHT));
25619
+ }
25620
+ if (cell.tooltip) tips.push({ left: rx, top: ry, right: rx + rw, bottom: ry + rh, text: cell.tooltip });
25621
+ }
25622
+ ctx.textBaseline = prevBaseline;
25623
+ ctx.textAlign = prevAlign;
25624
+ if (t.borderColor && t.borderWidth > 0) {
25625
+ ctx.strokeStyle = t.borderColor;
25626
+ ctx.lineWidth = t.borderWidth;
25627
+ const seen = /* @__PURE__ */ new Set();
25628
+ ctx.beginPath();
25629
+ const edge = (ax, ay, bx, by) => {
25630
+ const key = `${ax},${ay},${bx},${by}`;
25631
+ if (seen.has(key)) return;
25632
+ seen.add(key);
25633
+ ctx.moveTo(ax, ay);
25634
+ ctx.lineTo(bx, by);
25635
+ };
25636
+ for (const box of layout.boxes) {
25637
+ const l = Math.round(x0 + colX[box.c]);
25638
+ const r = Math.round(x0 + colX[box.c + box.cs]);
25639
+ const tp = Math.round(y0 + rowY[box.r]);
25640
+ const bt = Math.round(y0 + rowY[box.r + box.rs]);
25641
+ edge(l, tp, r, tp);
25642
+ edge(l, bt, r, bt);
25643
+ edge(l, tp, l, bt);
25644
+ edge(r, tp, r, bt);
25645
+ }
25646
+ ctx.stroke();
25647
+ }
25648
+ }
25649
+ function layoutTable(ctx, t, args) {
25650
+ const { span, omit } = mergeRenderPlan(t);
25651
+ const colW = new Array(t.columns).fill(0);
25652
+ const rowH = new Array(t.rows).fill(0);
25653
+ const boxes = [];
25654
+ for (let r = 0; r < t.rows; r += 1) {
25655
+ for (let c = 0; c < t.columns; c += 1) {
25656
+ if (omit.has(`${r}:${c}`)) continue;
25657
+ const cell = t.cells[r]?.[c];
25658
+ if (cell == null) continue;
25659
+ const sp = span.get(`${r}:${c}`);
25660
+ boxes.push({ cell, r, c, cs: Math.min(sp?.cs ?? 1, t.columns - c), rs: Math.min(sp?.rs ?? 1, t.rows - r) });
25661
+ }
25662
+ }
25663
+ if (boxes.length === 0) return null;
25664
+ const sizeOf = (cell) => {
25665
+ const px = fontPxOf(cell.textSize);
25666
+ ctx.font = cellFont(cell, px, args.theme);
25667
+ const lines = (cell.text ?? "").split("\n");
25668
+ let maxW = 0;
25669
+ for (const line of lines) maxW = Math.max(maxW, ctx.measureText(line).width);
25670
+ let w2 = Math.ceil(maxW) + 2 * PAD_X;
25671
+ let h2 = Math.ceil(lines.length * px * LINE_HEIGHT) + 2 * PAD_Y;
25672
+ if (cell.width) w2 = Math.max(w2, cell.width / 100 * args.plotWidth);
25673
+ if (cell.height) h2 = Math.max(h2, cell.height / 100 * args.paneHeight);
25674
+ return { w: w2, h: h2 };
25675
+ };
25676
+ const spanning = [];
25677
+ for (const box of boxes) {
25678
+ const { w: w2, h: h2 } = sizeOf(box.cell);
25679
+ if (box.cs === 1) colW[box.c] = Math.max(colW[box.c], w2);
25680
+ if (box.rs === 1) rowH[box.r] = Math.max(rowH[box.r], h2);
25681
+ if (box.cs > 1 || box.rs > 1) spanning.push({ box, w: w2, h: h2 });
25682
+ }
25683
+ for (const { box, w: w2, h: h2 } of spanning) {
25684
+ if (box.cs > 1) {
25685
+ let sum = 0;
25686
+ for (let c = box.c; c < box.c + box.cs; c += 1) sum += colW[c];
25687
+ if (w2 > sum) for (let c = box.c; c < box.c + box.cs; c += 1) colW[c] += (w2 - sum) / box.cs;
25688
+ }
25689
+ if (box.rs > 1) {
25690
+ let sum = 0;
25691
+ for (let r = box.r; r < box.r + box.rs; r += 1) sum += rowH[r];
25692
+ if (h2 > sum) for (let r = box.r; r < box.r + box.rs; r += 1) rowH[r] += (h2 - sum) / box.rs;
25693
+ }
25694
+ }
25695
+ let w = 0;
25696
+ for (const cw of colW) w += cw;
25697
+ let h = 0;
25698
+ for (const rh of rowH) h += rh;
25699
+ return { colW, rowH, w, h, boxes };
25700
+ }
25701
+ function anchorOrigin(position, totalW, totalH, args) {
25702
+ let y;
25703
+ if (position.startsWith("top")) y = MARGIN;
25704
+ else if (position.startsWith("bottom")) y = args.paneHeight - MARGIN - totalH;
25705
+ else y = args.paneHeight / 2 - totalH / 2;
25706
+ let x;
25707
+ if (position.endsWith("left")) x = MARGIN;
25708
+ else if (position.endsWith("right")) x = args.plotWidth - MARGIN - totalW;
25709
+ else x = args.plotWidth / 2 - totalW / 2;
25710
+ return { x, y };
25711
+ }
25712
+ function cellFont(cell, px, theme) {
25713
+ const family = cell.fontFamily === "monospace" ? "monospace" : theme.fontFamily || "sans-serif";
25714
+ return `${cell.italic ? "italic " : ""}${cell.bold ? "bold " : ""}${px}px ${family}`;
25715
+ }
25716
+
25717
+ // src/renderers/native/drawings/IndicatorDrawingSlices.ts
25718
+ function indicatorSliceKey(z, boundaries) {
25719
+ return boundaries.find((b) => b > z) ?? Infinity;
25720
+ }
25721
+ var IndicatorDrawingSlices = class {
25722
+ constructor() {
25723
+ this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
25724
+ /** Slice canvas cache, keyed `paneId|beforeZ` — same lifecycle as the user-drawing cache. */
25725
+ this.sliceCache = /* @__PURE__ */ new Map();
25726
+ /** Tooltip hit-rects of every label drawn this frame, in plot coords (rebuilt per prepare). */
25727
+ this.tips = [];
25728
+ }
25729
+ /**
25730
+ * Rebuild the per-indicator drawing slices for this data frame. `ref` is the data
25731
+ * canvas the slices must match pixel-for-pixel (the backend composites them 1:1).
25732
+ * Runs from the renderer's data paint, just before the backend composites the scene.
25733
+ */
25734
+ prepare(scene, coords, theme, ref) {
25735
+ this.tips = [];
25736
+ const out = /* @__PURE__ */ new Map();
25737
+ if (ref.width === 0 || ref.height === 0) {
25738
+ this.sliceCache.clear();
25739
+ return out;
25740
+ }
25741
+ this.drawScene.setDeps({
25742
+ timeToLogical: (ms) => coords.timeToLogical(ms),
25743
+ barAt: (logical) => {
25744
+ const b = scene.bars[Math.round(logical)];
25745
+ return b ? { high: b.high, low: b.low } : null;
25746
+ },
25747
+ theme
25748
+ });
25749
+ const dpr = coords.dpr;
25750
+ const dataW = coords.width;
25751
+ const buckets = /* @__PURE__ */ new Map();
25752
+ const add = (paneId, beforeZ, entry) => {
25753
+ const key = `${paneId}|${beforeZ}`;
25754
+ const bucket = buckets.get(key);
25755
+ if (bucket) bucket.entries.push(entry);
25756
+ else buckets.set(key, { paneId, beforeZ, entries: [entry] });
25757
+ };
25758
+ for (const pane of scene.orderedPanes()) {
25759
+ if (pane.collapsed) continue;
25760
+ const boundaries = scene.seriesBoundaries(pane.id);
25761
+ for (const m of scene.orderedIndicatorsForPane(pane.id)) {
25762
+ const set = modelDrawingSet(m, false);
25763
+ const tables = (m.tables ?? []).filter((t) => !t.overlay);
25764
+ if (drawingSetEmpty(set) && tables.length === 0) continue;
25765
+ const sc = scene.scaleFor(m, pane);
25766
+ const mp = sc === pane.scale ? pane : { ...pane, scale: sc };
25767
+ const beforeZ = indicatorSliceKey(scene.zOf(m.id), boundaries);
25768
+ add(pane.id, beforeZ, { set, tables, pane: mp, indexOffset: scene.offsetOf(m.id) });
25769
+ }
25770
+ if (pane.kind === "price") {
25771
+ for (const m of scene.indicators.values()) {
25772
+ const set = modelDrawingSet(m, true);
25773
+ const tables = (m.tables ?? []).filter((t) => t.overlay === true);
25774
+ if (drawingSetEmpty(set) && tables.length === 0) continue;
25775
+ add(pane.id, Infinity, { set, tables, pane, indexOffset: scene.offsetOf(m.id) });
25776
+ }
25777
+ }
25778
+ }
25779
+ for (const [key, { paneId, beforeZ, entries }] of buckets) {
25780
+ let canvas = this.sliceCache.get(key);
25781
+ if (!canvas) {
25782
+ canvas = document.createElement("canvas");
25783
+ this.sliceCache.set(key, canvas);
25784
+ }
25785
+ if (canvas.width !== ref.width || canvas.height !== ref.height) {
25786
+ canvas.width = ref.width;
25787
+ canvas.height = ref.height;
25788
+ }
25789
+ const ctx = canvas.getContext("2d");
25790
+ if (!ctx) continue;
25791
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
25792
+ ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
25793
+ for (const e of entries) this.paintEntry(ctx, e, coords, dataW, theme);
25794
+ const slices = out.get(paneId) ?? [];
25795
+ slices.push({ beforeZ, canvas });
25796
+ out.set(paneId, slices);
25797
+ }
25798
+ for (const key of [...this.sliceCache.keys()]) if (!buckets.has(key)) this.sliceCache.delete(key);
25799
+ for (const slices of out.values()) slices.sort((a, b) => a.beforeZ - b.beforeZ);
25800
+ return out;
25801
+ }
25802
+ paintEntry(ctx, e, coords, dataW, theme) {
25803
+ const { pane } = e;
25804
+ const paneTips = [];
25805
+ ctx.save();
25806
+ ctx.translate(0, pane.bounds.top);
25807
+ ctx.beginPath();
25808
+ ctx.rect(0, 0, dataW, pane.bounds.height);
25809
+ ctx.clip();
25810
+ this.drawScene.setSet(e.set, e.indexOffset);
25811
+ this.drawScene.render(
25812
+ ctx,
25813
+ dataW,
25814
+ pane.bounds.height,
25815
+ (l) => coords.logicalToX(l),
25816
+ (price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
25817
+ );
25818
+ paneTips.push(...this.drawScene.labelTipRegions());
25819
+ for (const t of e.tables) paintTable(ctx, t, { paneHeight: pane.bounds.height, plotWidth: dataW, theme }, paneTips);
25820
+ ctx.restore();
25821
+ for (const r of paneTips) {
25822
+ this.tips.push({ ...r, top: r.top + pane.bounds.top, bottom: r.bottom + pane.bounds.top });
25823
+ }
25824
+ }
25825
+ /** Tooltip of the topmost label or table cell under a plot-space point, or null. Fed by the last prepare. */
25826
+ labelTooltipAt(x, y) {
25827
+ for (let i = this.tips.length - 1; i >= 0; i -= 1) {
25828
+ const r = this.tips[i];
25829
+ if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return r.text;
25830
+ }
25831
+ return null;
25832
+ }
25833
+ };
25834
+ function mergeSlices(indicator, user) {
25835
+ const out = /* @__PURE__ */ new Map();
25836
+ for (const [paneId, slices] of indicator) out.set(paneId, [...slices]);
25837
+ for (const [paneId, slices] of user) out.set(paneId, [...out.get(paneId) ?? [], ...slices]);
25838
+ for (const slices of out.values()) slices.sort((a, b) => a.beforeZ - b.beforeZ);
25839
+ return out;
25840
+ }
25841
+
25058
25842
  // src/renderers/native/drawings/Projector.ts
25059
25843
  function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
25060
25844
  return {
@@ -25106,19 +25890,8 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
25106
25890
  for (const model of models) {
25107
25891
  const off = offsetOf(model.id);
25108
25892
  for (const s of model.series) {
25109
- if (s.kind === "candle" || s.kind === "bar") {
25110
- for (let i = i0; i <= i1; i += 1) {
25111
- const b = s.bars[i - off];
25112
- if (b) {
25113
- consider(b.high);
25114
- consider(b.low);
25115
- }
25116
- }
25117
- } else if (isLineLikeSeries(s)) {
25118
- for (let i = i0; i <= i1; i += 1) consider(s.points[i - off]?.value);
25119
- if (s.kind === "histogram" || s.kind === "columns") consider(s.style?.base ?? 0);
25120
- else if (s.style?.base != null) consider(s.style.base);
25121
- }
25893
+ if (s.overlay === true) continue;
25894
+ considerSeries(s, i0, i1, off, consider);
25122
25895
  }
25123
25896
  for (const pl of model.priceLines) consider(pl.price);
25124
25897
  }
@@ -25140,6 +25913,36 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
25140
25913
  const span = max - min;
25141
25914
  return { min: min - span * MARGIN_BOTTOM, max: max + span * MARGIN_TOP };
25142
25915
  }
25916
+ function considerSeries(s, i0, i1, off, consider) {
25917
+ if (s.kind === "candle" || s.kind === "bar") {
25918
+ for (let i = i0; i <= i1; i += 1) {
25919
+ const b = s.bars[i - off];
25920
+ if (b) {
25921
+ consider(b.high);
25922
+ consider(b.low);
25923
+ }
25924
+ }
25925
+ } else if (isLineLikeSeries(s)) {
25926
+ for (let i = i0; i <= i1; i += 1) consider(s.points[i - off]?.value);
25927
+ if (s.kind === "histogram" || s.kind === "columns") consider(s.style?.base ?? 0);
25928
+ else if (s.style?.base != null) consider(s.style.base);
25929
+ }
25930
+ }
25931
+ function overlaySeriesRange(models, i0, i1, offsetOf = () => 0) {
25932
+ let min = Infinity;
25933
+ let max = -Infinity;
25934
+ const consider = (v) => {
25935
+ if (v != null && Number.isFinite(v)) {
25936
+ if (v < min) min = v;
25937
+ if (v > max) max = v;
25938
+ }
25939
+ };
25940
+ for (const model of models) {
25941
+ const off = offsetOf(model.id);
25942
+ for (const s of model.series) if (s.overlay === true) considerSeries(s, i0, i1, off, consider);
25943
+ }
25944
+ return min === Infinity ? null : { min, max };
25945
+ }
25143
25946
  function expandScaleByPixels(scale, heightPx, abovePx, belowPx) {
25144
25947
  if (abovePx <= 0 && belowPx <= 0) return scale;
25145
25948
  const content = heightPx - abovePx - belowPx;
@@ -25915,6 +26718,11 @@ var NativeRenderer = class {
25915
26718
  this.vpvrRenderer = new VpvrRenderer();
25916
26719
  this.resizeObserver = null;
25917
26720
  this.dprMedia = null;
26721
+ /** Plot size in INTEGER device px, as last reported by the resize observer's
26722
+ * device-pixel-content-box — the browser's own statement of how many device pixels
26723
+ * it paints the plot into. `null` until the first report or where the box type is
26724
+ * unsupported (WebKit); syncSize then falls back to rounding the client rect. */
26725
+ this.plotDeviceSize = null;
25918
26726
  this.coords = new CoordinateSystem();
25919
26727
  this.scene = new SceneGraph();
25920
26728
  // chosen at mount (WebGL2 if available, else canvas2d)
@@ -25922,6 +26730,10 @@ var NativeRenderer = class {
25922
26730
  this.glowAmount = 0;
25923
26731
  // WebGL2 neon-glow intensity (canvas2d ignores it)
25924
26732
  this.chrome = new ChromeRenderer();
26733
+ /** Prepaints each indicator's Pine drawings into interleave slices at the model's z. */
26734
+ this.indicatorSlices = new IndicatorDrawingSlices();
26735
+ /** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
26736
+ this.labelTooltip = null;
25925
26737
  this.crosshairLayer = new CrosshairRenderer();
25926
26738
  /** 1 Hz repaint pump so the price-axis countdown-to-bar-close ticks; null when off. */
25927
26739
  this.countdownTimer = null;
@@ -25932,6 +26744,8 @@ var NativeRenderer = class {
25932
26744
  this.indicatorValuesOn = true;
25933
26745
  /** Host-contributed legend actions — held here so a rebuild of the legend re-wires them. */
25934
26746
  this.legendActionsProvider = null;
26747
+ /** Host-contributed legend callouts — held here so a rebuild of the legend re-wires them. */
26748
+ this.legendCalloutsProvider = null;
25935
26749
  /** Host override of the legend's fold toggle — held here so a remount re-applies it. */
25936
26750
  this.legendOverviewAction = null;
25937
26751
  // ── keyboard navigation / accessibility (item 11) ──
@@ -26066,7 +26880,6 @@ var NativeRenderer = class {
26066
26880
  this.toggleVisibleCbs = /* @__PURE__ */ new Set();
26067
26881
  this.moveIndicatorCbs = /* @__PURE__ */ new Set();
26068
26882
  this.priceStyleCbs = /* @__PURE__ */ new Set();
26069
- this.tableOverlays = /* @__PURE__ */ new Map();
26070
26883
  this.name = "native";
26071
26884
  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"];
26072
26885
  /** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
@@ -26380,7 +27193,6 @@ var NativeRenderer = class {
26380
27193
  * hidden) on clear so a re-show picks up the current theme.
26381
27194
  */
26382
27195
  setLoading(loading) {
26383
- for (const overlay of this.tableOverlays.values()) overlay.setVisible(!loading);
26384
27196
  if (!loading || !this.wrapper) {
26385
27197
  this.loadingEl?.remove();
26386
27198
  this.loadingEl = null;
@@ -27040,6 +27852,10 @@ var NativeRenderer = class {
27040
27852
  this.plot.appendChild(this.scrollButton);
27041
27853
  this.plot.addEventListener("pointermove", this.onScrollProximityMove);
27042
27854
  this.plot.addEventListener("pointerleave", this.onScrollProximityLeave);
27855
+ this.labelTooltip = new LabelTooltip(this.plot, {
27856
+ theme: () => this.chromeTheme(),
27857
+ lookup: (x, y) => this.indicatorSlices.labelTooltipAt(x, y)
27858
+ });
27043
27859
  this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
27044
27860
  projector: () => this.drawingProjector(),
27045
27861
  dpr: () => this.coords.dpr,
@@ -27079,9 +27895,10 @@ var NativeRenderer = class {
27079
27895
  this.inputsUI.setDialogHost(this.dialogHost);
27080
27896
  this.inputsUI.setSymbolPicker(this.symbolPicker);
27081
27897
  this.inputsUI.setLegendActions(this.legendActionsProvider);
27898
+ this.inputsUI.setLegendCallouts(this.legendCalloutsProvider);
27082
27899
  this.inputsUI.setLegendOverviewAction(this.legendOverviewAction);
27083
27900
  this.inputsUI.setOnChange((c) => {
27084
- for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value });
27901
+ for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value, ...c.kind ? { kind: c.kind } : {} });
27085
27902
  });
27086
27903
  this.inputsUI.setOnRemove((id) => {
27087
27904
  for (const cb of this.removeIndicatorCbs) cb(id);
@@ -27122,6 +27939,7 @@ var NativeRenderer = class {
27122
27939
  this.emitPaneAction({ type: "maximize", paneId, maximized });
27123
27940
  }
27124
27941
  });
27942
+ this.paneControls.setSuspended(this.layoutMode === "mobile");
27125
27943
  this.axisScaleButtons = new AxisScaleButtons(this.plot, theme, {
27126
27944
  panes: () => this.axisScaleViews(),
27127
27945
  rightAxis: () => this.rightAxisW,
@@ -27131,8 +27949,19 @@ var NativeRenderer = class {
27131
27949
  if (pane) this.setPaneLog(paneId, !paneLogScale(this.scene, pane));
27132
27950
  }
27133
27951
  });
27134
- this.resizeObserver = new ResizeObserver(() => this.resize());
27952
+ this.resizeObserver = new ResizeObserver((entries) => {
27953
+ for (const e of entries) {
27954
+ if (e.target !== this.plot) continue;
27955
+ const s = e.devicePixelContentBoxSize?.[0];
27956
+ if (s) this.plotDeviceSize = { width: s.inlineSize, height: s.blockSize };
27957
+ }
27958
+ this.resize();
27959
+ });
27135
27960
  this.resizeObserver.observe(this.wrapper);
27961
+ try {
27962
+ this.resizeObserver.observe(this.plot, { box: "device-pixel-content-box" });
27963
+ } catch {
27964
+ }
27136
27965
  this.watchDpr();
27137
27966
  this.syncSize();
27138
27967
  }
@@ -27257,8 +28086,6 @@ var NativeRenderer = class {
27257
28086
  this.inputsUI?.destroy();
27258
28087
  this.paneControls?.destroy();
27259
28088
  this.axisScaleButtons?.destroy();
27260
- for (const overlay of this.tableOverlays.values()) overlay.destroy();
27261
- this.tableOverlays.clear();
27262
28089
  this.resizeObserver?.disconnect();
27263
28090
  this.resizeObserver = null;
27264
28091
  this.dprMedia?.removeEventListener("change", this.onDprChange);
@@ -27271,6 +28098,8 @@ var NativeRenderer = class {
27271
28098
  this.settingsButton = null;
27272
28099
  this.plot?.removeEventListener("pointermove", this.onScrollProximityMove);
27273
28100
  this.plot?.removeEventListener("pointerleave", this.onScrollProximityLeave);
28101
+ this.labelTooltip?.destroy();
28102
+ this.labelTooltip = null;
27274
28103
  this.scrollButton?.remove();
27275
28104
  this.scrollButton = null;
27276
28105
  for (const l of this.extLayers) l.instance.destroy?.();
@@ -27285,6 +28114,7 @@ var NativeRenderer = class {
27285
28114
  this.attributionEl = null;
27286
28115
  this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
27287
28116
  this.mountContainer?.style.removeProperty("--vela-scale-gutter");
28117
+ this.mountContainer?.style.removeProperty("--vela-bottom-gutter");
27288
28118
  this.mountContainer?.style.removeProperty("--vela-price-pane-top");
27289
28119
  this.mountContainer?.style.removeProperty("--vela-price-pane-bottom");
27290
28120
  this.mountContainer = null;
@@ -27377,7 +28207,6 @@ var NativeRenderer = class {
27377
28207
  ensurePane(pane) {
27378
28208
  this.scene.ensurePane(pane.id, pane.kind, pane.order, pane.heightWeight ?? (pane.kind === "price" ? 3 : 1));
27379
28209
  this.layoutPanes();
27380
- this.repositionTables();
27381
28210
  this.paneControls?.refresh();
27382
28211
  this.scheduler.invalidate(4 /* Full */);
27383
28212
  }
@@ -27399,17 +28228,14 @@ var NativeRenderer = class {
27399
28228
  if (!model.ownScale) this.scene.dropIndicatorScale(handle.id);
27400
28229
  this.inputsUI.setPane(handle.id, paneId);
27401
28230
  this.refreshAnchorOffset(model);
27402
- this.syncTables(model);
27403
28231
  this.refreshAxisWidth();
27404
28232
  this.layoutPanes();
27405
- this.repositionTables();
27406
28233
  this.paneControls?.refresh();
27407
28234
  this.scheduler.invalidate(4 /* Full */);
27408
28235
  }
27409
28236
  orderPanes(orderedIds) {
27410
28237
  this.scene.orderPanes(orderedIds);
27411
28238
  this.layoutPanes();
27412
- this.repositionTables();
27413
28239
  this.paneControls?.refresh();
27414
28240
  this.scheduler.invalidate(4 /* Full */);
27415
28241
  }
@@ -27418,7 +28244,6 @@ var NativeRenderer = class {
27418
28244
  if (!pane || pane.collapsed === collapsed) return;
27419
28245
  pane.collapsed = collapsed;
27420
28246
  this.layoutPanes();
27421
- this.repositionTables();
27422
28247
  this.paneControls?.refresh();
27423
28248
  this.scheduler.invalidate(4 /* Full */);
27424
28249
  }
@@ -27426,7 +28251,6 @@ var NativeRenderer = class {
27426
28251
  if (paneId !== null && !this.scene.panes.has(paneId)) paneId = null;
27427
28252
  this.maximizedPaneId = paneId;
27428
28253
  this.layoutPanes();
27429
- this.repositionTables();
27430
28254
  this.paneControls?.refresh();
27431
28255
  this.scheduler.invalidate(4 /* Full */);
27432
28256
  }
@@ -27511,9 +28335,8 @@ var NativeRenderer = class {
27511
28335
  else this.scene.assignIndicatorZ(model.id);
27512
28336
  this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
27513
28337
  native: !!model.native,
27514
- ...model.shorttitle ? { settingsTitle: model.title } : {}
28338
+ ...model.props ? { props: model.props, propValues: model.propValues ?? {} } : {}
27515
28339
  });
27516
- this.syncTables(model);
27517
28340
  if (model.native?.type === "volume") {
27518
28341
  this.volumeActive = true;
27519
28342
  this.volumeHidden = false;
@@ -27537,7 +28360,6 @@ var NativeRenderer = class {
27537
28360
  }
27538
28361
  }
27539
28362
  applyPatch(model, patch);
27540
- this.syncTables(model);
27541
28363
  this.scheduler.invalidate(3 /* Light */);
27542
28364
  }
27543
28365
  removeIndicator(handle) {
@@ -27556,14 +28378,12 @@ var NativeRenderer = class {
27556
28378
  this.scene.forgetAnchorOffset(handle.id);
27557
28379
  this.scene.dropIndicatorScale(handle.id);
27558
28380
  this.inputsUI.remove(handle.id);
27559
- this.tableOverlays.get(handle.id)?.destroy();
27560
- this.tableOverlays.delete(handle.id);
27561
28381
  this.refreshAxisWidth();
27562
28382
  this.paneControls?.refresh();
27563
28383
  this.scheduler.invalidate(4 /* Full */);
27564
28384
  }
27565
- setIndicatorInputs(handle, values) {
27566
- this.inputsUI.setValues(handle.id, values);
28385
+ setIndicatorInputs(handle, values, props) {
28386
+ this.inputsUI.setValues(handle.id, values, props);
27567
28387
  }
27568
28388
  setSymbolPicker(picker) {
27569
28389
  this.symbolPicker = picker;
@@ -27573,6 +28393,10 @@ var NativeRenderer = class {
27573
28393
  this.legendActionsProvider = provider;
27574
28394
  this.inputsUI?.setLegendActions(provider);
27575
28395
  }
28396
+ setLegendCallouts(provider) {
28397
+ this.legendCalloutsProvider = provider;
28398
+ this.inputsUI?.setLegendCallouts(provider);
28399
+ }
27576
28400
  setLegendOverviewAction(action) {
27577
28401
  this.legendOverviewAction = action;
27578
28402
  this.inputsUI?.setLegendOverviewAction(action);
@@ -27597,8 +28421,6 @@ var NativeRenderer = class {
27597
28421
  }
27598
28422
  if (!visible) {
27599
28423
  this.scene.indicators.delete(handle.id);
27600
- this.tableOverlays.get(handle.id)?.destroy();
27601
- this.tableOverlays.delete(handle.id);
27602
28424
  }
27603
28425
  this.inputsUI.setVisible(handle.id, visible);
27604
28426
  this.scheduler.invalidate(4 /* Full */);
@@ -27644,6 +28466,7 @@ var NativeRenderer = class {
27644
28466
  this.userDrawings?.setLayoutMode(mode);
27645
28467
  this.settingsDialog?.setLayoutMode(mode);
27646
28468
  this.inputsUI?.setLayoutMode(mode);
28469
+ this.paneControls?.setSuspended(mode === "mobile");
27647
28470
  if (this.scrollButton) {
27648
28471
  const px = mode === "mobile" ? SCROLL_BTN_SIZE_TOUCH : SCROLL_BTN_SIZE;
27649
28472
  this.scrollButton.style.width = `${px}px`;
@@ -28039,7 +28862,6 @@ var NativeRenderer = class {
28039
28862
  /** Relayout + repaint + refresh the hover buttons after a collapse/maximize/order change. */
28040
28863
  afterPaneLayoutChange() {
28041
28864
  this.layoutPanes();
28042
- this.repositionTables();
28043
28865
  this.paneControls?.refresh();
28044
28866
  this.scheduler.invalidate(4 /* Full */);
28045
28867
  }
@@ -28120,7 +28942,6 @@ var NativeRenderer = class {
28120
28942
  above.heightWeight = next.above;
28121
28943
  below.heightWeight = next.below;
28122
28944
  this.layoutPanes();
28123
- this.repositionTables();
28124
28945
  this.scheduler.invalidate(4 /* Full */);
28125
28946
  }
28126
28947
  /** Double-click a separator → split the two adjacent panes evenly (each gets half of
@@ -28135,7 +28956,6 @@ var NativeRenderer = class {
28135
28956
  above.heightWeight = half;
28136
28957
  below.heightWeight = half;
28137
28958
  this.layoutPanes();
28138
- this.repositionTables();
28139
28959
  this.scheduler.invalidate(4 /* Full */);
28140
28960
  }
28141
28961
  // ── keyboard navigation / accessibility (item 11) ──
@@ -28414,7 +29234,10 @@ var NativeRenderer = class {
28414
29234
  const liveActual = li >= 0 ? this.bars[li] : void 0;
28415
29235
  const easeLive = !!liveActual && this.liveEaseTime === liveActual.time && (liveActual.high !== this.liveEaseHigh || liveActual.low !== this.liveEaseLow || liveActual.close !== this.liveEaseClose);
28416
29236
  if (easeLive && liveActual) this.bars[li] = { ...liveActual, high: this.liveEaseHigh, low: this.liveEaseLow, close: this.liveEaseClose };
28417
- this.scene.drawingSlices = this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map();
29237
+ this.scene.drawingSlices = mergeSlices(
29238
+ this.indicatorSlices.prepare(this.scene, this.coords, this.theme, this.dataCanvas),
29239
+ this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map()
29240
+ );
28418
29241
  this.backdropRenderer.render(this.scene, this.coords, this.theme, gridAlpha);
28419
29242
  this.backend.render(this.scene, this.coords, this.theme);
28420
29243
  this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
@@ -28553,7 +29376,11 @@ var NativeRenderer = class {
28553
29376
  }
28554
29377
  const models = this.scene.indicatorsForPane(pane.id);
28555
29378
  const masterModels = models.filter((m) => m.ownScale !== true);
28556
- const dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
29379
+ let dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
29380
+ if (pane === pricePane) {
29381
+ const or = overlaySeriesRange(this.scene.indicators.values(), i0, i1, (id) => this.scene.offsetOf(id));
29382
+ if (or) dr = dr ? { min: Math.min(dr.min, or.min), max: Math.max(dr.max, or.max) } : or;
29383
+ }
28557
29384
  const includeCandles = pane.kind === "price" && !this.scene.candlesHidden;
28558
29385
  pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
28559
29386
  pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
@@ -28827,6 +29654,7 @@ var NativeRenderer = class {
28827
29654
  for (const m of models) {
28828
29655
  const off = this.scene.offsetOf(m.id);
28829
29656
  for (const s of m.series) {
29657
+ if (s.overlay === true) continue;
28830
29658
  if (isLineLikeSeries(s)) {
28831
29659
  const v = s.points[i0 - off]?.value;
28832
29660
  if (v != null && Number.isFinite(v)) return v;
@@ -28847,27 +29675,6 @@ var NativeRenderer = class {
28847
29675
  }
28848
29676
  return maxVol;
28849
29677
  }
28850
- /** Create/update/destroy an indicator's DOM table overlay (anchored off real pane geometry). */
28851
- syncTables(model) {
28852
- const tables = model.tables ?? [];
28853
- let overlay = this.tableOverlays.get(model.id);
28854
- if (tables.length === 0) {
28855
- if (overlay) {
28856
- overlay.destroy();
28857
- this.tableOverlays.delete(model.id);
28858
- }
28859
- return;
28860
- }
28861
- if (!overlay) {
28862
- overlay = new TableOverlay(this.plot, this.theme, (id) => this.paneBoundsFor(id));
28863
- overlay.setVisible(this.loadingEl === null);
28864
- this.tableOverlays.set(model.id, overlay);
28865
- }
28866
- overlay.update(tables);
28867
- }
28868
- repositionTables() {
28869
- for (const overlay of this.tableOverlays.values()) overlay.reposition();
28870
- }
28871
29678
  layoutPanes() {
28872
29679
  const panes = this.scene.orderedPanes();
28873
29680
  const dataHeight = this.coords.height;
@@ -28918,6 +29725,7 @@ var NativeRenderer = class {
28918
29725
  const visible = maxPane ? [maxPane] : this.scene.orderedPanes().filter((p) => !p.collapsed);
28919
29726
  const paneBottom = visible.length ? Math.max(...visible.map((p) => p.bounds.top + p.bounds.height)) : dataHeight;
28920
29727
  this.scrollBtnBottomPx = SCROLL_BTN_BOTTOM + Math.max(0, dataHeight - paneBottom);
29728
+ this.mountContainer?.style.setProperty("--vela-bottom-gutter", `${TIME_AXIS_H + Math.max(0, dataHeight - paneBottom)}px`);
28921
29729
  this.scrollBtnRightPx = this.rightAxisW + SCROLL_BTN_RIGHT_INSET;
28922
29730
  if (this.scrollButton) {
28923
29731
  this.scrollButton.style.bottom = `${this.scrollBtnBottomPx}px`;
@@ -29010,30 +29818,33 @@ var NativeRenderer = class {
29010
29818
  if (w <= 0 || h <= 0) return;
29011
29819
  const dpr = window.devicePixelRatio || 1;
29012
29820
  this.plot.style.left = `${this.toolbarGutter}px`;
29013
- const pw = Math.max(1, w - this.toolbarGutter);
29014
- const ph = h;
29015
- this.dataCanvas.width = Math.round(pw * dpr);
29016
- this.dataCanvas.height = Math.round(ph * dpr);
29017
- this.backdropCanvas.width = this.dataCanvas.width;
29018
- this.backdropCanvas.height = this.dataCanvas.height;
29019
- this.volumeCanvas.width = this.dataCanvas.width;
29020
- this.volumeCanvas.height = this.dataCanvas.height;
29021
- for (const l of this.extLayers) {
29022
- l.canvas.width = this.dataCanvas.width;
29023
- l.canvas.height = this.dataCanvas.height;
29024
- }
29025
- this.vpvrCanvas.width = this.dataCanvas.width;
29026
- this.vpvrCanvas.height = this.dataCanvas.height;
29027
- this.chromeCanvas.width = this.dataCanvas.width;
29028
- this.chromeCanvas.height = this.dataCanvas.height;
29029
- this.drawingsCanvas.width = this.dataCanvas.width;
29030
- this.drawingsCanvas.height = this.dataCanvas.height;
29031
- this.cursorCanvas.width = this.dataCanvas.width;
29032
- this.cursorCanvas.height = this.dataCanvas.height;
29821
+ const rect = this.plot.getBoundingClientRect();
29822
+ let bw = Math.max(1, Math.round(rect.width * dpr));
29823
+ let bh = Math.max(1, Math.round(rect.height * dpr));
29824
+ const dev = this.plotDeviceSize;
29825
+ if (dev && Math.abs(dev.width - rect.width * dpr) <= 1 && Math.abs(dev.height - rect.height * dpr) <= 1) {
29826
+ bw = Math.max(1, dev.width);
29827
+ bh = Math.max(1, dev.height);
29828
+ }
29829
+ const pw = bw / dpr;
29830
+ const ph = bh / dpr;
29831
+ const size = (canvas) => {
29832
+ canvas.width = bw;
29833
+ canvas.height = bh;
29834
+ canvas.style.width = `${pw}px`;
29835
+ canvas.style.height = `${ph}px`;
29836
+ };
29837
+ size(this.dataCanvas);
29838
+ size(this.backdropCanvas);
29839
+ size(this.volumeCanvas);
29840
+ for (const l of this.extLayers) size(l.canvas);
29841
+ size(this.vpvrCanvas);
29842
+ size(this.chromeCanvas);
29843
+ size(this.drawingsCanvas);
29844
+ size(this.cursorCanvas);
29033
29845
  this.coords.setSize(Math.max(1, pw - this.rightAxisW), Math.max(1, ph - TIME_AXIS_H), dpr);
29034
29846
  this.scene.crosshair = null;
29035
29847
  this.layoutPanes();
29036
- this.repositionTables();
29037
29848
  this.userDrawings?.onResize();
29038
29849
  if (!this.didInitialFit && this.coords.barCount > 0) {
29039
29850
  this.fitContent();
@@ -29732,6 +30543,19 @@ function unregisterLegendAction(id) {
29732
30543
  function legendActions() {
29733
30544
  return [...legendRegistry.values()].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
29734
30545
  }
30546
+ var calloutRegistry = /* @__PURE__ */ new Map();
30547
+ function registerLegendCallout(desc) {
30548
+ calloutRegistry.set(desc.id, desc);
30549
+ return () => {
30550
+ if (calloutRegistry.get(desc.id) === desc) calloutRegistry.delete(desc.id);
30551
+ };
30552
+ }
30553
+ function unregisterLegendCallout(id) {
30554
+ calloutRegistry.delete(id);
30555
+ }
30556
+ function legendCallouts() {
30557
+ return [...calloutRegistry.values()].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
30558
+ }
29735
30559
  var stateHandlers = /* @__PURE__ */ new Map();
29736
30560
  function registerStatePersistence(handler) {
29737
30561
  stateHandlers.set(handler.key, handler);
@@ -29840,6 +30664,7 @@ exports.getNativeIndicator = getNativeIndicator;
29840
30664
  exports.iconMarkup = iconMarkup;
29841
30665
  exports.inputVisible = inputVisible;
29842
30666
  exports.legendActions = legendActions;
30667
+ exports.legendCallouts = legendCallouts;
29843
30668
  exports.nativeIndicatorDescriptors = nativeIndicatorDescriptors;
29844
30669
  exports.nativeIndicatorTypes = nativeIndicatorTypes;
29845
30670
  exports.normalizeSettingsRow = normalizeSettingsRow;
@@ -29848,6 +30673,7 @@ exports.registerDefaultEngine = registerDefaultEngine;
29848
30673
  exports.registerDrawingType = registerDrawingType;
29849
30674
  exports.registerIcon = registerIcon;
29850
30675
  exports.registerLegendAction = registerLegendAction;
30676
+ exports.registerLegendCallout = registerLegendCallout;
29851
30677
  exports.registerNativeIndicator = registerNativeIndicator;
29852
30678
  exports.registerRendererDefaults = registerRendererDefaults;
29853
30679
  exports.registerRendererLayer = registerRendererLayer;
@@ -29872,6 +30698,7 @@ exports.topbarActionOverride = topbarActionOverride;
29872
30698
  exports.unregisterChartType = unregisterChartType;
29873
30699
  exports.unregisterDefaultEngine = unregisterDefaultEngine;
29874
30700
  exports.unregisterLegendAction = unregisterLegendAction;
30701
+ exports.unregisterLegendCallout = unregisterLegendCallout;
29875
30702
  exports.unregisterNativeIndicator = unregisterNativeIndicator;
29876
30703
  exports.unregisterRendererDefaults = unregisterRendererDefaults;
29877
30704
  exports.unregisterRendererLayer = unregisterRendererLayer;