@luxalgo/vela 0.6.8 → 0.6.9

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-BsQM2WNH.d.cts → DataProvider-8Z95Q-RJ.d.cts} +1 -1
  2. package/dist/{DataProvider-BSLlBpB9.d.ts → DataProvider-DExJrfut.d.ts} +1 -1
  3. package/dist/{chunk-2R7BANEM.js → chunk-62SVGONC.js} +2 -2
  4. package/dist/{chunk-SQETIBFU.js → chunk-6WDDVMBJ.js} +502 -139
  5. package/dist/{chunk-L3I2CCYO.js → chunk-EQCHJZOT.js} +49 -2
  6. package/dist/{chunk-OICPLNU7.js → chunk-FCVIG7JG.js} +188 -2
  7. package/dist/{chunk-N7LGMKCE.js → chunk-STHSKXOR.js} +107 -84
  8. package/dist/{chunk-PN5KWFZ4.js → chunk-WFSZBX3R.js} +2 -2
  9. package/dist/{contributions-Vw-Hm58R.d.cts → contributions-C1U2Krwg.d.cts} +99 -7
  10. package/dist/{contributions-DLLdV9jD.d.ts → contributions-D7PVZO2i.d.ts} +99 -7
  11. package/dist/index.cjs +709 -144
  12. package/dist/index.d.cts +12 -7
  13. package/dist/index.d.ts +12 -7
  14. package/dist/index.js +4 -4
  15. package/dist/{options-BaVTMXaO.d.ts → options-FM0peknS.d.cts} +84 -7
  16. package/dist/{options-BaVTMXaO.d.cts → options-FM0peknS.d.ts} +84 -7
  17. package/dist/{plugin-CC7rBrOY.d.ts → plugin-7bkF32Rk.d.ts} +3 -3
  18. package/dist/{plugin-BnJgjLAy.d.cts → plugin-DfVqBz9p.d.cts} +3 -3
  19. package/dist/plugin.cjs +18 -2
  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-DqzBqy42.d.cts → statusline-DOPiT6I6.d.cts} +5 -3
  30. package/dist/{statusline-CGkB2EOo.d.ts → statusline-zdF4eZLr.d.ts} +5 -3
  31. package/dist/ui.cjs +201 -4
  32. package/dist/ui.d.cts +89 -2
  33. package/dist/ui.d.ts +89 -2
  34. package/dist/ui.js +3 -3
  35. package/dist/vela.global.js +709 -144
  36. package/dist/vela.global.min.js +93 -44
  37. package/dist/widget.cjs +798 -188
  38. package/dist/widget.d.cts +6 -6
  39. package/dist/widget.d.ts +6 -6
  40. package/dist/widget.js +7 -7
  41. package/dist/workspace.cjs +798 -188
  42. package/dist/workspace.d.cts +16 -6
  43. package/dist/workspace.d.ts +16 -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(
@@ -7342,7 +7359,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
7342
7359
  for (const engine of engines) this.registerEngine(engine.language, engine);
7343
7360
  this.defaultLanguage = config.defaultLanguage;
7344
7361
  this.renderer.mount(container, config.theme);
7345
- this.renderer.onInputChange((e) => this.applyInputs(e.indicatorId, { [e.key]: e.value }));
7362
+ this.renderer.onInputChange(
7363
+ (e) => e.kind === "prop" ? this.applyProps(e.indicatorId, { [e.key]: e.value }) : this.applyInputs(e.indicatorId, { [e.key]: e.value })
7364
+ );
7346
7365
  this.renderer.onRemoveIndicator((id) => this.removeIndicator(id));
7347
7366
  this.renderer.onToggleIndicatorVisible?.((id, visible) => this.setVisible(id, visible));
7348
7367
  this.paneActionUnsub = this.renderer.onPaneAction?.((a) => this.handlePaneAction(a)) ?? null;
@@ -8022,7 +8041,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
8022
8041
  const title = options.title ?? "Indicator";
8023
8042
  const handle = new IndicatorHandleImpl(id, title, this, source);
8024
8043
  this.handles.set(id, handle);
8025
- this.registry.add({ id, title, source, options, inputValues: { ...options.inputs ?? {} } });
8044
+ this.registry.add({ id, title, source, options, inputValues: { ...options.inputs ?? {} }, propValues: { ...options.props ?? {} } });
8026
8045
  void this.startIndicator(id, source, options, handle);
8027
8046
  return handle;
8028
8047
  }
@@ -8045,7 +8064,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
8045
8064
  return handle;
8046
8065
  }
8047
8066
  const inputValues = { ...descriptor.defaultInputs(), ...options.inputs ?? {} };
8048
- this.registry.add({ id, title, source: type, inputValues, native: { type, instance: descriptor.create(), descriptor } });
8067
+ this.registry.add({ id, title, source: type, inputValues, propValues: {}, native: { type, instance: descriptor.create(), descriptor } });
8049
8068
  handle.setSchema(descriptor.inputsSchema());
8050
8069
  void this.startNativeIndicator(id, handle);
8051
8070
  return handle;
@@ -8098,6 +8117,19 @@ var EngineOrchestrator = class _EngineOrchestrator {
8098
8117
  if (record.session) record.session.update(record.inputValues);
8099
8118
  else if (record.native && !record.hidden) record.native.instance.setInputs(record.inputValues);
8100
8119
  }
8120
+ /** IndicatorController: re-run an indicator with merged declaration-prop overrides.
8121
+ * Same lifecycle as {@link applyInputs} — a prop change replays the whole script.
8122
+ * Script indicators only: natives have no declaration props. */
8123
+ applyProps(id, values) {
8124
+ const record = this.registry.get(id);
8125
+ if (!record?.session) return;
8126
+ record.propValues = { ...record.propValues, ...values };
8127
+ if (record.renderHandle) this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
8128
+ record.pendingStructural = true;
8129
+ if (!record.hidden) this.setLoading(record, true);
8130
+ record.pendingCause = "inputs";
8131
+ record.session.update(record.inputValues, record.propValues);
8132
+ }
8101
8133
  /** IndicatorController: tear down an indicator and (if now empty) its pane. */
8102
8134
  /** Live handles of every indicator on the chart (script + native), insertion order. */
8103
8135
  listIndicators() {
@@ -8246,6 +8278,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
8246
8278
  for (const input of prepared.inputs) defaults2[input.key] = input.defval;
8247
8279
  record.inputValues = { ...defaults2, ...record.inputValues };
8248
8280
  handle.setSchema(prepared.inputs);
8281
+ const propDefaults = {};
8282
+ for (const prop of prepared.props ?? []) propDefaults[prop.key] = prop.defval;
8283
+ record.propValues = { ...propDefaults, ...record.propValues };
8284
+ handle.setPropsSchema(prepared.props ?? []);
8249
8285
  this.mountLoadingPlaceholder(id, record);
8250
8286
  this.executeIndicator(id, handle);
8251
8287
  } catch (err) {
@@ -8269,6 +8305,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
8269
8305
  getBars: () => this.bars,
8270
8306
  fetchSeries: (sym, tf, range) => this.fetchSeries(sym, tf, range),
8271
8307
  inputs: record.inputValues,
8308
+ props: record.propValues,
8272
8309
  visibleRange: this.currentVisibleRange(),
8273
8310
  mode,
8274
8311
  // Mid-backfill session starts (add / re-show / price-style re-execute) let the
@@ -8362,6 +8399,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
8362
8399
  const meta = record.prepared.meta;
8363
8400
  const model = {
8364
8401
  id,
8402
+ // Deliberately the FULL title, never a shorttitle: while the script loads the
8403
+ // legend identifies it by its full name; the compact shorttitle arrives with
8404
+ // the first computed model and takes over from there.
8365
8405
  title: record.options?.title ?? meta.title,
8366
8406
  overlay: meta.overlay,
8367
8407
  paneHint: meta.overlay ? "price" : "new",
@@ -8370,7 +8410,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
8370
8410
  backgrounds: [],
8371
8411
  priceLines: [],
8372
8412
  inputs: record.prepared.inputs,
8373
- inputValues: record.inputValues
8413
+ inputValues: record.inputValues,
8414
+ ...record.prepared.props ? { props: record.prepared.props, propValues: record.propValues } : {}
8374
8415
  };
8375
8416
  const paneId = this.routePane(id, model, record.options ?? {});
8376
8417
  this.placeModel(model, id, paneId);
@@ -8603,7 +8644,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
8603
8644
  record.model = model;
8604
8645
  if (record.pendingStructural) {
8605
8646
  record.renderHandle = this.renderer.mountIndicator(model);
8606
- this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues);
8647
+ this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
8607
8648
  record.pendingStructural = false;
8608
8649
  } else {
8609
8650
  this.renderer.updateIndicator(record.renderHandle, modelToValuePatch(model));
@@ -8630,16 +8671,17 @@ var EngineOrchestrator = class _EngineOrchestrator {
8630
8671
  placeModel(model, id, paneId) {
8631
8672
  model.id = id;
8632
8673
  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;
8674
+ const paneOf = (item) => item.overlay === true ? "price" : paneId;
8675
+ for (const series of model.series) series.paneId = paneOf(series);
8676
+ for (const fill of model.fills) fill.paneId = paneOf(fill);
8677
+ for (const bg of model.backgrounds) bg.paneId = paneOf(bg);
8636
8678
  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;
8679
+ if (model.lines) for (const ln of model.lines) ln.paneId = paneOf(ln);
8680
+ if (model.boxes) for (const bx of model.boxes) bx.paneId = paneOf(bx);
8681
+ if (model.labels) for (const lb of model.labels) lb.paneId = paneOf(lb);
8682
+ if (model.polylines) for (const pl of model.polylines) pl.paneId = paneOf(pl);
8683
+ if (model.linefills) for (const lf of model.linefills) lf.paneId = paneOf(lf);
8684
+ if (model.tables) for (const tb of model.tables) tb.paneId = paneOf(tb);
8643
8685
  }
8644
8686
  emitContextChanged(id) {
8645
8687
  if (!this.registry.get(id)?.session?.getContext) return;
@@ -8792,6 +8834,15 @@ var RendererControl = class {
8792
8834
  setLegendActions(provider) {
8793
8835
  this.renderer.setLegendActions?.(provider);
8794
8836
  }
8837
+ /**
8838
+ * Wire the legend rows' HOST-CONTRIBUTED callout bubbles (the shells route the
8839
+ * plugin registry through this; see `registerLegendCallout`). Silent on a renderer
8840
+ * without the seam — contributed callouts simply never show there, same graceful
8841
+ * degradation as {@link setLegendActions}.
8842
+ */
8843
+ setLegendCallouts(provider) {
8844
+ this.renderer.setLegendCallouts?.(provider);
8845
+ }
8795
8846
  /**
8796
8847
  * Replace the indicator legend's fold toggle with a host action (or restore it with
8797
8848
  * `null`) — multi-chart shells point the chip at their indicator overview instead of
@@ -10379,13 +10430,6 @@ var Menu = class {
10379
10430
  }
10380
10431
  };
10381
10432
 
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
10433
  // src/ui/tokens.ts
10390
10434
  var STATIC_ID = "vela-ui-tokens";
10391
10435
  var STATIC_DECLS = Object.entries(STATIC_TOKENS).map(([k, v]) => ` ${k}: ${v};`).join("\n");
@@ -10631,6 +10675,199 @@ var Popover = class {
10631
10675
  }
10632
10676
  };
10633
10677
 
10678
+ // src/ui/components/callout-bubble/controller.ts
10679
+ function closesPanel(item) {
10680
+ return item.close !== false;
10681
+ }
10682
+ function calloutPanelRows(items) {
10683
+ const rows = [];
10684
+ for (const item of items) {
10685
+ if (item.type === "text") {
10686
+ rows.push({ type: "text", text: item.text });
10687
+ continue;
10688
+ }
10689
+ const last = rows[rows.length - 1];
10690
+ if (last && last.type === "buttons") last.buttons.push(item);
10691
+ else rows.push({ type: "buttons", buttons: [item] });
10692
+ }
10693
+ return rows;
10694
+ }
10695
+
10696
+ // src/ui/components/callout-bubble/styles.ts
10697
+ var CALLOUT_STYLE_ID = "vela-ui-callout-bubble";
10698
+ var CALLOUT_CSS = `
10699
+ .vela-callout {
10700
+ display: inline-grid;
10701
+ place-items: center;
10702
+ border-radius: 50%;
10703
+ flex: none;
10704
+ line-height: 0;
10705
+ box-sizing: border-box;
10706
+ cursor: default;
10707
+ user-select: none;
10708
+ -webkit-user-select: none;
10709
+ }
10710
+ .vela-callout[role='button'] { cursor: pointer; }
10711
+ .vela-callout svg { display: block; }
10712
+ /* The deployed panel \u2014 carries the kit's elevated-card look itself (the popover
10713
+ shell is bare positioning chrome). */
10714
+ .vela-callout-panel {
10715
+ display: flex;
10716
+ flex-direction: column;
10717
+ gap: 8px;
10718
+ padding: 10px 12px;
10719
+ max-width: 280px;
10720
+ box-sizing: border-box;
10721
+ background: var(--vela-surface-elev);
10722
+ border: 1px solid var(--vela-border-strong);
10723
+ border-radius: 6px;
10724
+ box-shadow: var(--vela-shadow);
10725
+ color: var(--vela-fg);
10726
+ font: var(--vela-font-size-md) var(--vela-font);
10727
+ }
10728
+ .vela-callout-title { font-weight: 600; color: var(--vela-fg-bright); }
10729
+ .vela-callout-text { color: var(--vela-fg-muted); line-height: 1.45; white-space: pre-line; }
10730
+ .vela-callout-actions { display: flex; flex-wrap: wrap; gap: 8px; }
10731
+ .vela-callout-btn {
10732
+ cursor: pointer;
10733
+ height: 26px;
10734
+ padding: 0 10px;
10735
+ border-radius: 5px;
10736
+ border: 1px solid var(--vela-border);
10737
+ background: transparent;
10738
+ color: var(--vela-fg);
10739
+ font-size: var(--vela-font-size-md);
10740
+ font-family: inherit;
10741
+ 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;
10742
+ }
10743
+ .vela-callout-btn:hover { background: var(--vela-hover); color: var(--vela-fg-bright); border-color: var(--vela-fg-muted); }
10744
+ .vela-callout-btn-primary { border-color: var(--vela-selected-bg); background: var(--vela-selected-bg); color: var(--vela-selected-fg); }
10745
+ .vela-callout-btn-primary:hover { background: var(--vela-selected-bg); color: var(--vela-selected-fg); opacity: 0.85; border-color: var(--vela-selected-bg); }
10746
+ `;
10747
+
10748
+ // src/ui/components/callout-bubble/view.ts
10749
+ var CalloutBubble = class {
10750
+ constructor(opts) {
10751
+ this.pop = null;
10752
+ this.spec = { icon: opts.icon, background: opts.background, label: opts.label };
10753
+ if (opts.color !== void 0) this.spec.color = opts.color;
10754
+ if (opts.panel !== void 0) this.spec.panel = opts.panel;
10755
+ this.size = opts.size ?? 16;
10756
+ this.host = opts.host;
10757
+ this.theme = opts.theme;
10758
+ this.boundary = opts.boundary;
10759
+ const doc = (opts.host ?? document.body).ownerDocument;
10760
+ injectStyles(CALLOUT_STYLE_ID, CALLOUT_CSS, doc);
10761
+ this.el = doc.createElement("span");
10762
+ this.el.className = "vela-callout";
10763
+ this.el.style.width = `${this.size}px`;
10764
+ this.el.style.height = `${this.size}px`;
10765
+ this.el.addEventListener("click", (e) => {
10766
+ if (!this.spec.panel) return;
10767
+ e.stopPropagation();
10768
+ this.toggle();
10769
+ });
10770
+ this.el.addEventListener("dblclick", (e) => {
10771
+ if (this.spec.panel) e.stopPropagation();
10772
+ });
10773
+ this.el.addEventListener("keydown", (e) => {
10774
+ if (!this.spec.panel || e.key !== "Enter" && e.key !== " ") return;
10775
+ e.preventDefault();
10776
+ this.toggle();
10777
+ });
10778
+ this.dress();
10779
+ }
10780
+ /** Re-dress the bubble (a status change: new icon, tint, label, panel). */
10781
+ set(spec) {
10782
+ this.spec = { ...this.spec, ...spec };
10783
+ if ("panel" in spec) this.pop?.hide();
10784
+ this.dress();
10785
+ }
10786
+ /** Whether the deployed panel is currently open. */
10787
+ get open() {
10788
+ return this.pop?.open ?? false;
10789
+ }
10790
+ destroy() {
10791
+ this.pop?.destroy();
10792
+ this.pop = null;
10793
+ this.el.remove();
10794
+ }
10795
+ dress() {
10796
+ const clickable = this.spec.panel !== void 0;
10797
+ this.el.style.background = this.spec.background;
10798
+ this.el.style.color = this.spec.color ?? "";
10799
+ this.el.innerHTML = iconAt(this.spec.icon, this.size - 4);
10800
+ this.el.setAttribute("aria-label", this.spec.label);
10801
+ if (clickable) {
10802
+ this.el.setAttribute("role", "button");
10803
+ this.el.tabIndex = 0;
10804
+ } else {
10805
+ this.el.removeAttribute("role");
10806
+ this.el.removeAttribute("tabindex");
10807
+ }
10808
+ }
10809
+ toggle() {
10810
+ if (this.pop?.open) {
10811
+ this.pop.hide();
10812
+ return;
10813
+ }
10814
+ const panel = this.spec.panel;
10815
+ if (!panel) return;
10816
+ this.pop?.destroy();
10817
+ this.pop = new Popover({
10818
+ trigger: this.el,
10819
+ gap: 6,
10820
+ content: (body) => this.buildPanel(body, panel),
10821
+ ...this.host ? { host: this.host } : {},
10822
+ ...this.theme ? { theme: this.theme() } : {},
10823
+ ...this.boundary ? { boundary: this.boundary } : {}
10824
+ });
10825
+ this.pop.show();
10826
+ }
10827
+ buildPanel(body, panel) {
10828
+ const doc = body.ownerDocument;
10829
+ const root = doc.createElement("div");
10830
+ root.className = "vela-callout-panel";
10831
+ if (panel.title) {
10832
+ const title = doc.createElement("div");
10833
+ title.className = "vela-callout-title";
10834
+ title.textContent = panel.title;
10835
+ root.appendChild(title);
10836
+ }
10837
+ for (const row of calloutPanelRows(panel.items)) {
10838
+ if (row.type === "text") {
10839
+ const text = doc.createElement("div");
10840
+ text.className = "vela-callout-text";
10841
+ text.textContent = row.text;
10842
+ root.appendChild(text);
10843
+ continue;
10844
+ }
10845
+ const actions = doc.createElement("div");
10846
+ actions.className = "vela-callout-actions";
10847
+ for (const item of row.buttons) {
10848
+ const btn2 = doc.createElement("button");
10849
+ btn2.type = "button";
10850
+ btn2.className = "vela-callout-btn" + (item.primary ? " vela-callout-btn-primary" : "");
10851
+ btn2.textContent = item.label;
10852
+ btn2.addEventListener("click", () => {
10853
+ item.run();
10854
+ if (closesPanel(item)) this.pop?.hide();
10855
+ });
10856
+ actions.appendChild(btn2);
10857
+ }
10858
+ root.appendChild(actions);
10859
+ }
10860
+ body.appendChild(root);
10861
+ }
10862
+ };
10863
+
10864
+ // src/core/model/inputs.ts
10865
+ function inputVisible(when, values) {
10866
+ if (!when) return true;
10867
+ const conds = Array.isArray(when) ? when : [when];
10868
+ return conds.every((c) => c.anyOf ? c.anyOf.some((x) => x === values[c.key]) : values[c.key] === c.equals);
10869
+ }
10870
+
10634
10871
  // src/ui/components/select/controller.ts
10635
10872
  function selectController(opts) {
10636
10873
  const options = opts.options;
@@ -12545,6 +12782,10 @@ function buildFieldControl(desc) {
12545
12782
  }
12546
12783
 
12547
12784
  // src/renderers/shared/IndicatorInputsDialog.ts
12785
+ var PROPS_TAB = "Properties";
12786
+ function bagOf(row, decl) {
12787
+ return decl.prop ? row.propValues : row.values;
12788
+ }
12548
12789
  var SOURCES = ["close", "open", "high", "low", "hl2", "hlc3", "ohlc4", "volume"];
12549
12790
  var IndicatorInputsDialog = class {
12550
12791
  constructor(host) {
@@ -12555,6 +12796,7 @@ var IndicatorInputsDialog = class {
12555
12796
  this.openId = null;
12556
12797
  this.row = null;
12557
12798
  this.snapshot = null;
12799
+ this.propSnapshot = null;
12558
12800
  this.dialogTips = [];
12559
12801
  /** Re-applies every `when` gate against current values; set while the dialog is open. */
12560
12802
  this.refreshVisibility = null;
@@ -12577,10 +12819,11 @@ var IndicatorInputsDialog = class {
12577
12819
  }
12578
12820
  open(row) {
12579
12821
  this.close();
12580
- if (row.inputs.length === 0) return;
12822
+ if (row.inputs.length === 0 && row.props.length === 0) return;
12581
12823
  this.row = row;
12582
12824
  this.openId = row.id;
12583
12825
  this.snapshot = { ...row.values };
12826
+ this.propSnapshot = { ...row.propValues };
12584
12827
  ensureDialogStyles();
12585
12828
  const t = this.host.theme();
12586
12829
  const border = "var(--vela-border)";
@@ -12588,7 +12831,7 @@ var IndicatorInputsDialog = class {
12588
12831
  const host = this.host.dialogHost() ?? this.host.container;
12589
12832
  const ui = new Dialog({
12590
12833
  host,
12591
- title: row.settingsTitle,
12834
+ title: row.title,
12592
12835
  // Non-modal: live-edit dialog — a modal machine locks pointer events on the
12593
12836
  // whole body, killing the chart and the body-portaled popovers.
12594
12837
  modal: false,
@@ -12620,6 +12863,16 @@ var IndicatorInputsDialog = class {
12620
12863
  text: () => "Close"
12621
12864
  }));
12622
12865
  const tabDefs = tabInputs(row.inputs);
12866
+ if (row.props.length > 0) {
12867
+ const propDecls = row.props.map((p) => {
12868
+ const d = { ...p, prop: true };
12869
+ delete d.tab;
12870
+ return d;
12871
+ });
12872
+ const existing = tabDefs.find((t2) => t2.name === PROPS_TAB);
12873
+ if (existing) existing.inputs.push(...propDecls);
12874
+ else tabDefs.push({ name: PROPS_TAB, inputs: propDecls });
12875
+ }
12623
12876
  const tabs = document.createElement("div");
12624
12877
  tabs.style.cssText = `display:flex;gap:12px;padding:0 20px;border-bottom:1px solid ${border};flex:0 0 auto;`;
12625
12878
  const tabEls = [];
@@ -12722,9 +12975,10 @@ var IndicatorInputsDialog = class {
12722
12975
  this.openId = null;
12723
12976
  this.row = null;
12724
12977
  this.snapshot = null;
12978
+ this.propSnapshot = null;
12725
12979
  ui?.destroy();
12726
12980
  }
12727
- /** Restore every input to its open-time value (re-running the indicator), then close. */
12981
+ /** Restore every input and prop to its open-time value (re-running the indicator), then close. */
12728
12982
  revertAndClose() {
12729
12983
  const row = this.row;
12730
12984
  const snap = this.snapshot;
@@ -12738,6 +12992,17 @@ var IndicatorInputsDialog = class {
12738
12992
  }
12739
12993
  }
12740
12994
  }
12995
+ const propSnap = this.propSnapshot;
12996
+ if (row && propSnap) {
12997
+ for (const p of row.props) {
12998
+ const snapped = propSnap[p.key];
12999
+ const before = snapped !== void 0 ? snapped : p.defval;
13000
+ if (row.propValues[p.key] !== before) {
13001
+ row.propValues[p.key] = before;
13002
+ this.host.onChange?.({ indicatorId: row.id, key: p.key, value: before, kind: "prop" });
13003
+ }
13004
+ }
13005
+ }
12741
13006
  this.close();
12742
13007
  }
12743
13008
  /** The footer's reset button — same chip as Cancel, pinned to the LEFT edge
@@ -12755,19 +13020,27 @@ var IndicatorInputsDialog = class {
12755
13020
  const row = this.row;
12756
13021
  if (!row) return;
12757
13022
  const snap = this.snapshot;
13023
+ const propSnap = this.propSnapshot;
12758
13024
  for (const inp of row.inputs) {
12759
13025
  if (row.values[inp.key] !== inp.defval) {
12760
13026
  row.values[inp.key] = inp.defval;
12761
13027
  this.host.onChange?.({ indicatorId: row.id, key: inp.key, value: inp.defval });
12762
13028
  }
12763
13029
  }
13030
+ for (const p of row.props) {
13031
+ if (row.propValues[p.key] !== p.defval) {
13032
+ row.propValues[p.key] = p.defval;
13033
+ this.host.onChange?.({ indicatorId: row.id, key: p.key, value: p.defval, kind: "prop" });
13034
+ }
13035
+ }
12764
13036
  this.open(row);
12765
13037
  if (snap) this.snapshot = snap;
13038
+ if (propSnap) this.propSnapshot = propSnap;
12766
13039
  }
12767
13040
  /** 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 });
13041
+ commit(row, decl, value) {
13042
+ bagOf(row, decl)[decl.key] = value;
13043
+ this.host.onChange?.({ indicatorId: row.id, key: decl.key, value, ...decl.prop ? { kind: "prop" } : {} });
12771
13044
  this.refreshVisibility?.();
12772
13045
  }
12773
13046
  /** One settings row (or several `inline=` inputs) placed into a section's grid. */
@@ -12794,7 +13067,7 @@ var IndicatorInputsDialog = class {
12794
13067
  const id = idOf(lead);
12795
13068
  const info = lead.tooltip ? this.infoButton(lead.tooltip) : void 0;
12796
13069
  if (lead.type === "bool") {
12797
- const current = Boolean(row.values[lead.key] ?? lead.defval);
13070
+ const current = Boolean(bagOf(row, lead)[lead.key] ?? lead.defval);
12798
13071
  return append(fieldRow({
12799
13072
  label: nameOf(lead),
12800
13073
  id,
@@ -12803,7 +13076,7 @@ var IndicatorInputsDialog = class {
12803
13076
  toggle: {
12804
13077
  id,
12805
13078
  checked: current,
12806
- onChange: (v) => this.commit(row, lead.key, v)
13079
+ onChange: (v) => this.commit(row, lead, v)
12807
13080
  }
12808
13081
  }));
12809
13082
  }
@@ -12828,8 +13101,8 @@ var IndicatorInputsDialog = class {
12828
13101
  }
12829
13102
  /** Build the typed control for one input, committing edits live via `onChange`. */
12830
13103
  buildControl(row, inp, id) {
12831
- const current = row.values[inp.key] ?? inp.defval;
12832
- const emit = (value) => this.commit(row, inp.key, value);
13104
+ const current = bagOf(row, inp)[inp.key] ?? inp.defval;
13105
+ const emit = (value) => this.commit(row, inp, value);
12833
13106
  if (inp.type === "bool") {
12834
13107
  return buildFieldControl({ kind: "switch", id, checked: Boolean(current), onChange: (v) => emit(v) }).el;
12835
13108
  }
@@ -12840,7 +13113,7 @@ var IndicatorInputsDialog = class {
12840
13113
  kind: "color",
12841
13114
  id,
12842
13115
  theme: this.host.theme(),
12843
- get: () => String(row.values[inp.key] ?? inp.defval),
13116
+ get: () => String(bagOf(row, inp)[inp.key] ?? inp.defval),
12844
13117
  onChange: (v) => emit(v)
12845
13118
  }).el;
12846
13119
  }
@@ -13403,6 +13676,7 @@ function timeParts(ts) {
13403
13676
  }
13404
13677
 
13405
13678
  // src/renderers/shared/InputsUI.ts
13679
+ var LEGEND_AT_TOP_ATTR = "data-vela-pane-at-top";
13406
13680
  var STATUS_KEYFRAMES = "@keyframes vela-ind-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.3;transform:scale(.6)}}";
13407
13681
  var LEGEND_ICON_PX2 = 16;
13408
13682
  var LEGEND_CTL_PX = 18;
@@ -13441,10 +13715,14 @@ var InputsUI = class {
13441
13715
  this.moveApi = null;
13442
13716
  /** Host-contributed legend actions, resolved PER ROW at render time (see setLegendActions). */
13443
13717
  this.legendActions = null;
13718
+ /** Host-contributed callout bubbles, resolved PER ROW at render time (see setLegendCallouts). */
13719
+ this.legendCallouts = null;
13444
13720
  /** Chrome-tooltip disposers, per row id — a tip open at removal must not outlive its row. */
13445
13721
  this.rowTips = /* @__PURE__ */ new Map();
13446
13722
  /** Same, for the contributed extras only (rebuilt independently by setLegendActions). */
13447
13723
  this.extrasTips = /* @__PURE__ */ new Map();
13724
+ /** Same, for the contributed callouts only (rebuilt independently by setLegendCallouts). */
13725
+ this.calloutTips = /* @__PURE__ */ new Map();
13448
13726
  /** Open "Move to" menu (kept so it can be torn down). */
13449
13727
  this.moveMenu = null;
13450
13728
  this.moveTargets = [];
@@ -13602,6 +13880,42 @@ var InputsUI = class {
13602
13880
  extrasEl.appendChild(btn2);
13603
13881
  }
13604
13882
  }
13883
+ /**
13884
+ * Wire the host-contributed callout bubbles. Same contract as
13885
+ * {@link setLegendActions}: re-calling replaces the provider and re-projects the
13886
+ * rows already on screen.
13887
+ */
13888
+ setLegendCallouts(provider) {
13889
+ this.legendCallouts = provider;
13890
+ for (const row of this.rows.values()) this.renderCallouts(row);
13891
+ }
13892
+ /** (Re)build one row's callout bubbles — tinted icon circles beside the title whose
13893
+ * click (when the view carries content) deploys a panel of text and actions. */
13894
+ renderCallouts(row) {
13895
+ this.disposeTips(this.calloutTips, row.id);
13896
+ for (const bubble of row.callouts) bubble.destroy();
13897
+ row.callouts = [];
13898
+ row.calloutsEl.replaceChildren();
13899
+ const views = this.legendCallouts?.(row.id) ?? [];
13900
+ row.calloutsEl.style.display = views.length > 0 ? "inline-flex" : "none";
13901
+ for (const view of views) {
13902
+ const bubble = new CalloutBubble({
13903
+ icon: view.icon,
13904
+ background: view.background,
13905
+ ...view.color !== void 0 ? { color: view.color } : {},
13906
+ label: view.tooltip,
13907
+ ...view.content !== void 0 ? { panel: view.content } : {},
13908
+ // The panel portals into the plot host and self-tokens: it must work on
13909
+ // a bare chart, where no `.vela-ui` token ancestor exists.
13910
+ host: this.container,
13911
+ theme: () => this.theme
13912
+ });
13913
+ bubble.el.dataset.legendCallout = view.id;
13914
+ this.tip(this.calloutTips, row.id, bubble.el, view.tooltip);
13915
+ row.callouts.push(bubble);
13916
+ row.calloutsEl.appendChild(bubble.el);
13917
+ }
13918
+ }
13605
13919
  /** Reposition the per-pane legend containers after a layout change. */
13606
13920
  reposition() {
13607
13921
  for (const [paneId, lg] of this.legends) this.positionLegend(lg, paneId);
@@ -13769,6 +14083,7 @@ var InputsUI = class {
13769
14083
  }
13770
14084
  positionLegend(lg, paneId) {
13771
14085
  const bounds = this.paneBoundsOf ? this.paneBoundsOf(paneId) : { top: 0, height: Infinity };
14086
+ lg.toggleAttribute(LEGEND_AT_TOP_ATTR, bounds.top === 0);
13772
14087
  lg.style.display = bounds.height < 4 || !this.titlesVisible ? "none" : "flex";
13773
14088
  const collapsed = this.paneCollapse.has(paneId);
13774
14089
  const masterId = this.paneCollapse.get(paneId) ?? null;
@@ -13844,13 +14159,13 @@ var InputsUI = class {
13844
14159
  }
13845
14160
  /** Create or update an indicator's legend row (in the legend for its pane). */
13846
14161
  upsert(id, title, inputs, values, paneId = "price", opts = {}) {
13847
- const settingsTitle = opts.settingsTitle ?? title;
13848
14162
  const existing = this.rows.get(id);
13849
14163
  if (existing) {
13850
14164
  existing.title = title;
13851
- existing.settingsTitle = settingsTitle;
13852
14165
  existing.inputs = inputs;
13853
14166
  existing.values = { ...values };
14167
+ existing.props = opts.props ?? [];
14168
+ existing.propValues = { ...opts.propValues ?? {} };
13854
14169
  existing.titleEl.textContent = title;
13855
14170
  if (existing.paneId !== paneId) {
13856
14171
  existing.paneId = paneId;
@@ -13902,6 +14217,9 @@ var InputsUI = class {
13902
14217
  titleWrap.appendChild(beta);
13903
14218
  }
13904
14219
  el.appendChild(titleWrap);
14220
+ const calloutsEl = document.createElement("span");
14221
+ calloutsEl.style.cssText = `display:none;align-items:center;gap:4px;flex:none;margin-left:${LEGEND_TITLE_STATUS_GAP_PX}px;`;
14222
+ el.appendChild(calloutsEl);
13905
14223
  el.appendChild(statusEl);
13906
14224
  const valuesEl = document.createElement("span");
13907
14225
  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 +14236,13 @@ var InputsUI = class {
13918
14236
  eye.className = "vela-ind-ctl";
13919
14237
  eye.style.cssText = LEGEND_CTL_CSS;
13920
14238
  eye.addEventListener("click", () => {
13921
- const row = this.rows.get(id);
13922
- this.onToggleVisible?.(id, Boolean(row?.hidden));
14239
+ const row2 = this.rows.get(id);
14240
+ this.onToggleVisible?.(id, Boolean(row2?.hidden));
13923
14241
  });
13924
14242
  controlsEl.appendChild(eye);
13925
14243
  eyeEl = eye;
13926
14244
  }
13927
- if (inputs.length > 0) {
14245
+ if (inputs.length > 0 || (opts.props ?? []).length > 0) {
13928
14246
  const gear = document.createElement("button");
13929
14247
  gear.type = "button";
13930
14248
  gear.setAttribute("aria-label", "Settings");
@@ -13964,7 +14282,9 @@ var InputsUI = class {
13964
14282
  controlsEl.appendChild(close);
13965
14283
  el.appendChild(controlsEl);
13966
14284
  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 });
14285
+ 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 };
14286
+ this.rows.set(id, row);
14287
+ this.renderCallouts(row);
13968
14288
  this.syncFoldToggle();
13969
14289
  }
13970
14290
  /** Place a row in its pane's legend — native rows PREPEND (pinned to the top), Pine rows append. */
@@ -13973,9 +14293,11 @@ var InputsUI = class {
13973
14293
  else container.appendChild(el);
13974
14294
  }
13975
14295
  /** Reflect programmatic input changes (so a re-opened dialog shows current values). */
13976
- setValues(id, values) {
14296
+ setValues(id, values, props) {
13977
14297
  const row = this.rows.get(id);
13978
- if (row) row.values = { ...row.values, ...values };
14298
+ if (!row) return;
14299
+ row.values = { ...row.values, ...values };
14300
+ if (props) row.propValues = { ...row.propValues, ...props };
13979
14301
  }
13980
14302
  /**
13981
14303
  * Reflect an indicator's live status in its legend row: `'loading'` shows three pulsing
@@ -14057,8 +14379,13 @@ var InputsUI = class {
14057
14379
  syncRowActions(row) {
14058
14380
  const open2 = row.highlighted;
14059
14381
  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);
14382
+ if (open2) {
14383
+ row.el.appendChild(row.statusEl);
14384
+ row.el.appendChild(row.calloutsEl);
14385
+ } else {
14386
+ row.el.insertBefore(row.statusEl, row.valuesEl);
14387
+ row.el.insertBefore(row.calloutsEl, row.statusEl);
14388
+ }
14062
14389
  for (const child of Array.from(row.controlsEl.children)) {
14063
14390
  if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
14064
14391
  if (child === row.extrasEl) {
@@ -14076,10 +14403,12 @@ var InputsUI = class {
14076
14403
  }
14077
14404
  remove(id) {
14078
14405
  const row = this.rows.get(id);
14406
+ for (const bubble of row?.callouts ?? []) bubble.destroy();
14079
14407
  row?.el.remove();
14080
14408
  this.rows.delete(id);
14081
14409
  this.disposeTips(this.rowTips, id);
14082
14410
  this.disposeTips(this.extrasTips, id);
14411
+ this.disposeTips(this.calloutTips, id);
14083
14412
  if (this.selectedId === id) this.selectedId = null;
14084
14413
  if (this.inputsDialog.openId === id) this.inputsDialog.close();
14085
14414
  this.syncFoldToggle();
@@ -14099,6 +14428,8 @@ var InputsUI = class {
14099
14428
  this.rowMenu = null;
14100
14429
  for (const id of [...this.rowTips.keys()]) this.disposeTips(this.rowTips, id);
14101
14430
  for (const id of [...this.extrasTips.keys()]) this.disposeTips(this.extrasTips, id);
14431
+ for (const id of [...this.calloutTips.keys()]) this.disposeTips(this.calloutTips, id);
14432
+ for (const row of this.rows.values()) for (const bubble of row.callouts) bubble.destroy();
14102
14433
  for (const lg of this.legends.values()) lg.remove();
14103
14434
  this.legends.clear();
14104
14435
  this.rows.clear();
@@ -14453,6 +14784,32 @@ var SIZE_PX3 = {
14453
14784
  large: 16,
14454
14785
  huge: 20
14455
14786
  };
14787
+ function fontPxOf(size) {
14788
+ if (typeof size === "number") return size > 0 ? size : SIZE_PX3.auto;
14789
+ return SIZE_PX3[size] ?? SIZE_PX3.auto;
14790
+ }
14791
+ function tableHasContent(t) {
14792
+ return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
14793
+ }
14794
+ function mergeRenderPlan(t) {
14795
+ const span = /* @__PURE__ */ new Map();
14796
+ const omit = /* @__PURE__ */ new Set();
14797
+ for (const m of t.merges) {
14798
+ span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
14799
+ for (let r = m.startRow; r <= m.endRow; r += 1) {
14800
+ for (let c = m.startCol; c <= m.endCol; c += 1) {
14801
+ if (r !== m.startRow || c !== m.startCol) omit.add(`${r}:${c}`);
14802
+ }
14803
+ }
14804
+ }
14805
+ for (let r = 0; r < t.rows; r += 1) {
14806
+ for (let c = 0; c < t.columns; c += 1) {
14807
+ if (t.cells[r]?.[c]?.merged && !span.has(`${r}:${c}`)) omit.add(`${r}:${c}`);
14808
+ }
14809
+ }
14810
+ for (const key of span.keys()) omit.delete(key);
14811
+ return { span, omit };
14812
+ }
14456
14813
  var TableOverlay = class {
14457
14814
  constructor(container, theme, paneBounds) {
14458
14815
  this.container = container;
@@ -14473,7 +14830,9 @@ var TableOverlay = class {
14473
14830
  update(tables) {
14474
14831
  this.lastTables = tables;
14475
14832
  this.root.replaceChildren();
14476
- for (const t of tables) this.root.appendChild(this.renderTable(t));
14833
+ for (const t of tables) {
14834
+ if (tableHasContent(t)) this.root.appendChild(this.renderTable(t));
14835
+ }
14477
14836
  }
14478
14837
  /** Re-render at the current pane geometry — after layout settles or on resize. */
14479
14838
  reposition() {
@@ -14488,39 +14847,36 @@ var TableOverlay = class {
14488
14847
  this.root.remove();
14489
14848
  }
14490
14849
  renderTable(t) {
14850
+ const b = this.paneBounds(t.paneId);
14491
14851
  const wrap = document.createElement("div");
14492
14852
  wrap.style.position = "absolute";
14493
- this.anchor(wrap, t.position, this.paneBounds(t.paneId));
14853
+ if (t.frameColor && t.frameWidth > 0) wrap.style.border = `${t.frameWidth}px solid ${t.frameColor}`;
14854
+ this.anchor(wrap, t.position, b);
14494
14855
  const table = document.createElement("table");
14495
14856
  Object.assign(table.style, {
14496
14857
  borderCollapse: "collapse",
14497
14858
  background: t.bgColor ?? "transparent",
14498
14859
  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
14860
  border: "none",
14503
14861
  tableLayout: "auto",
14504
14862
  // Re-enable pointer events on the table only (root is none) so cell tooltips work.
14505
14863
  pointerEvents: "auto"
14506
14864
  });
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
- }
14865
+ const { span, omit } = mergeRenderPlan(t);
14866
+ const plotW = Math.max(0, (this.root.clientWidth || this.container.clientWidth) - b.rightAxis);
14867
+ const paneH = b.height;
14517
14868
  const cellBorder = t.borderColor && t.borderWidth > 0 ? `${t.borderWidth}px solid ${t.borderColor}` : "none";
14518
14869
  for (let r = 0; r < t.rows; r += 1) {
14519
14870
  const tr = document.createElement("tr");
14520
14871
  for (let c = 0; c < t.columns; c += 1) {
14872
+ if (omit.has(`${r}:${c}`)) continue;
14521
14873
  const cell = t.cells[r]?.[c] ?? null;
14522
- if (skip.has(`${r}:${c}`) || cell?.merged) continue;
14523
14874
  const td = document.createElement("td");
14875
+ if (cell === null) {
14876
+ Object.assign(td.style, { padding: "0", border: "none" });
14877
+ tr.appendChild(td);
14878
+ continue;
14879
+ }
14524
14880
  const sp = span.get(`${r}:${c}`);
14525
14881
  if (sp) {
14526
14882
  if (sp.cs > 1) td.colSpan = sp.cs;
@@ -14529,18 +14885,22 @@ var TableOverlay = class {
14529
14885
  Object.assign(td.style, {
14530
14886
  border: cellBorder,
14531
14887
  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"
14888
+ background: cell.bgColor ?? "transparent",
14889
+ color: cell.textColor ?? this.theme.textColor,
14890
+ textAlign: cell.hAlign,
14891
+ verticalAlign: cell.vAlign === "top" ? "top" : cell.vAlign === "bottom" ? "bottom" : "middle",
14892
+ fontSize: `${fontPxOf(cell.textSize)}px`,
14893
+ fontFamily: cell.fontFamily === "monospace" ? "monospace" : "inherit",
14894
+ fontWeight: cell.bold ? "bold" : "normal",
14895
+ fontStyle: cell.italic ? "italic" : "normal",
14896
+ // Pine cell text never wraps; `\n` still breaks lines. Wrapping
14897
+ // used to collapse unicode sparklines and ━━━ dividers.
14898
+ whiteSpace: "pre"
14541
14899
  });
14542
- if (cell?.tooltip) td.title = cell.tooltip;
14543
- td.textContent = cell?.text ?? "";
14900
+ if (cell.width) td.style.width = `${cell.width / 100 * plotW}px`;
14901
+ if (cell.height) td.style.height = `${cell.height / 100 * paneH}px`;
14902
+ if (cell.tooltip) td.title = cell.tooltip;
14903
+ td.textContent = cell.text ?? "";
14544
14904
  tr.appendChild(td);
14545
14905
  }
14546
14906
  table.appendChild(tr);
@@ -15425,8 +15785,14 @@ var WebGL2Backend = class {
15425
15785
  return sc === pane.scale ? pane : { ...pane, scale: sc };
15426
15786
  };
15427
15787
  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));
15788
+ for (const m of models) for (const bgSpan of m.backgrounds) if (bgSpan.overlay !== true) this.emitBackground(b, bgSpan, pane, coords);
15789
+ for (const m of models) for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, effPane(m), coords, i0, i1, scene.offsetOf(m.id));
15790
+ if (isPrice) {
15791
+ for (const m of scene.indicators.values()) {
15792
+ for (const bgSpan of m.backgrounds) if (bgSpan.overlay === true) this.emitBackground(b, bgSpan, pane, coords);
15793
+ for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, i1, scene.offsetOf(m.id));
15794
+ }
15795
+ }
15430
15796
  const drawCandles = isPrice && !scene.candlesHidden;
15431
15797
  let candleDrawn = false;
15432
15798
  for (const m of models) {
@@ -15440,7 +15806,7 @@ var WebGL2Backend = class {
15440
15806
  b.alpha = this.modelAlpha;
15441
15807
  const off = scene.offsetOf(m.id);
15442
15808
  const mp = effPane(m);
15443
- for (const s of m.series) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
15809
+ for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
15444
15810
  }
15445
15811
  if (drawCandles && !candleDrawn) {
15446
15812
  drawSlicesUpTo(scene.candleZ);
@@ -15449,6 +15815,12 @@ var WebGL2Backend = class {
15449
15815
  }
15450
15816
  drawSlicesUpTo(Infinity);
15451
15817
  b.alpha = this.modelAlpha;
15818
+ if (isPrice) {
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
+ }
15452
15824
  for (const m of models) {
15453
15825
  const mp = effPane(m);
15454
15826
  for (const pl of m.priceLines) this.emitHline(b, pl, mp, coords, dataW, theme);
@@ -15495,7 +15867,7 @@ var WebGL2Backend = class {
15495
15867
  for (const pane of scene.orderedPanes()) {
15496
15868
  const b = this.glowBatch;
15497
15869
  b.reset();
15498
- this.emitGlowSources(b, scene.indicatorsForPane(pane.id), pane, coords, i0, i1, (id) => scene.offsetOf(id));
15870
+ this.emitGlowSources(b, scene, pane, coords, i0, i1);
15499
15871
  if (b.vertexCount === 0) continue;
15500
15872
  const topH = Math.round(pane.bounds.top * dpr * 0.5);
15501
15873
  const botH = Math.round((pane.bounds.top + pane.bounds.height) * dpr * 0.5);
@@ -15564,15 +15936,23 @@ var WebGL2Backend = class {
15564
15936
  gl.bindVertexArray(null);
15565
15937
  return true;
15566
15938
  }
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);
15939
+ /** The "neon" elements that glow: line/area/step lines + point markers (not candles/fills/bars).
15940
+ * Routing mirrors the main pass: own series per pane, force_overlay series on the price pane. */
15941
+ emitGlowSources(b, scene, pane, coords, i0, i1) {
15942
+ const emitOne = (s, off) => {
15943
+ if (!isLineLikeSeries(s) || s.visible === false) return;
15944
+ if (s.kind === "histogram" || s.kind === "columns") return;
15945
+ if (s.kind === "circles" || s.kind === "cross") this.emitPointMarkers(b, s, pane, coords, i0, i1, off);
15946
+ else this.emitPolyline(b, s, pane, coords, i0, i1, s.kind === "step", off);
15947
+ };
15948
+ for (const m of scene.indicatorsForPane(pane.id)) {
15949
+ const off = scene.offsetOf(m.id);
15950
+ for (const s of m.series) if (s.overlay !== true) emitOne(s, off);
15951
+ }
15952
+ if (pane.kind === "price") {
15953
+ for (const m of scene.indicators.values()) {
15954
+ const off = scene.offsetOf(m.id);
15955
+ for (const s of m.series) if (s.overlay === true) emitOne(s, off);
15576
15956
  }
15577
15957
  }
15578
15958
  }
@@ -15855,9 +16235,9 @@ var WebGL2Backend = class {
15855
16235
  b.alpha = this.candleBodyAlpha;
15856
16236
  b.rect(g.bodyX, bodyTop, g.bodyW, bodyH, c);
15857
16237
  }
15858
- if (cs.borderVisible || bc || fading && cs.bodyVisible) {
16238
+ if (cs.borderVisible || fading && cs.bodyVisible) {
15859
16239
  b.alpha = this.candleStructureAlpha;
15860
- const bord = cs.borderVisible || bc ? parseColor((isUp ? cs.borderUpColor : cs.borderDownColor) ?? dir) : c;
16240
+ const bord = cs.borderVisible ? parseColor((isUp ? cs.borderUpColor : cs.borderDownColor) ?? bodyColorStr) : c;
15861
16241
  const bw = Math.max(0, g.bodyW - 1);
15862
16242
  const bh = Math.max(0, bodyH - 1);
15863
16243
  b.rectStroke(g.bodyX + 0.5, bodyTop + 0.5, bw, bh, 1, bord);
@@ -17409,8 +17789,14 @@ var Canvas2dBackend = class {
17409
17789
  return sc === pane.scale ? pane : { ...pane, scale: sc };
17410
17790
  };
17411
17791
  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));
17792
+ for (const m of models) for (const bg of m.backgrounds) if (bg.overlay !== true) this.drawBackground(ctx, bg, pane, coords);
17793
+ for (const m of models) for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, effPane(m), coords, i0, i1, scene.offsetOf(m.id));
17794
+ if (isPrice) {
17795
+ for (const m of scene.indicators.values()) {
17796
+ for (const bg of m.backgrounds) if (bg.overlay === true) this.drawBackground(ctx, bg, pane, coords);
17797
+ for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, scene.offsetOf(m.id));
17798
+ }
17799
+ }
17414
17800
  const slices = scene.drawingSlices.get(pane.id) ?? [];
17415
17801
  let si = 0;
17416
17802
  const drawSlicesUpTo = (z) => {
@@ -17432,7 +17818,7 @@ 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 s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
17436
17822
  }
17437
17823
  if (drawCandles && !candleDrawn) {
17438
17824
  drawSlicesUpTo(scene.candleZ);
@@ -17440,6 +17826,13 @@ var Canvas2dBackend = class {
17440
17826
  this.drawPriceSeries(ctx, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
17441
17827
  }
17442
17828
  drawSlicesUpTo(Infinity);
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 s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0, i1, theme, off);
17834
+ }
17835
+ }
17443
17836
  ctx.globalAlpha = this.modelAlpha;
17444
17837
  for (const m of models) {
17445
17838
  const mp = effPane(m);
@@ -17711,9 +18104,9 @@ var Canvas2dBackend = class {
17711
18104
  ctx.fillStyle = color;
17712
18105
  ctx.fillRect(g.bodyX, top, g.bodyW, bodyH);
17713
18106
  }
17714
- if (cs.borderVisible || bc || fading && cs.bodyVisible) {
18107
+ if (cs.borderVisible || fading && cs.bodyVisible) {
17715
18108
  ctx.globalAlpha = this.candleStructureAlpha;
17716
- ctx.strokeStyle = cs.borderVisible || bc ? (up ? cs.borderUpColor : cs.borderDownColor) ?? dir : color;
18109
+ ctx.strokeStyle = cs.borderVisible ? (up ? cs.borderUpColor : cs.borderDownColor) ?? color : color;
17717
18110
  ctx.lineWidth = 1;
17718
18111
  const bw = Math.max(0, g.bodyW - 1);
17719
18112
  const bh = Math.max(0, bodyH - 1);
@@ -18145,6 +18538,8 @@ var DrawingSceneRenderer = class {
18145
18538
  this.set = set;
18146
18539
  /** measureText width cache, keyed by `${font} ${text}`, persists across frames. */
18147
18540
  this.widthCache = /* @__PURE__ */ new Map();
18541
+ /** Tooltip hit-rects captured while drawing the CURRENT set (rebuilt per render). */
18542
+ this.tipRegions = [];
18148
18543
  /**
18149
18544
  * Index offset of the CURRENT set's model: its `xloc:'bar_index'` coordinates count
18150
18545
  * from the model's anchor bar, so they shift by this to land on chart logical indices.
@@ -18165,8 +18560,13 @@ var DrawingSceneRenderer = class {
18165
18560
  const s = this.set;
18166
18561
  return !s.lines.length && !s.boxes.length && !s.labels.length && !s.polylines.length && !s.linefills.length;
18167
18562
  }
18563
+ /** Tooltip hit-rects of the labels drawn by the LAST `render` call (same coords as `ctx`). */
18564
+ labelTipRegions() {
18565
+ return this.tipRegions;
18566
+ }
18168
18567
  /** Draw the whole set into `ctx` using the supplied coordinate closures. */
18169
18568
  render(ctx, W, H, xOf, yOf) {
18569
+ this.tipRegions = [];
18170
18570
  if (this.isEmpty()) return;
18171
18571
  ctx.save();
18172
18572
  this.drawLinefills(ctx, W, H, xOf, yOf);
@@ -18514,13 +18914,35 @@ var DrawingSceneRenderer = class {
18514
18914
  if (this.isPointShape(lb.style)) {
18515
18915
  if (!lb.noFill) this.drawLabelShape(ctx, lb.style, px, py, fontPx, color);
18516
18916
  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");
18917
+ if (lb.tooltip) {
18918
+ const r = Math.max(4, fontPx * 0.6) + 3;
18919
+ this.tipRegions.push({ left: px - r, top: py - r, right: px + r, bottom: py + r, text: lb.tooltip });
18920
+ }
18921
+ } else if (lb.style === "none" || lb.style === "text_outline") {
18922
+ if (lb.text) {
18923
+ this.drawLabelText(ctx, lb, px, py, fontPx, lb.style === "text_outline");
18924
+ if (lb.tooltip) this.tipRegions.push(this.textRegion(ctx, lb, px, py, fontPx, lb.tooltip));
18925
+ }
18519
18926
  } else {
18520
- this.drawBubble(ctx, lb, px, py, fontPx, color);
18927
+ const r = this.drawBubble(ctx, lb, px, py, fontPx, color);
18928
+ 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
18929
  }
18522
18930
  }
18523
18931
  }
18932
+ /** Canvas font for a label's text — family + Pine `text_formatting` (bold/italic). */
18933
+ labelFont(lb, fontPx) {
18934
+ const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
18935
+ return `${lb.italic ? "italic " : ""}${lb.bold ? "bold " : ""}${fontPx}px ${family}`;
18936
+ }
18937
+ /** Hover rect of a text-only label (style none/text_outline/noFill), centered like drawLabelText. */
18938
+ textRegion(ctx, lb, cx, cy, fontPx, text) {
18939
+ const font = this.labelFont(lb, fontPx);
18940
+ const lines = (lb.text ?? "").split("\n");
18941
+ const w = Math.max(1, ...lines.map((l) => this.measure(ctx, font, l)));
18942
+ const h = fontPx * 1.25 * lines.length;
18943
+ const left = lb.textAlign === "left" ? cx : lb.textAlign === "right" ? cx - w : cx - w / 2;
18944
+ return { left, top: cy - h / 2, right: left + w, bottom: cy + h / 2, text };
18945
+ }
18524
18946
  isPointShape(style) {
18525
18947
  switch (style) {
18526
18948
  case "circle":
@@ -18539,8 +18961,7 @@ var DrawingSceneRenderer = class {
18539
18961
  }
18540
18962
  }
18541
18963
  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}`;
18964
+ ctx.font = this.labelFont(lb, fontPx);
18544
18965
  ctx.textAlign = lb.textAlign === "left" ? "left" : lb.textAlign === "right" ? "right" : "center";
18545
18966
  ctx.textBaseline = "middle";
18546
18967
  const lines = lb.text.split("\n");
@@ -18620,9 +19041,9 @@ var DrawingSceneRenderer = class {
18620
19041
  ctx.fill();
18621
19042
  }
18622
19043
  }
19044
+ /** Draw a bubble label; returns the bubble body rect (for tooltip hit-testing). */
18623
19045
  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}`;
19046
+ ctx.font = this.labelFont(lb, fontPx);
18626
19047
  const lines = (lb.text ?? "").split("\n");
18627
19048
  const padX = 6;
18628
19049
  const padY = 4;
@@ -18680,38 +19101,51 @@ var DrawingSceneRenderer = class {
18680
19101
  by = py - h - ptr;
18681
19102
  pointer = "down";
18682
19103
  }
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();
19104
+ if (!lb.noFill) {
19105
+ ctx.fillStyle = color;
19106
+ this.roundRect(ctx, bx, by, w, h, 4);
18706
19107
  ctx.fill();
19108
+ if (pointer !== "none") {
19109
+ ctx.beginPath();
19110
+ if (pointer === "down") {
19111
+ ctx.moveTo(px - ptr, by + h);
19112
+ ctx.lineTo(px + ptr, by + h);
19113
+ ctx.lineTo(px, py);
19114
+ } else if (pointer === "up") {
19115
+ ctx.moveTo(px - ptr, by);
19116
+ ctx.lineTo(px + ptr, by);
19117
+ ctx.lineTo(px, py);
19118
+ } else if (pointer === "left") {
19119
+ ctx.moveTo(bx, py - ptr);
19120
+ ctx.lineTo(bx, py + ptr);
19121
+ ctx.lineTo(px, py);
19122
+ } else {
19123
+ ctx.moveTo(bx + w, py - ptr);
19124
+ ctx.lineTo(bx + w, py + ptr);
19125
+ ctx.lineTo(px, py);
19126
+ }
19127
+ ctx.closePath();
19128
+ ctx.fill();
19129
+ }
18707
19130
  }
18708
19131
  if (lb.text) {
18709
- ctx.fillStyle = lb.textColor ?? contrastColor(color);
18710
- ctx.textAlign = "center";
19132
+ ctx.fillStyle = lb.textColor ?? (lb.noFill ? this.deps.theme.textColor : contrastColor(color));
18711
19133
  ctx.textBaseline = "middle";
19134
+ let tx;
19135
+ if (lb.textAlign === "left") {
19136
+ ctx.textAlign = "left";
19137
+ tx = bx + padX;
19138
+ } else if (lb.textAlign === "right") {
19139
+ ctx.textAlign = "right";
19140
+ tx = bx + w - padX;
19141
+ } else {
19142
+ ctx.textAlign = "center";
19143
+ tx = bx + w / 2;
19144
+ }
18712
19145
  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);
19146
+ for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], tx, startY + i * lineH);
18714
19147
  }
19148
+ return { x: bx, y: by, w, h };
18715
19149
  }
18716
19150
  roundRect(ctx, x, y, w, h, r) {
18717
19151
  const rr = Math.min(r, w / 2, h / 2);
@@ -18949,6 +19383,8 @@ var ChromeRenderer = class {
18949
19383
  this.axisTextColor = DARK_THEME.textColor;
18950
19384
  // Shared Pine-drawing renderer (line/box/label/polyline/linefill); widthCache persists.
18951
19385
  this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
19386
+ // Tooltip hit-rects of every label drawn this frame, in plot coords (rebuilt per render).
19387
+ this.labelTips = [];
18952
19388
  }
18953
19389
  mount(canvas) {
18954
19390
  this.canvas = canvas;
@@ -18990,6 +19426,7 @@ var ChromeRenderer = class {
18990
19426
  const dataW = coords.width;
18991
19427
  const dataH = coords.height;
18992
19428
  this.axisTextColor = surface?.textColor ?? theme.textColor;
19429
+ this.labelTips = [];
18993
19430
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
18994
19431
  ctx.clearRect(0, 0, fullW, fullH);
18995
19432
  if (surface && (fullW > dataW || fullH > dataH)) {
@@ -19099,6 +19536,17 @@ var ChromeRenderer = class {
19099
19536
  (price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
19100
19537
  );
19101
19538
  ctx.restore();
19539
+ for (const r of this.drawScene.labelTipRegions()) {
19540
+ this.labelTips.push({ ...r, top: r.top + pane.bounds.top, bottom: r.bottom + pane.bounds.top });
19541
+ }
19542
+ }
19543
+ /** Tooltip of the topmost label under a plot-space point, or null. Fed by the last render. */
19544
+ labelTooltipAt(x, y) {
19545
+ for (let i = this.labelTips.length - 1; i >= 0; i -= 1) {
19546
+ const r = this.labelTips[i];
19547
+ if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return r.text;
19548
+ }
19549
+ return null;
19102
19550
  }
19103
19551
  // ── axes ──
19104
19552
  drawPriceAxes(ctx, scene, coords, theme, dataW, panes) {
@@ -19315,6 +19763,68 @@ function formatCountdown(ms) {
19315
19763
  return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
19316
19764
  }
19317
19765
 
19766
+ // src/renderers/native/chrome/LabelTooltip.ts
19767
+ var HOVER_DELAY_MS = 350;
19768
+ var LabelTooltip = class {
19769
+ constructor(plot, deps) {
19770
+ this.plot = plot;
19771
+ this.deps = deps;
19772
+ this.tip = null;
19773
+ this.timer = null;
19774
+ /** Text of the currently open OR armed tip — dedupes moves inside one label. */
19775
+ this.current = null;
19776
+ this.onMove = (e) => {
19777
+ if (e.pointerType !== "mouse") return;
19778
+ const rect = this.plot.getBoundingClientRect();
19779
+ const x = e.clientX - rect.left;
19780
+ const y = e.clientY - rect.top;
19781
+ const text = this.deps.lookup(x, y);
19782
+ if (!text) {
19783
+ this.clear();
19784
+ return;
19785
+ }
19786
+ if (text === this.current) return;
19787
+ this.clear();
19788
+ this.current = text;
19789
+ this.timer = window.setTimeout(() => this.show(text, x, y), HOVER_DELAY_MS);
19790
+ };
19791
+ this.onLeave = () => {
19792
+ this.clear();
19793
+ };
19794
+ plot.addEventListener("pointermove", this.onMove);
19795
+ plot.addEventListener("pointerleave", this.onLeave);
19796
+ plot.addEventListener("pointerdown", this.onLeave);
19797
+ }
19798
+ destroy() {
19799
+ this.plot.removeEventListener("pointermove", this.onMove);
19800
+ this.plot.removeEventListener("pointerleave", this.onLeave);
19801
+ this.plot.removeEventListener("pointerdown", this.onLeave);
19802
+ this.clear();
19803
+ }
19804
+ show(text, x, y) {
19805
+ const doc = this.plot.ownerDocument;
19806
+ const tip = doc.createElement("div");
19807
+ tip.textContent = text;
19808
+ 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;";
19809
+ applyChromeTokens(tip, this.deps.theme());
19810
+ this.plot.appendChild(tip);
19811
+ const pw = this.plot.clientWidth;
19812
+ const ph = this.plot.clientHeight;
19813
+ tip.style.left = `${Math.max(0, Math.min(x + 12, pw - tip.offsetWidth - 4))}px`;
19814
+ tip.style.top = `${Math.max(0, Math.min(y + 16, ph - tip.offsetHeight - 4))}px`;
19815
+ this.tip = tip;
19816
+ }
19817
+ clear() {
19818
+ if (this.timer !== null) {
19819
+ clearTimeout(this.timer);
19820
+ this.timer = null;
19821
+ }
19822
+ this.tip?.remove();
19823
+ this.tip = null;
19824
+ this.current = null;
19825
+ }
19826
+ };
19827
+
19318
19828
  // src/renderers/native/chrome/CrosshairRenderer.ts
19319
19829
  var CrosshairRenderer = class {
19320
19830
  constructor() {
@@ -25106,19 +25616,8 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
25106
25616
  for (const model of models) {
25107
25617
  const off = offsetOf(model.id);
25108
25618
  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
- }
25619
+ if (s.overlay === true) continue;
25620
+ considerSeries(s, i0, i1, off, consider);
25122
25621
  }
25123
25622
  for (const pl of model.priceLines) consider(pl.price);
25124
25623
  }
@@ -25140,6 +25639,36 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
25140
25639
  const span = max - min;
25141
25640
  return { min: min - span * MARGIN_BOTTOM, max: max + span * MARGIN_TOP };
25142
25641
  }
25642
+ function considerSeries(s, i0, i1, off, consider) {
25643
+ if (s.kind === "candle" || s.kind === "bar") {
25644
+ for (let i = i0; i <= i1; i += 1) {
25645
+ const b = s.bars[i - off];
25646
+ if (b) {
25647
+ consider(b.high);
25648
+ consider(b.low);
25649
+ }
25650
+ }
25651
+ } else if (isLineLikeSeries(s)) {
25652
+ for (let i = i0; i <= i1; i += 1) consider(s.points[i - off]?.value);
25653
+ if (s.kind === "histogram" || s.kind === "columns") consider(s.style?.base ?? 0);
25654
+ else if (s.style?.base != null) consider(s.style.base);
25655
+ }
25656
+ }
25657
+ function overlaySeriesRange(models, i0, i1, offsetOf = () => 0) {
25658
+ let min = Infinity;
25659
+ let max = -Infinity;
25660
+ const consider = (v) => {
25661
+ if (v != null && Number.isFinite(v)) {
25662
+ if (v < min) min = v;
25663
+ if (v > max) max = v;
25664
+ }
25665
+ };
25666
+ for (const model of models) {
25667
+ const off = offsetOf(model.id);
25668
+ for (const s of model.series) if (s.overlay === true) considerSeries(s, i0, i1, off, consider);
25669
+ }
25670
+ return min === Infinity ? null : { min, max };
25671
+ }
25143
25672
  function expandScaleByPixels(scale, heightPx, abovePx, belowPx) {
25144
25673
  if (abovePx <= 0 && belowPx <= 0) return scale;
25145
25674
  const content = heightPx - abovePx - belowPx;
@@ -25922,6 +26451,8 @@ var NativeRenderer = class {
25922
26451
  this.glowAmount = 0;
25923
26452
  // WebGL2 neon-glow intensity (canvas2d ignores it)
25924
26453
  this.chrome = new ChromeRenderer();
26454
+ /** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
26455
+ this.labelTooltip = null;
25925
26456
  this.crosshairLayer = new CrosshairRenderer();
25926
26457
  /** 1 Hz repaint pump so the price-axis countdown-to-bar-close ticks; null when off. */
25927
26458
  this.countdownTimer = null;
@@ -25932,6 +26463,8 @@ var NativeRenderer = class {
25932
26463
  this.indicatorValuesOn = true;
25933
26464
  /** Host-contributed legend actions — held here so a rebuild of the legend re-wires them. */
25934
26465
  this.legendActionsProvider = null;
26466
+ /** Host-contributed legend callouts — held here so a rebuild of the legend re-wires them. */
26467
+ this.legendCalloutsProvider = null;
25935
26468
  /** Host override of the legend's fold toggle — held here so a remount re-applies it. */
25936
26469
  this.legendOverviewAction = null;
25937
26470
  // ── keyboard navigation / accessibility (item 11) ──
@@ -27040,6 +27573,10 @@ var NativeRenderer = class {
27040
27573
  this.plot.appendChild(this.scrollButton);
27041
27574
  this.plot.addEventListener("pointermove", this.onScrollProximityMove);
27042
27575
  this.plot.addEventListener("pointerleave", this.onScrollProximityLeave);
27576
+ this.labelTooltip = new LabelTooltip(this.plot, {
27577
+ theme: () => this.chromeTheme(),
27578
+ lookup: (x, y) => this.chrome.labelTooltipAt(x, y)
27579
+ });
27043
27580
  this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
27044
27581
  projector: () => this.drawingProjector(),
27045
27582
  dpr: () => this.coords.dpr,
@@ -27079,9 +27616,10 @@ var NativeRenderer = class {
27079
27616
  this.inputsUI.setDialogHost(this.dialogHost);
27080
27617
  this.inputsUI.setSymbolPicker(this.symbolPicker);
27081
27618
  this.inputsUI.setLegendActions(this.legendActionsProvider);
27619
+ this.inputsUI.setLegendCallouts(this.legendCalloutsProvider);
27082
27620
  this.inputsUI.setLegendOverviewAction(this.legendOverviewAction);
27083
27621
  this.inputsUI.setOnChange((c) => {
27084
- for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value });
27622
+ for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value, ...c.kind ? { kind: c.kind } : {} });
27085
27623
  });
27086
27624
  this.inputsUI.setOnRemove((id) => {
27087
27625
  for (const cb of this.removeIndicatorCbs) cb(id);
@@ -27271,6 +27809,8 @@ var NativeRenderer = class {
27271
27809
  this.settingsButton = null;
27272
27810
  this.plot?.removeEventListener("pointermove", this.onScrollProximityMove);
27273
27811
  this.plot?.removeEventListener("pointerleave", this.onScrollProximityLeave);
27812
+ this.labelTooltip?.destroy();
27813
+ this.labelTooltip = null;
27274
27814
  this.scrollButton?.remove();
27275
27815
  this.scrollButton = null;
27276
27816
  for (const l of this.extLayers) l.instance.destroy?.();
@@ -27511,7 +28051,7 @@ var NativeRenderer = class {
27511
28051
  else this.scene.assignIndicatorZ(model.id);
27512
28052
  this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
27513
28053
  native: !!model.native,
27514
- ...model.shorttitle ? { settingsTitle: model.title } : {}
28054
+ ...model.props ? { props: model.props, propValues: model.propValues ?? {} } : {}
27515
28055
  });
27516
28056
  this.syncTables(model);
27517
28057
  if (model.native?.type === "volume") {
@@ -27562,8 +28102,8 @@ var NativeRenderer = class {
27562
28102
  this.paneControls?.refresh();
27563
28103
  this.scheduler.invalidate(4 /* Full */);
27564
28104
  }
27565
- setIndicatorInputs(handle, values) {
27566
- this.inputsUI.setValues(handle.id, values);
28105
+ setIndicatorInputs(handle, values, props) {
28106
+ this.inputsUI.setValues(handle.id, values, props);
27567
28107
  }
27568
28108
  setSymbolPicker(picker) {
27569
28109
  this.symbolPicker = picker;
@@ -27573,6 +28113,10 @@ var NativeRenderer = class {
27573
28113
  this.legendActionsProvider = provider;
27574
28114
  this.inputsUI?.setLegendActions(provider);
27575
28115
  }
28116
+ setLegendCallouts(provider) {
28117
+ this.legendCalloutsProvider = provider;
28118
+ this.inputsUI?.setLegendCallouts(provider);
28119
+ }
27576
28120
  setLegendOverviewAction(action) {
27577
28121
  this.legendOverviewAction = action;
27578
28122
  this.inputsUI?.setLegendOverviewAction(action);
@@ -28553,7 +29097,11 @@ var NativeRenderer = class {
28553
29097
  }
28554
29098
  const models = this.scene.indicatorsForPane(pane.id);
28555
29099
  const masterModels = models.filter((m) => m.ownScale !== true);
28556
- const dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
29100
+ let dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
29101
+ if (pane === pricePane) {
29102
+ const or = overlaySeriesRange(this.scene.indicators.values(), i0, i1, (id) => this.scene.offsetOf(id));
29103
+ if (or) dr = dr ? { min: Math.min(dr.min, or.min), max: Math.max(dr.max, or.max) } : or;
29104
+ }
28557
29105
  const includeCandles = pane.kind === "price" && !this.scene.candlesHidden;
28558
29106
  pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
28559
29107
  pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
@@ -28827,6 +29375,7 @@ var NativeRenderer = class {
28827
29375
  for (const m of models) {
28828
29376
  const off = this.scene.offsetOf(m.id);
28829
29377
  for (const s of m.series) {
29378
+ if (s.overlay === true) continue;
28830
29379
  if (isLineLikeSeries(s)) {
28831
29380
  const v = s.points[i0 - off]?.value;
28832
29381
  if (v != null && Number.isFinite(v)) return v;
@@ -29732,6 +30281,19 @@ function unregisterLegendAction(id) {
29732
30281
  function legendActions() {
29733
30282
  return [...legendRegistry.values()].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
29734
30283
  }
30284
+ var calloutRegistry = /* @__PURE__ */ new Map();
30285
+ function registerLegendCallout(desc) {
30286
+ calloutRegistry.set(desc.id, desc);
30287
+ return () => {
30288
+ if (calloutRegistry.get(desc.id) === desc) calloutRegistry.delete(desc.id);
30289
+ };
30290
+ }
30291
+ function unregisterLegendCallout(id) {
30292
+ calloutRegistry.delete(id);
30293
+ }
30294
+ function legendCallouts() {
30295
+ return [...calloutRegistry.values()].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
30296
+ }
29735
30297
  var stateHandlers = /* @__PURE__ */ new Map();
29736
30298
  function registerStatePersistence(handler) {
29737
30299
  stateHandlers.set(handler.key, handler);
@@ -29840,6 +30402,7 @@ exports.getNativeIndicator = getNativeIndicator;
29840
30402
  exports.iconMarkup = iconMarkup;
29841
30403
  exports.inputVisible = inputVisible;
29842
30404
  exports.legendActions = legendActions;
30405
+ exports.legendCallouts = legendCallouts;
29843
30406
  exports.nativeIndicatorDescriptors = nativeIndicatorDescriptors;
29844
30407
  exports.nativeIndicatorTypes = nativeIndicatorTypes;
29845
30408
  exports.normalizeSettingsRow = normalizeSettingsRow;
@@ -29848,6 +30411,7 @@ exports.registerDefaultEngine = registerDefaultEngine;
29848
30411
  exports.registerDrawingType = registerDrawingType;
29849
30412
  exports.registerIcon = registerIcon;
29850
30413
  exports.registerLegendAction = registerLegendAction;
30414
+ exports.registerLegendCallout = registerLegendCallout;
29851
30415
  exports.registerNativeIndicator = registerNativeIndicator;
29852
30416
  exports.registerRendererDefaults = registerRendererDefaults;
29853
30417
  exports.registerRendererLayer = registerRendererLayer;
@@ -29872,6 +30436,7 @@ exports.topbarActionOverride = topbarActionOverride;
29872
30436
  exports.unregisterChartType = unregisterChartType;
29873
30437
  exports.unregisterDefaultEngine = unregisterDefaultEngine;
29874
30438
  exports.unregisterLegendAction = unregisterLegendAction;
30439
+ exports.unregisterLegendCallout = unregisterLegendCallout;
29875
30440
  exports.unregisterNativeIndicator = unregisterNativeIndicator;
29876
30441
  exports.unregisterRendererDefaults = unregisterRendererDefaults;
29877
30442
  exports.unregisterRendererLayer = unregisterRendererLayer;