@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
@@ -114,6 +114,7 @@ var Vela = (function (exports) {
114
114
  constructor(id, title, controller, source) {
115
115
  this.controller = controller;
116
116
  this.schema = [];
117
+ this.propsSchema = [];
117
118
  this.visibleState = true;
118
119
  this.bus = new TypedEventBus();
119
120
  this.id = id;
@@ -123,6 +124,9 @@ var Vela = (function (exports) {
123
124
  get inputs() {
124
125
  return this.schema;
125
126
  }
127
+ get props() {
128
+ return this.propsSchema;
129
+ }
126
130
  get visible() {
127
131
  return this.visibleState;
128
132
  }
@@ -132,6 +136,12 @@ var Vela = (function (exports) {
132
136
  setInputs(values) {
133
137
  this.controller.applyInputs(this.id, values);
134
138
  }
139
+ setProp(key, value) {
140
+ this.controller.applyProps(this.id, { [key]: value });
141
+ }
142
+ setProps(values) {
143
+ this.controller.applyProps(this.id, values);
144
+ }
135
145
  setVisible(visible) {
136
146
  this.controller.setVisible(this.id, visible);
137
147
  }
@@ -151,6 +161,9 @@ var Vela = (function (exports) {
151
161
  setSchema(schema) {
152
162
  this.schema = schema;
153
163
  }
164
+ setPropsSchema(schema) {
165
+ this.propsSchema = schema;
166
+ }
154
167
  /** Sync the public `visible` getter when the orchestrator changes visibility (API or legend eye). */
155
168
  setVisibleState(visible) {
156
169
  this.visibleState = visible;
@@ -164,6 +177,8 @@ var Vela = (function (exports) {
164
177
  function summarizeModel(model) {
165
178
  const series = {};
166
179
  for (const s of model.series) series[s.kind] = (series[s.kind] ?? 0) + 1;
180
+ const countOverlay = (items) => items?.filter((i) => i.overlay === true).length ?? 0;
181
+ const forcedOverlay = countOverlay(model.series) + countOverlay(model.fills) + countOverlay(model.backgrounds) + countOverlay(model.lines) + countOverlay(model.boxes) + countOverlay(model.labels) + countOverlay(model.polylines) + countOverlay(model.linefills) + countOverlay(model.tables);
167
182
  return {
168
183
  id: model.id,
169
184
  title: model.title,
@@ -183,7 +198,8 @@ var Vela = (function (exports) {
183
198
  tables: model.tables?.length ?? 0,
184
199
  barColors: model.barColors?.length ?? 0,
185
200
  trades: model.trades?.length ?? 0,
186
- inputs: model.inputs.length
201
+ inputs: model.inputs.length,
202
+ forcedOverlay
187
203
  };
188
204
  }
189
205
  function inspectModels(models) {
@@ -204,7 +220,8 @@ var Vela = (function (exports) {
204
220
  linefills: sum((s) => s.linefills),
205
221
  tables: sum((s) => s.tables),
206
222
  barColors: sum((s) => s.barColors),
207
- trades: sum((s) => s.trades)
223
+ trades: sum((s) => s.trades),
224
+ forcedOverlay: sum((s) => s.forcedOverlay)
208
225
  }
209
226
  };
210
227
  }
@@ -474,11 +491,11 @@ var Vela = (function (exports) {
474
491
  );
475
492
  registerIcon(
476
493
  "market-pre",
477
- svg24('<path d="M12 2v8"/><path d="m4.93 10.93 1.41 1.41"/><path d="M2 18h2"/><path d="M20 18h2"/><path d="m19.07 10.93-1.41 1.41"/><path d="M22 22H2"/><path d="m16 6-4 4-4-4"/><path d="M16 18a4 4 0 0 0-8 0"/>')
494
+ svg24('<path d="M12 2v8"/><path d="m4.93 10.93 1.41 1.41"/><path d="M2 18h2"/><path d="M20 18h2"/><path d="m19.07 10.93-1.41 1.41"/><path d="M22 22H2"/><path d="m8 6 4-4 4 4"/><path d="M16 18a4 4 0 0 0-8 0"/>')
478
495
  );
479
496
  registerIcon(
480
497
  "market-post",
481
- svg24('<path d="M12 10V2"/><path d="m4.93 10.93 1.41 1.41"/><path d="M2 18h2"/><path d="M20 18h2"/><path d="m19.07 10.93-1.41 1.41"/><path d="M22 22H2"/><path d="m16 18-4-4-4 4"/><path d="M16 6a4 4 0 0 0-8 0"/>')
498
+ svg24('<path d="M12 10V2"/><path d="m4.93 10.93 1.41 1.41"/><path d="M2 18h2"/><path d="M20 18h2"/><path d="m19.07 10.93-1.41 1.41"/><path d="M22 22H2"/><path d="m16 14-4 4-4-4"/><path d="M16 6a4 4 0 0 0-8 0"/>')
482
499
  );
483
500
  registerIcon("market-closed", svg24('<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>'));
484
501
  registerIcon(
@@ -7318,7 +7335,9 @@ var Vela = (function (exports) {
7318
7335
  for (const engine of engines) this.registerEngine(engine.language, engine);
7319
7336
  this.defaultLanguage = config.defaultLanguage;
7320
7337
  this.renderer.mount(container, config.theme);
7321
- this.renderer.onInputChange((e) => this.applyInputs(e.indicatorId, { [e.key]: e.value }));
7338
+ this.renderer.onInputChange(
7339
+ (e) => e.kind === "prop" ? this.applyProps(e.indicatorId, { [e.key]: e.value }) : this.applyInputs(e.indicatorId, { [e.key]: e.value })
7340
+ );
7322
7341
  this.renderer.onRemoveIndicator((id) => this.removeIndicator(id));
7323
7342
  this.renderer.onToggleIndicatorVisible?.((id, visible) => this.setVisible(id, visible));
7324
7343
  this.paneActionUnsub = this.renderer.onPaneAction?.((a) => this.handlePaneAction(a)) ?? null;
@@ -7998,7 +8017,7 @@ var Vela = (function (exports) {
7998
8017
  const title = options.title ?? "Indicator";
7999
8018
  const handle = new IndicatorHandleImpl(id, title, this, source);
8000
8019
  this.handles.set(id, handle);
8001
- this.registry.add({ id, title, source, options, inputValues: { ...options.inputs ?? {} } });
8020
+ this.registry.add({ id, title, source, options, inputValues: { ...options.inputs ?? {} }, propValues: { ...options.props ?? {} } });
8002
8021
  void this.startIndicator(id, source, options, handle);
8003
8022
  return handle;
8004
8023
  }
@@ -8021,7 +8040,7 @@ var Vela = (function (exports) {
8021
8040
  return handle;
8022
8041
  }
8023
8042
  const inputValues = { ...descriptor.defaultInputs(), ...options.inputs ?? {} };
8024
- this.registry.add({ id, title, source: type, inputValues, native: { type, instance: descriptor.create(), descriptor } });
8043
+ this.registry.add({ id, title, source: type, inputValues, propValues: {}, native: { type, instance: descriptor.create(), descriptor } });
8025
8044
  handle.setSchema(descriptor.inputsSchema());
8026
8045
  void this.startNativeIndicator(id, handle);
8027
8046
  return handle;
@@ -8074,6 +8093,19 @@ var Vela = (function (exports) {
8074
8093
  if (record.session) record.session.update(record.inputValues);
8075
8094
  else if (record.native && !record.hidden) record.native.instance.setInputs(record.inputValues);
8076
8095
  }
8096
+ /** IndicatorController: re-run an indicator with merged declaration-prop overrides.
8097
+ * Same lifecycle as {@link applyInputs} — a prop change replays the whole script.
8098
+ * Script indicators only: natives have no declaration props. */
8099
+ applyProps(id, values) {
8100
+ const record = this.registry.get(id);
8101
+ if (!record?.session) return;
8102
+ record.propValues = { ...record.propValues, ...values };
8103
+ if (record.renderHandle) this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
8104
+ record.pendingStructural = true;
8105
+ if (!record.hidden) this.setLoading(record, true);
8106
+ record.pendingCause = "inputs";
8107
+ record.session.update(record.inputValues, record.propValues);
8108
+ }
8077
8109
  /** IndicatorController: tear down an indicator and (if now empty) its pane. */
8078
8110
  /** Live handles of every indicator on the chart (script + native), insertion order. */
8079
8111
  listIndicators() {
@@ -8222,6 +8254,10 @@ var Vela = (function (exports) {
8222
8254
  for (const input of prepared.inputs) defaults2[input.key] = input.defval;
8223
8255
  record.inputValues = { ...defaults2, ...record.inputValues };
8224
8256
  handle.setSchema(prepared.inputs);
8257
+ const propDefaults = {};
8258
+ for (const prop of prepared.props ?? []) propDefaults[prop.key] = prop.defval;
8259
+ record.propValues = { ...propDefaults, ...record.propValues };
8260
+ handle.setPropsSchema(prepared.props ?? []);
8225
8261
  this.mountLoadingPlaceholder(id, record);
8226
8262
  this.executeIndicator(id, handle);
8227
8263
  } catch (err) {
@@ -8245,6 +8281,7 @@ var Vela = (function (exports) {
8245
8281
  getBars: () => this.bars,
8246
8282
  fetchSeries: (sym, tf, range) => this.fetchSeries(sym, tf, range),
8247
8283
  inputs: record.inputValues,
8284
+ props: record.propValues,
8248
8285
  visibleRange: this.currentVisibleRange(),
8249
8286
  mode,
8250
8287
  // Mid-backfill session starts (add / re-show / price-style re-execute) let the
@@ -8338,6 +8375,9 @@ var Vela = (function (exports) {
8338
8375
  const meta = record.prepared.meta;
8339
8376
  const model = {
8340
8377
  id,
8378
+ // Deliberately the FULL title, never a shorttitle: while the script loads the
8379
+ // legend identifies it by its full name; the compact shorttitle arrives with
8380
+ // the first computed model and takes over from there.
8341
8381
  title: record.options?.title ?? meta.title,
8342
8382
  overlay: meta.overlay,
8343
8383
  paneHint: meta.overlay ? "price" : "new",
@@ -8346,7 +8386,8 @@ var Vela = (function (exports) {
8346
8386
  backgrounds: [],
8347
8387
  priceLines: [],
8348
8388
  inputs: record.prepared.inputs,
8349
- inputValues: record.inputValues
8389
+ inputValues: record.inputValues,
8390
+ ...record.prepared.props ? { props: record.prepared.props, propValues: record.propValues } : {}
8350
8391
  };
8351
8392
  const paneId = this.routePane(id, model, record.options ?? {});
8352
8393
  this.placeModel(model, id, paneId);
@@ -8579,7 +8620,7 @@ var Vela = (function (exports) {
8579
8620
  record.model = model;
8580
8621
  if (record.pendingStructural) {
8581
8622
  record.renderHandle = this.renderer.mountIndicator(model);
8582
- this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues);
8623
+ this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
8583
8624
  record.pendingStructural = false;
8584
8625
  } else {
8585
8626
  this.renderer.updateIndicator(record.renderHandle, modelToValuePatch(model));
@@ -8606,16 +8647,17 @@ var Vela = (function (exports) {
8606
8647
  placeModel(model, id, paneId) {
8607
8648
  model.id = id;
8608
8649
  model.paneId = paneId;
8609
- for (const series of model.series) series.paneId = paneId;
8610
- for (const fill of model.fills) fill.paneId = paneId;
8611
- for (const bg of model.backgrounds) bg.paneId = paneId;
8650
+ const paneOf = (item) => item.overlay === true ? "price" : paneId;
8651
+ for (const series of model.series) series.paneId = paneOf(series);
8652
+ for (const fill of model.fills) fill.paneId = paneOf(fill);
8653
+ for (const bg of model.backgrounds) bg.paneId = paneOf(bg);
8612
8654
  for (const line of model.priceLines) line.paneId = paneId;
8613
- if (model.lines) for (const ln of model.lines) ln.paneId = paneId;
8614
- if (model.boxes) for (const bx of model.boxes) bx.paneId = paneId;
8615
- if (model.labels) for (const lb of model.labels) lb.paneId = paneId;
8616
- if (model.polylines) for (const pl of model.polylines) pl.paneId = paneId;
8617
- if (model.linefills) for (const lf of model.linefills) lf.paneId = paneId;
8618
- if (model.tables) for (const tb of model.tables) tb.paneId = paneId;
8655
+ if (model.lines) for (const ln of model.lines) ln.paneId = paneOf(ln);
8656
+ if (model.boxes) for (const bx of model.boxes) bx.paneId = paneOf(bx);
8657
+ if (model.labels) for (const lb of model.labels) lb.paneId = paneOf(lb);
8658
+ if (model.polylines) for (const pl of model.polylines) pl.paneId = paneOf(pl);
8659
+ if (model.linefills) for (const lf of model.linefills) lf.paneId = paneOf(lf);
8660
+ if (model.tables) for (const tb of model.tables) tb.paneId = paneOf(tb);
8619
8661
  }
8620
8662
  emitContextChanged(id) {
8621
8663
  if (!this.registry.get(id)?.session?.getContext) return;
@@ -8768,6 +8810,15 @@ var Vela = (function (exports) {
8768
8810
  setLegendActions(provider) {
8769
8811
  this.renderer.setLegendActions?.(provider);
8770
8812
  }
8813
+ /**
8814
+ * Wire the legend rows' HOST-CONTRIBUTED callout bubbles (the shells route the
8815
+ * plugin registry through this; see `registerLegendCallout`). Silent on a renderer
8816
+ * without the seam — contributed callouts simply never show there, same graceful
8817
+ * degradation as {@link setLegendActions}.
8818
+ */
8819
+ setLegendCallouts(provider) {
8820
+ this.renderer.setLegendCallouts?.(provider);
8821
+ }
8771
8822
  /**
8772
8823
  * Replace the indicator legend's fold toggle with a host action (or restore it with
8773
8824
  * `null`) — multi-chart shells point the chip at their indicator overview instead of
@@ -16606,13 +16657,6 @@ var Vela = (function (exports) {
16606
16657
  }
16607
16658
  };
16608
16659
 
16609
- // src/core/model/inputs.ts
16610
- function inputVisible(when, values) {
16611
- if (!when) return true;
16612
- const conds = Array.isArray(when) ? when : [when];
16613
- return conds.every((c) => c.anyOf ? c.anyOf.some((x) => x === values[c.key]) : values[c.key] === c.equals);
16614
- }
16615
-
16616
16660
  // src/ui/tokens.ts
16617
16661
  var STATIC_ID = "vela-ui-tokens";
16618
16662
  var STATIC_DECLS = Object.entries(STATIC_TOKENS).map(([k, v]) => ` ${k}: ${v};`).join("\n");
@@ -16858,6 +16902,199 @@ ${STATIC_DECLS}
16858
16902
  }
16859
16903
  };
16860
16904
 
16905
+ // src/ui/components/callout-bubble/controller.ts
16906
+ function closesPanel(item) {
16907
+ return item.close !== false;
16908
+ }
16909
+ function calloutPanelRows(items) {
16910
+ const rows = [];
16911
+ for (const item of items) {
16912
+ if (item.type === "text") {
16913
+ rows.push({ type: "text", text: item.text });
16914
+ continue;
16915
+ }
16916
+ const last2 = rows[rows.length - 1];
16917
+ if (last2 && last2.type === "buttons") last2.buttons.push(item);
16918
+ else rows.push({ type: "buttons", buttons: [item] });
16919
+ }
16920
+ return rows;
16921
+ }
16922
+
16923
+ // src/ui/components/callout-bubble/styles.ts
16924
+ var CALLOUT_STYLE_ID = "vela-ui-callout-bubble";
16925
+ var CALLOUT_CSS = `
16926
+ .vela-callout {
16927
+ display: inline-grid;
16928
+ place-items: center;
16929
+ border-radius: 50%;
16930
+ flex: none;
16931
+ line-height: 0;
16932
+ box-sizing: border-box;
16933
+ cursor: default;
16934
+ user-select: none;
16935
+ -webkit-user-select: none;
16936
+ }
16937
+ .vela-callout[role='button'] { cursor: pointer; }
16938
+ .vela-callout svg { display: block; }
16939
+ /* The deployed panel \u2014 carries the kit's elevated-card look itself (the popover
16940
+ shell is bare positioning chrome). */
16941
+ .vela-callout-panel {
16942
+ display: flex;
16943
+ flex-direction: column;
16944
+ gap: 8px;
16945
+ padding: 10px 12px;
16946
+ max-width: 280px;
16947
+ box-sizing: border-box;
16948
+ background: var(--vela-surface-elev);
16949
+ border: 1px solid var(--vela-border-strong);
16950
+ border-radius: 6px;
16951
+ box-shadow: var(--vela-shadow);
16952
+ color: var(--vela-fg);
16953
+ font: var(--vela-font-size-md) var(--vela-font);
16954
+ }
16955
+ .vela-callout-title { font-weight: 600; color: var(--vela-fg-bright); }
16956
+ .vela-callout-text { color: var(--vela-fg-muted); line-height: 1.45; white-space: pre-line; }
16957
+ .vela-callout-actions { display: flex; flex-wrap: wrap; gap: 8px; }
16958
+ .vela-callout-btn {
16959
+ cursor: pointer;
16960
+ height: 26px;
16961
+ padding: 0 10px;
16962
+ border-radius: 5px;
16963
+ border: 1px solid var(--vela-border);
16964
+ background: transparent;
16965
+ color: var(--vela-fg);
16966
+ font-size: var(--vela-font-size-md);
16967
+ font-family: inherit;
16968
+ 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;
16969
+ }
16970
+ .vela-callout-btn:hover { background: var(--vela-hover); color: var(--vela-fg-bright); border-color: var(--vela-fg-muted); }
16971
+ .vela-callout-btn-primary { border-color: var(--vela-selected-bg); background: var(--vela-selected-bg); color: var(--vela-selected-fg); }
16972
+ .vela-callout-btn-primary:hover { background: var(--vela-selected-bg); color: var(--vela-selected-fg); opacity: 0.85; border-color: var(--vela-selected-bg); }
16973
+ `;
16974
+
16975
+ // src/ui/components/callout-bubble/view.ts
16976
+ var CalloutBubble = class {
16977
+ constructor(opts) {
16978
+ this.pop = null;
16979
+ this.spec = { icon: opts.icon, background: opts.background, label: opts.label };
16980
+ if (opts.color !== void 0) this.spec.color = opts.color;
16981
+ if (opts.panel !== void 0) this.spec.panel = opts.panel;
16982
+ this.size = opts.size ?? 16;
16983
+ this.host = opts.host;
16984
+ this.theme = opts.theme;
16985
+ this.boundary = opts.boundary;
16986
+ const doc = (opts.host ?? document.body).ownerDocument;
16987
+ injectStyles(CALLOUT_STYLE_ID, CALLOUT_CSS, doc);
16988
+ this.el = doc.createElement("span");
16989
+ this.el.className = "vela-callout";
16990
+ this.el.style.width = `${this.size}px`;
16991
+ this.el.style.height = `${this.size}px`;
16992
+ this.el.addEventListener("click", (e) => {
16993
+ if (!this.spec.panel) return;
16994
+ e.stopPropagation();
16995
+ this.toggle();
16996
+ });
16997
+ this.el.addEventListener("dblclick", (e) => {
16998
+ if (this.spec.panel) e.stopPropagation();
16999
+ });
17000
+ this.el.addEventListener("keydown", (e) => {
17001
+ if (!this.spec.panel || e.key !== "Enter" && e.key !== " ") return;
17002
+ e.preventDefault();
17003
+ this.toggle();
17004
+ });
17005
+ this.dress();
17006
+ }
17007
+ /** Re-dress the bubble (a status change: new icon, tint, label, panel). */
17008
+ set(spec) {
17009
+ this.spec = { ...this.spec, ...spec };
17010
+ if ("panel" in spec) this.pop?.hide();
17011
+ this.dress();
17012
+ }
17013
+ /** Whether the deployed panel is currently open. */
17014
+ get open() {
17015
+ return this.pop?.open ?? false;
17016
+ }
17017
+ destroy() {
17018
+ this.pop?.destroy();
17019
+ this.pop = null;
17020
+ this.el.remove();
17021
+ }
17022
+ dress() {
17023
+ const clickable = this.spec.panel !== void 0;
17024
+ this.el.style.background = this.spec.background;
17025
+ this.el.style.color = this.spec.color ?? "";
17026
+ this.el.innerHTML = iconAt(this.spec.icon, this.size - 4);
17027
+ this.el.setAttribute("aria-label", this.spec.label);
17028
+ if (clickable) {
17029
+ this.el.setAttribute("role", "button");
17030
+ this.el.tabIndex = 0;
17031
+ } else {
17032
+ this.el.removeAttribute("role");
17033
+ this.el.removeAttribute("tabindex");
17034
+ }
17035
+ }
17036
+ toggle() {
17037
+ if (this.pop?.open) {
17038
+ this.pop.hide();
17039
+ return;
17040
+ }
17041
+ const panel = this.spec.panel;
17042
+ if (!panel) return;
17043
+ this.pop?.destroy();
17044
+ this.pop = new Popover({
17045
+ trigger: this.el,
17046
+ gap: 6,
17047
+ content: (body) => this.buildPanel(body, panel),
17048
+ ...this.host ? { host: this.host } : {},
17049
+ ...this.theme ? { theme: this.theme() } : {},
17050
+ ...this.boundary ? { boundary: this.boundary } : {}
17051
+ });
17052
+ this.pop.show();
17053
+ }
17054
+ buildPanel(body, panel) {
17055
+ const doc = body.ownerDocument;
17056
+ const root = doc.createElement("div");
17057
+ root.className = "vela-callout-panel";
17058
+ if (panel.title) {
17059
+ const title = doc.createElement("div");
17060
+ title.className = "vela-callout-title";
17061
+ title.textContent = panel.title;
17062
+ root.appendChild(title);
17063
+ }
17064
+ for (const row of calloutPanelRows(panel.items)) {
17065
+ if (row.type === "text") {
17066
+ const text = doc.createElement("div");
17067
+ text.className = "vela-callout-text";
17068
+ text.textContent = row.text;
17069
+ root.appendChild(text);
17070
+ continue;
17071
+ }
17072
+ const actions = doc.createElement("div");
17073
+ actions.className = "vela-callout-actions";
17074
+ for (const item of row.buttons) {
17075
+ const btn2 = doc.createElement("button");
17076
+ btn2.type = "button";
17077
+ btn2.className = "vela-callout-btn" + (item.primary ? " vela-callout-btn-primary" : "");
17078
+ btn2.textContent = item.label;
17079
+ btn2.addEventListener("click", () => {
17080
+ item.run();
17081
+ if (closesPanel(item)) this.pop?.hide();
17082
+ });
17083
+ actions.appendChild(btn2);
17084
+ }
17085
+ root.appendChild(actions);
17086
+ }
17087
+ body.appendChild(root);
17088
+ }
17089
+ };
17090
+
17091
+ // src/core/model/inputs.ts
17092
+ function inputVisible(when, values) {
17093
+ if (!when) return true;
17094
+ const conds = Array.isArray(when) ? when : [when];
17095
+ return conds.every((c) => c.anyOf ? c.anyOf.some((x) => x === values[c.key]) : values[c.key] === c.equals);
17096
+ }
17097
+
16861
17098
  // src/ui/components/select/controller.ts
16862
17099
  function selectController(opts) {
16863
17100
  const options = opts.options;
@@ -19961,6 +20198,10 @@ ${overlayScrollbarCss(".vela-dialog *")}
19961
20198
  }
19962
20199
 
19963
20200
  // src/renderers/shared/IndicatorInputsDialog.ts
20201
+ var PROPS_TAB = "Properties";
20202
+ function bagOf(row, decl) {
20203
+ return decl.prop ? row.propValues : row.values;
20204
+ }
19964
20205
  var SOURCES = ["close", "open", "high", "low", "hl2", "hlc3", "ohlc4", "volume"];
19965
20206
  var IndicatorInputsDialog = class {
19966
20207
  constructor(host) {
@@ -19971,6 +20212,7 @@ ${overlayScrollbarCss(".vela-dialog *")}
19971
20212
  this.openId = null;
19972
20213
  this.row = null;
19973
20214
  this.snapshot = null;
20215
+ this.propSnapshot = null;
19974
20216
  this.dialogTips = [];
19975
20217
  /** Re-applies every `when` gate against current values; set while the dialog is open. */
19976
20218
  this.refreshVisibility = null;
@@ -19993,10 +20235,11 @@ ${overlayScrollbarCss(".vela-dialog *")}
19993
20235
  }
19994
20236
  open(row) {
19995
20237
  this.close();
19996
- if (row.inputs.length === 0) return;
20238
+ if (row.inputs.length === 0 && row.props.length === 0) return;
19997
20239
  this.row = row;
19998
20240
  this.openId = row.id;
19999
20241
  this.snapshot = { ...row.values };
20242
+ this.propSnapshot = { ...row.propValues };
20000
20243
  ensureDialogStyles();
20001
20244
  const t = this.host.theme();
20002
20245
  const border = "var(--vela-border)";
@@ -20004,7 +20247,7 @@ ${overlayScrollbarCss(".vela-dialog *")}
20004
20247
  const host = this.host.dialogHost() ?? this.host.container;
20005
20248
  const ui = new Dialog({
20006
20249
  host,
20007
- title: row.settingsTitle,
20250
+ title: row.title,
20008
20251
  // Non-modal: live-edit dialog — a modal machine locks pointer events on the
20009
20252
  // whole body, killing the chart and the body-portaled popovers.
20010
20253
  modal: false,
@@ -20036,6 +20279,16 @@ ${overlayScrollbarCss(".vela-dialog *")}
20036
20279
  text: () => "Close"
20037
20280
  }));
20038
20281
  const tabDefs = tabInputs(row.inputs);
20282
+ if (row.props.length > 0) {
20283
+ const propDecls = row.props.map((p) => {
20284
+ const d = { ...p, prop: true };
20285
+ delete d.tab;
20286
+ return d;
20287
+ });
20288
+ const existing = tabDefs.find((t2) => t2.name === PROPS_TAB);
20289
+ if (existing) existing.inputs.push(...propDecls);
20290
+ else tabDefs.push({ name: PROPS_TAB, inputs: propDecls });
20291
+ }
20039
20292
  const tabs = document.createElement("div");
20040
20293
  tabs.style.cssText = `display:flex;gap:12px;padding:0 20px;border-bottom:1px solid ${border};flex:0 0 auto;`;
20041
20294
  const tabEls = [];
@@ -20138,9 +20391,10 @@ ${overlayScrollbarCss(".vela-dialog *")}
20138
20391
  this.openId = null;
20139
20392
  this.row = null;
20140
20393
  this.snapshot = null;
20394
+ this.propSnapshot = null;
20141
20395
  ui?.destroy();
20142
20396
  }
20143
- /** Restore every input to its open-time value (re-running the indicator), then close. */
20397
+ /** Restore every input and prop to its open-time value (re-running the indicator), then close. */
20144
20398
  revertAndClose() {
20145
20399
  const row = this.row;
20146
20400
  const snap = this.snapshot;
@@ -20154,6 +20408,17 @@ ${overlayScrollbarCss(".vela-dialog *")}
20154
20408
  }
20155
20409
  }
20156
20410
  }
20411
+ const propSnap = this.propSnapshot;
20412
+ if (row && propSnap) {
20413
+ for (const p of row.props) {
20414
+ const snapped = propSnap[p.key];
20415
+ const before = snapped !== void 0 ? snapped : p.defval;
20416
+ if (row.propValues[p.key] !== before) {
20417
+ row.propValues[p.key] = before;
20418
+ this.host.onChange?.({ indicatorId: row.id, key: p.key, value: before, kind: "prop" });
20419
+ }
20420
+ }
20421
+ }
20157
20422
  this.close();
20158
20423
  }
20159
20424
  /** The footer's reset button — same chip as Cancel, pinned to the LEFT edge
@@ -20171,19 +20436,27 @@ ${overlayScrollbarCss(".vela-dialog *")}
20171
20436
  const row = this.row;
20172
20437
  if (!row) return;
20173
20438
  const snap = this.snapshot;
20439
+ const propSnap = this.propSnapshot;
20174
20440
  for (const inp of row.inputs) {
20175
20441
  if (row.values[inp.key] !== inp.defval) {
20176
20442
  row.values[inp.key] = inp.defval;
20177
20443
  this.host.onChange?.({ indicatorId: row.id, key: inp.key, value: inp.defval });
20178
20444
  }
20179
20445
  }
20446
+ for (const p of row.props) {
20447
+ if (row.propValues[p.key] !== p.defval) {
20448
+ row.propValues[p.key] = p.defval;
20449
+ this.host.onChange?.({ indicatorId: row.id, key: p.key, value: p.defval, kind: "prop" });
20450
+ }
20451
+ }
20180
20452
  this.open(row);
20181
20453
  if (snap) this.snapshot = snap;
20454
+ if (propSnap) this.propSnapshot = propSnap;
20182
20455
  }
20183
20456
  /** Write one edit through: store it, notify the host, and re-apply the `when` gates. */
20184
- commit(row, key, value) {
20185
- row.values[key] = value;
20186
- this.host.onChange?.({ indicatorId: row.id, key, value });
20457
+ commit(row, decl, value) {
20458
+ bagOf(row, decl)[decl.key] = value;
20459
+ this.host.onChange?.({ indicatorId: row.id, key: decl.key, value, ...decl.prop ? { kind: "prop" } : {} });
20187
20460
  this.refreshVisibility?.();
20188
20461
  }
20189
20462
  /** One settings row (or several `inline=` inputs) placed into a section's grid. */
@@ -20210,7 +20483,7 @@ ${overlayScrollbarCss(".vela-dialog *")}
20210
20483
  const id = idOf(lead);
20211
20484
  const info = lead.tooltip ? this.infoButton(lead.tooltip) : void 0;
20212
20485
  if (lead.type === "bool") {
20213
- const current = Boolean(row.values[lead.key] ?? lead.defval);
20486
+ const current = Boolean(bagOf(row, lead)[lead.key] ?? lead.defval);
20214
20487
  return append(fieldRow({
20215
20488
  label: nameOf(lead),
20216
20489
  id,
@@ -20219,7 +20492,7 @@ ${overlayScrollbarCss(".vela-dialog *")}
20219
20492
  toggle: {
20220
20493
  id,
20221
20494
  checked: current,
20222
- onChange: (v) => this.commit(row, lead.key, v)
20495
+ onChange: (v) => this.commit(row, lead, v)
20223
20496
  }
20224
20497
  }));
20225
20498
  }
@@ -20244,8 +20517,8 @@ ${overlayScrollbarCss(".vela-dialog *")}
20244
20517
  }
20245
20518
  /** Build the typed control for one input, committing edits live via `onChange`. */
20246
20519
  buildControl(row, inp, id) {
20247
- const current = row.values[inp.key] ?? inp.defval;
20248
- const emit = (value) => this.commit(row, inp.key, value);
20520
+ const current = bagOf(row, inp)[inp.key] ?? inp.defval;
20521
+ const emit = (value) => this.commit(row, inp, value);
20249
20522
  if (inp.type === "bool") {
20250
20523
  return buildFieldControl({ kind: "switch", id, checked: Boolean(current), onChange: (v) => emit(v) }).el;
20251
20524
  }
@@ -20256,7 +20529,7 @@ ${overlayScrollbarCss(".vela-dialog *")}
20256
20529
  kind: "color",
20257
20530
  id,
20258
20531
  theme: this.host.theme(),
20259
- get: () => String(row.values[inp.key] ?? inp.defval),
20532
+ get: () => String(bagOf(row, inp)[inp.key] ?? inp.defval),
20260
20533
  onChange: (v) => emit(v)
20261
20534
  }).el;
20262
20535
  }
@@ -20819,6 +21092,7 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
20819
21092
  }
20820
21093
 
20821
21094
  // src/renderers/shared/InputsUI.ts
21095
+ var LEGEND_AT_TOP_ATTR = "data-vela-pane-at-top";
20822
21096
  var STATUS_KEYFRAMES = "@keyframes vela-ind-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.3;transform:scale(.6)}}";
20823
21097
  var LEGEND_ICON_PX2 = 16;
20824
21098
  var LEGEND_CTL_PX = 18;
@@ -20857,10 +21131,14 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
20857
21131
  this.moveApi = null;
20858
21132
  /** Host-contributed legend actions, resolved PER ROW at render time (see setLegendActions). */
20859
21133
  this.legendActions = null;
21134
+ /** Host-contributed callout bubbles, resolved PER ROW at render time (see setLegendCallouts). */
21135
+ this.legendCallouts = null;
20860
21136
  /** Chrome-tooltip disposers, per row id — a tip open at removal must not outlive its row. */
20861
21137
  this.rowTips = /* @__PURE__ */ new Map();
20862
21138
  /** Same, for the contributed extras only (rebuilt independently by setLegendActions). */
20863
21139
  this.extrasTips = /* @__PURE__ */ new Map();
21140
+ /** Same, for the contributed callouts only (rebuilt independently by setLegendCallouts). */
21141
+ this.calloutTips = /* @__PURE__ */ new Map();
20864
21142
  /** Open "Move to" menu (kept so it can be torn down). */
20865
21143
  this.moveMenu = null;
20866
21144
  this.moveTargets = [];
@@ -21018,6 +21296,42 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21018
21296
  extrasEl.appendChild(btn2);
21019
21297
  }
21020
21298
  }
21299
+ /**
21300
+ * Wire the host-contributed callout bubbles. Same contract as
21301
+ * {@link setLegendActions}: re-calling replaces the provider and re-projects the
21302
+ * rows already on screen.
21303
+ */
21304
+ setLegendCallouts(provider) {
21305
+ this.legendCallouts = provider;
21306
+ for (const row of this.rows.values()) this.renderCallouts(row);
21307
+ }
21308
+ /** (Re)build one row's callout bubbles — tinted icon circles beside the title whose
21309
+ * click (when the view carries content) deploys a panel of text and actions. */
21310
+ renderCallouts(row) {
21311
+ this.disposeTips(this.calloutTips, row.id);
21312
+ for (const bubble of row.callouts) bubble.destroy();
21313
+ row.callouts = [];
21314
+ row.calloutsEl.replaceChildren();
21315
+ const views = this.legendCallouts?.(row.id) ?? [];
21316
+ row.calloutsEl.style.display = views.length > 0 ? "inline-flex" : "none";
21317
+ for (const view of views) {
21318
+ const bubble = new CalloutBubble({
21319
+ icon: view.icon,
21320
+ background: view.background,
21321
+ ...view.color !== void 0 ? { color: view.color } : {},
21322
+ label: view.tooltip,
21323
+ ...view.content !== void 0 ? { panel: view.content } : {},
21324
+ // The panel portals into the plot host and self-tokens: it must work on
21325
+ // a bare chart, where no `.vela-ui` token ancestor exists.
21326
+ host: this.container,
21327
+ theme: () => this.theme
21328
+ });
21329
+ bubble.el.dataset.legendCallout = view.id;
21330
+ this.tip(this.calloutTips, row.id, bubble.el, view.tooltip);
21331
+ row.callouts.push(bubble);
21332
+ row.calloutsEl.appendChild(bubble.el);
21333
+ }
21334
+ }
21021
21335
  /** Reposition the per-pane legend containers after a layout change. */
21022
21336
  reposition() {
21023
21337
  for (const [paneId, lg] of this.legends) this.positionLegend(lg, paneId);
@@ -21185,6 +21499,7 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21185
21499
  }
21186
21500
  positionLegend(lg, paneId) {
21187
21501
  const bounds = this.paneBoundsOf ? this.paneBoundsOf(paneId) : { top: 0, height: Infinity };
21502
+ lg.toggleAttribute(LEGEND_AT_TOP_ATTR, bounds.top === 0);
21188
21503
  lg.style.display = bounds.height < 4 || !this.titlesVisible ? "none" : "flex";
21189
21504
  const collapsed = this.paneCollapse.has(paneId);
21190
21505
  const masterId = this.paneCollapse.get(paneId) ?? null;
@@ -21260,13 +21575,13 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21260
21575
  }
21261
21576
  /** Create or update an indicator's legend row (in the legend for its pane). */
21262
21577
  upsert(id, title, inputs, values, paneId = "price", opts = {}) {
21263
- const settingsTitle = opts.settingsTitle ?? title;
21264
21578
  const existing = this.rows.get(id);
21265
21579
  if (existing) {
21266
21580
  existing.title = title;
21267
- existing.settingsTitle = settingsTitle;
21268
21581
  existing.inputs = inputs;
21269
21582
  existing.values = { ...values };
21583
+ existing.props = opts.props ?? [];
21584
+ existing.propValues = { ...opts.propValues ?? {} };
21270
21585
  existing.titleEl.textContent = title;
21271
21586
  if (existing.paneId !== paneId) {
21272
21587
  existing.paneId = paneId;
@@ -21318,6 +21633,9 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21318
21633
  titleWrap.appendChild(beta);
21319
21634
  }
21320
21635
  el.appendChild(titleWrap);
21636
+ const calloutsEl = document.createElement("span");
21637
+ calloutsEl.style.cssText = `display:none;align-items:center;gap:4px;flex:none;margin-left:${LEGEND_TITLE_STATUS_GAP_PX}px;`;
21638
+ el.appendChild(calloutsEl);
21321
21639
  el.appendChild(statusEl);
21322
21640
  const valuesEl = document.createElement("span");
21323
21641
  valuesEl.style.cssText = `display:none;align-items:center;gap:5px;margin-left:${LEGEND_TITLE_VALUES_GAP_PX}px;white-space:nowrap;font-variant-numeric:tabular-nums;`;
@@ -21334,13 +21652,13 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21334
21652
  eye.className = "vela-ind-ctl";
21335
21653
  eye.style.cssText = LEGEND_CTL_CSS;
21336
21654
  eye.addEventListener("click", () => {
21337
- const row = this.rows.get(id);
21338
- this.onToggleVisible?.(id, Boolean(row?.hidden));
21655
+ const row2 = this.rows.get(id);
21656
+ this.onToggleVisible?.(id, Boolean(row2?.hidden));
21339
21657
  });
21340
21658
  controlsEl.appendChild(eye);
21341
21659
  eyeEl = eye;
21342
21660
  }
21343
- if (inputs.length > 0) {
21661
+ if (inputs.length > 0 || (opts.props ?? []).length > 0) {
21344
21662
  const gear = document.createElement("button");
21345
21663
  gear.type = "button";
21346
21664
  gear.setAttribute("aria-label", "Settings");
@@ -21380,7 +21698,9 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21380
21698
  controlsEl.appendChild(close);
21381
21699
  el.appendChild(controlsEl);
21382
21700
  this.attach(this.legendFor(paneId), el, !!opts.native);
21383
- this.rows.set(id, { id, title, settingsTitle, inputs, values: { ...values }, el, titleEl, statusEl, valuesEl, plotValues: [], plotValuesKey: "", showValues: null, highlighted: false, paneId, hidden: false, eyeEl, controlsEl, extrasEl, native: !!opts.native });
21701
+ 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 };
21702
+ this.rows.set(id, row);
21703
+ this.renderCallouts(row);
21384
21704
  this.syncFoldToggle();
21385
21705
  }
21386
21706
  /** Place a row in its pane's legend — native rows PREPEND (pinned to the top), Pine rows append. */
@@ -21389,9 +21709,11 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21389
21709
  else container.appendChild(el);
21390
21710
  }
21391
21711
  /** Reflect programmatic input changes (so a re-opened dialog shows current values). */
21392
- setValues(id, values) {
21712
+ setValues(id, values, props) {
21393
21713
  const row = this.rows.get(id);
21394
- if (row) row.values = { ...row.values, ...values };
21714
+ if (!row) return;
21715
+ row.values = { ...row.values, ...values };
21716
+ if (props) row.propValues = { ...row.propValues, ...props };
21395
21717
  }
21396
21718
  /**
21397
21719
  * Reflect an indicator's live status in its legend row: `'loading'` shows three pulsing
@@ -21473,8 +21795,13 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21473
21795
  syncRowActions(row) {
21474
21796
  const open2 = row.highlighted;
21475
21797
  row.controlsEl.style.display = open2 || row.hidden ? "inline-flex" : "none";
21476
- if (open2) row.el.appendChild(row.statusEl);
21477
- else row.el.insertBefore(row.statusEl, row.valuesEl);
21798
+ if (open2) {
21799
+ row.el.appendChild(row.statusEl);
21800
+ row.el.appendChild(row.calloutsEl);
21801
+ } else {
21802
+ row.el.insertBefore(row.statusEl, row.valuesEl);
21803
+ row.el.insertBefore(row.calloutsEl, row.statusEl);
21804
+ }
21478
21805
  for (const child of Array.from(row.controlsEl.children)) {
21479
21806
  if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
21480
21807
  if (child === row.extrasEl) {
@@ -21492,10 +21819,12 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21492
21819
  }
21493
21820
  remove(id) {
21494
21821
  const row = this.rows.get(id);
21822
+ for (const bubble of row?.callouts ?? []) bubble.destroy();
21495
21823
  row?.el.remove();
21496
21824
  this.rows.delete(id);
21497
21825
  this.disposeTips(this.rowTips, id);
21498
21826
  this.disposeTips(this.extrasTips, id);
21827
+ this.disposeTips(this.calloutTips, id);
21499
21828
  if (this.selectedId === id) this.selectedId = null;
21500
21829
  if (this.inputsDialog.openId === id) this.inputsDialog.close();
21501
21830
  this.syncFoldToggle();
@@ -21515,6 +21844,8 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21515
21844
  this.rowMenu = null;
21516
21845
  for (const id of [...this.rowTips.keys()]) this.disposeTips(this.rowTips, id);
21517
21846
  for (const id of [...this.extrasTips.keys()]) this.disposeTips(this.extrasTips, id);
21847
+ for (const id of [...this.calloutTips.keys()]) this.disposeTips(this.calloutTips, id);
21848
+ for (const row of this.rows.values()) for (const bubble of row.callouts) bubble.destroy();
21518
21849
  for (const lg of this.legends.values()) lg.remove();
21519
21850
  this.legends.clear();
21520
21851
  this.rows.clear();
@@ -21869,6 +22200,32 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21869
22200
  large: 16,
21870
22201
  huge: 20
21871
22202
  };
22203
+ function fontPxOf(size3) {
22204
+ if (typeof size3 === "number") return size3 > 0 ? size3 : SIZE_PX3.auto;
22205
+ return SIZE_PX3[size3] ?? SIZE_PX3.auto;
22206
+ }
22207
+ function tableHasContent(t) {
22208
+ return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
22209
+ }
22210
+ function mergeRenderPlan(t) {
22211
+ const span = /* @__PURE__ */ new Map();
22212
+ const omit = /* @__PURE__ */ new Set();
22213
+ for (const m of t.merges) {
22214
+ span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
22215
+ for (let r = m.startRow; r <= m.endRow; r += 1) {
22216
+ for (let c = m.startCol; c <= m.endCol; c += 1) {
22217
+ if (r !== m.startRow || c !== m.startCol) omit.add(`${r}:${c}`);
22218
+ }
22219
+ }
22220
+ }
22221
+ for (let r = 0; r < t.rows; r += 1) {
22222
+ for (let c = 0; c < t.columns; c += 1) {
22223
+ if (t.cells[r]?.[c]?.merged && !span.has(`${r}:${c}`)) omit.add(`${r}:${c}`);
22224
+ }
22225
+ }
22226
+ for (const key of span.keys()) omit.delete(key);
22227
+ return { span, omit };
22228
+ }
21872
22229
  var TableOverlay = class {
21873
22230
  constructor(container, theme, paneBounds) {
21874
22231
  this.container = container;
@@ -21889,7 +22246,9 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21889
22246
  update(tables) {
21890
22247
  this.lastTables = tables;
21891
22248
  this.root.replaceChildren();
21892
- for (const t of tables) this.root.appendChild(this.renderTable(t));
22249
+ for (const t of tables) {
22250
+ if (tableHasContent(t)) this.root.appendChild(this.renderTable(t));
22251
+ }
21893
22252
  }
21894
22253
  /** Re-render at the current pane geometry — after layout settles or on resize. */
21895
22254
  reposition() {
@@ -21904,39 +22263,36 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21904
22263
  this.root.remove();
21905
22264
  }
21906
22265
  renderTable(t) {
22266
+ const b = this.paneBounds(t.paneId);
21907
22267
  const wrap2 = document.createElement("div");
21908
22268
  wrap2.style.position = "absolute";
21909
- this.anchor(wrap2, t.position, this.paneBounds(t.paneId));
22269
+ if (t.frameColor && t.frameWidth > 0) wrap2.style.border = `${t.frameWidth}px solid ${t.frameColor}`;
22270
+ this.anchor(wrap2, t.position, b);
21910
22271
  const table = document.createElement("table");
21911
22272
  Object.assign(table.style, {
21912
22273
  borderCollapse: "collapse",
21913
22274
  background: t.bgColor ?? "transparent",
21914
22275
  fontFamily: this.theme.fontFamily || "sans-serif",
21915
- // Frame as an inset shadow (not a `border`) so it stays independent of the
21916
- // collapsed cell borders even when frame_width != border_width.
21917
- boxShadow: t.frameColor && t.frameWidth > 0 ? `inset 0 0 0 ${t.frameWidth}px ${t.frameColor}` : "none",
21918
22276
  border: "none",
21919
22277
  tableLayout: "auto",
21920
22278
  // Re-enable pointer events on the table only (root is none) so cell tooltips work.
21921
22279
  pointerEvents: "auto"
21922
22280
  });
21923
- const span = /* @__PURE__ */ new Map();
21924
- const skip = /* @__PURE__ */ new Set();
21925
- for (const m of t.merges) {
21926
- span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
21927
- for (let r = m.startRow; r <= m.endRow; r += 1) {
21928
- for (let c = m.startCol; c <= m.endCol; c += 1) {
21929
- if (r !== m.startRow || c !== m.startCol) skip.add(`${r}:${c}`);
21930
- }
21931
- }
21932
- }
22281
+ const { span, omit } = mergeRenderPlan(t);
22282
+ const plotW = Math.max(0, (this.root.clientWidth || this.container.clientWidth) - b.rightAxis);
22283
+ const paneH = b.height;
21933
22284
  const cellBorder = t.borderColor && t.borderWidth > 0 ? `${t.borderWidth}px solid ${t.borderColor}` : "none";
21934
22285
  for (let r = 0; r < t.rows; r += 1) {
21935
22286
  const tr = document.createElement("tr");
21936
22287
  for (let c = 0; c < t.columns; c += 1) {
22288
+ if (omit.has(`${r}:${c}`)) continue;
21937
22289
  const cell = t.cells[r]?.[c] ?? null;
21938
- if (skip.has(`${r}:${c}`) || cell?.merged) continue;
21939
22290
  const td = document.createElement("td");
22291
+ if (cell === null) {
22292
+ Object.assign(td.style, { padding: "0", border: "none" });
22293
+ tr.appendChild(td);
22294
+ continue;
22295
+ }
21940
22296
  const sp = span.get(`${r}:${c}`);
21941
22297
  if (sp) {
21942
22298
  if (sp.cs > 1) td.colSpan = sp.cs;
@@ -21945,18 +22301,22 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21945
22301
  Object.assign(td.style, {
21946
22302
  border: cellBorder,
21947
22303
  padding: "2px 6px",
21948
- background: cell?.bgColor ?? "transparent",
21949
- color: cell?.textColor ?? this.theme.textColor,
21950
- textAlign: cell?.hAlign ?? "center",
21951
- verticalAlign: cell?.vAlign === "top" ? "top" : cell?.vAlign === "bottom" ? "bottom" : "middle",
21952
- fontSize: `${SIZE_PX3[cell?.textSize ?? "normal"]}px`,
21953
- fontFamily: cell?.fontFamily === "monospace" ? "monospace" : "inherit",
21954
- fontWeight: cell?.bold ? "bold" : "normal",
21955
- fontStyle: cell?.italic ? "italic" : "normal",
21956
- whiteSpace: "pre-line"
22304
+ background: cell.bgColor ?? "transparent",
22305
+ color: cell.textColor ?? this.theme.textColor,
22306
+ textAlign: cell.hAlign,
22307
+ verticalAlign: cell.vAlign === "top" ? "top" : cell.vAlign === "bottom" ? "bottom" : "middle",
22308
+ fontSize: `${fontPxOf(cell.textSize)}px`,
22309
+ fontFamily: cell.fontFamily === "monospace" ? "monospace" : "inherit",
22310
+ fontWeight: cell.bold ? "bold" : "normal",
22311
+ fontStyle: cell.italic ? "italic" : "normal",
22312
+ // Pine cell text never wraps; `\n` still breaks lines. Wrapping
22313
+ // used to collapse unicode sparklines and ━━━ dividers.
22314
+ whiteSpace: "pre"
21957
22315
  });
21958
- if (cell?.tooltip) td.title = cell.tooltip;
21959
- td.textContent = cell?.text ?? "";
22316
+ if (cell.width) td.style.width = `${cell.width / 100 * plotW}px`;
22317
+ if (cell.height) td.style.height = `${cell.height / 100 * paneH}px`;
22318
+ if (cell.tooltip) td.title = cell.tooltip;
22319
+ td.textContent = cell.text ?? "";
21960
22320
  tr.appendChild(td);
21961
22321
  }
21962
22322
  table.appendChild(tr);
@@ -22841,8 +23201,14 @@ void main() {
22841
23201
  return sc === pane.scale ? pane : { ...pane, scale: sc };
22842
23202
  };
22843
23203
  b.alpha = this.modelAlpha;
22844
- for (const m of models) for (const bgSpan of m.backgrounds) this.emitBackground(b, bgSpan, pane, coords);
22845
- for (const m of models) for (const f of m.fills) this.emitFill(b, m, f, effPane(m), coords, i0, i1, scene.offsetOf(m.id));
23204
+ for (const m of models) for (const bgSpan of m.backgrounds) if (bgSpan.overlay !== true) this.emitBackground(b, bgSpan, pane, coords);
23205
+ 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));
23206
+ if (isPrice) {
23207
+ for (const m of scene.indicators.values()) {
23208
+ for (const bgSpan of m.backgrounds) if (bgSpan.overlay === true) this.emitBackground(b, bgSpan, pane, coords);
23209
+ for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, i1, scene.offsetOf(m.id));
23210
+ }
23211
+ }
22846
23212
  const drawCandles = isPrice && !scene.candlesHidden;
22847
23213
  let candleDrawn = false;
22848
23214
  for (const m of models) {
@@ -22856,7 +23222,7 @@ void main() {
22856
23222
  b.alpha = this.modelAlpha;
22857
23223
  const off = scene.offsetOf(m.id);
22858
23224
  const mp = effPane(m);
22859
- for (const s of m.series) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
23225
+ for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
22860
23226
  }
22861
23227
  if (drawCandles && !candleDrawn) {
22862
23228
  drawSlicesUpTo(scene.candleZ);
@@ -22865,6 +23231,12 @@ void main() {
22865
23231
  }
22866
23232
  drawSlicesUpTo(Infinity);
22867
23233
  b.alpha = this.modelAlpha;
23234
+ if (isPrice) {
23235
+ for (const m of scene.indicators.values()) {
23236
+ const off = scene.offsetOf(m.id);
23237
+ for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0, i1, theme, off);
23238
+ }
23239
+ }
22868
23240
  for (const m of models) {
22869
23241
  const mp = effPane(m);
22870
23242
  for (const pl of m.priceLines) this.emitHline(b, pl, mp, coords, dataW, theme);
@@ -22911,7 +23283,7 @@ void main() {
22911
23283
  for (const pane of scene.orderedPanes()) {
22912
23284
  const b = this.glowBatch;
22913
23285
  b.reset();
22914
- this.emitGlowSources(b, scene.indicatorsForPane(pane.id), pane, coords, i0, i1, (id) => scene.offsetOf(id));
23286
+ this.emitGlowSources(b, scene, pane, coords, i0, i1);
22915
23287
  if (b.vertexCount === 0) continue;
22916
23288
  const topH = Math.round(pane.bounds.top * dpr * 0.5);
22917
23289
  const botH = Math.round((pane.bounds.top + pane.bounds.height) * dpr * 0.5);
@@ -22980,15 +23352,23 @@ void main() {
22980
23352
  gl.bindVertexArray(null);
22981
23353
  return true;
22982
23354
  }
22983
- /** The "neon" elements that glow: line/area/step lines + point markers (not candles/fills/bars). */
22984
- emitGlowSources(b, models, pane, coords, i0, i1, offsetOf = () => 0) {
22985
- for (const m of models) {
22986
- const off = offsetOf(m.id);
22987
- for (const s of m.series) {
22988
- if (!isLineLikeSeries(s) || s.visible === false) continue;
22989
- if (s.kind === "histogram" || s.kind === "columns") continue;
22990
- if (s.kind === "circles" || s.kind === "cross") this.emitPointMarkers(b, s, pane, coords, i0, i1, off);
22991
- else this.emitPolyline(b, s, pane, coords, i0, i1, s.kind === "step", off);
23355
+ /** The "neon" elements that glow: line/area/step lines + point markers (not candles/fills/bars).
23356
+ * Routing mirrors the main pass: own series per pane, force_overlay series on the price pane. */
23357
+ emitGlowSources(b, scene, pane, coords, i0, i1) {
23358
+ const emitOne = (s, off) => {
23359
+ if (!isLineLikeSeries(s) || s.visible === false) return;
23360
+ if (s.kind === "histogram" || s.kind === "columns") return;
23361
+ if (s.kind === "circles" || s.kind === "cross") this.emitPointMarkers(b, s, pane, coords, i0, i1, off);
23362
+ else this.emitPolyline(b, s, pane, coords, i0, i1, s.kind === "step", off);
23363
+ };
23364
+ for (const m of scene.indicatorsForPane(pane.id)) {
23365
+ const off = scene.offsetOf(m.id);
23366
+ for (const s of m.series) if (s.overlay !== true) emitOne(s, off);
23367
+ }
23368
+ if (pane.kind === "price") {
23369
+ for (const m of scene.indicators.values()) {
23370
+ const off = scene.offsetOf(m.id);
23371
+ for (const s of m.series) if (s.overlay === true) emitOne(s, off);
22992
23372
  }
22993
23373
  }
22994
23374
  }
@@ -23271,9 +23651,9 @@ void main() {
23271
23651
  b.alpha = this.candleBodyAlpha;
23272
23652
  b.rect(g.bodyX, bodyTop, g.bodyW, bodyH, c);
23273
23653
  }
23274
- if (cs.borderVisible || bc || fading && cs.bodyVisible) {
23654
+ if (cs.borderVisible || fading && cs.bodyVisible) {
23275
23655
  b.alpha = this.candleStructureAlpha;
23276
- const bord = cs.borderVisible || bc ? parseColor((isUp ? cs.borderUpColor : cs.borderDownColor) ?? dir) : c;
23656
+ const bord = cs.borderVisible ? parseColor((isUp ? cs.borderUpColor : cs.borderDownColor) ?? bodyColorStr) : c;
23277
23657
  const bw = Math.max(0, g.bodyW - 1);
23278
23658
  const bh = Math.max(0, bodyH - 1);
23279
23659
  b.rectStroke(g.bodyX + 0.5, bodyTop + 0.5, bw, bh, 1, bord);
@@ -24825,8 +25205,14 @@ void main() {
24825
25205
  return sc === pane.scale ? pane : { ...pane, scale: sc };
24826
25206
  };
24827
25207
  ctx.globalAlpha = this.modelAlpha;
24828
- for (const m of models) for (const bg of m.backgrounds) this.drawBackground(ctx, bg, pane, coords);
24829
- for (const m of models) for (const f of m.fills) this.drawFill(ctx, m, f, effPane(m), coords, i0, i1, scene.offsetOf(m.id));
25208
+ for (const m of models) for (const bg of m.backgrounds) if (bg.overlay !== true) this.drawBackground(ctx, bg, pane, coords);
25209
+ 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));
25210
+ if (isPrice) {
25211
+ for (const m of scene.indicators.values()) {
25212
+ for (const bg of m.backgrounds) if (bg.overlay === true) this.drawBackground(ctx, bg, pane, coords);
25213
+ for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, scene.offsetOf(m.id));
25214
+ }
25215
+ }
24830
25216
  const slices = scene.drawingSlices.get(pane.id) ?? [];
24831
25217
  let si = 0;
24832
25218
  const drawSlicesUpTo = (z) => {
@@ -24848,7 +25234,7 @@ void main() {
24848
25234
  ctx.globalAlpha = this.modelAlpha;
24849
25235
  const off = scene.offsetOf(m.id);
24850
25236
  const mp = effPane(m);
24851
- for (const s of m.series) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
25237
+ for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
24852
25238
  }
24853
25239
  if (drawCandles && !candleDrawn) {
24854
25240
  drawSlicesUpTo(scene.candleZ);
@@ -24856,6 +25242,13 @@ void main() {
24856
25242
  this.drawPriceSeries(ctx, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
24857
25243
  }
24858
25244
  drawSlicesUpTo(Infinity);
25245
+ if (isPrice) {
25246
+ ctx.globalAlpha = this.modelAlpha;
25247
+ for (const m of scene.indicators.values()) {
25248
+ const off = scene.offsetOf(m.id);
25249
+ for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0, i1, theme, off);
25250
+ }
25251
+ }
24859
25252
  ctx.globalAlpha = this.modelAlpha;
24860
25253
  for (const m of models) {
24861
25254
  const mp = effPane(m);
@@ -25127,9 +25520,9 @@ void main() {
25127
25520
  ctx.fillStyle = color;
25128
25521
  ctx.fillRect(g.bodyX, top, g.bodyW, bodyH);
25129
25522
  }
25130
- if (cs.borderVisible || bc || fading && cs.bodyVisible) {
25523
+ if (cs.borderVisible || fading && cs.bodyVisible) {
25131
25524
  ctx.globalAlpha = this.candleStructureAlpha;
25132
- ctx.strokeStyle = cs.borderVisible || bc ? (up ? cs.borderUpColor : cs.borderDownColor) ?? dir : color;
25525
+ ctx.strokeStyle = cs.borderVisible ? (up ? cs.borderUpColor : cs.borderDownColor) ?? color : color;
25133
25526
  ctx.lineWidth = 1;
25134
25527
  const bw = Math.max(0, g.bodyW - 1);
25135
25528
  const bh = Math.max(0, bodyH - 1);
@@ -25561,6 +25954,8 @@ void main() {
25561
25954
  this.set = set;
25562
25955
  /** measureText width cache, keyed by `${font} ${text}`, persists across frames. */
25563
25956
  this.widthCache = /* @__PURE__ */ new Map();
25957
+ /** Tooltip hit-rects captured while drawing the CURRENT set (rebuilt per render). */
25958
+ this.tipRegions = [];
25564
25959
  /**
25565
25960
  * Index offset of the CURRENT set's model: its `xloc:'bar_index'` coordinates count
25566
25961
  * from the model's anchor bar, so they shift by this to land on chart logical indices.
@@ -25581,8 +25976,13 @@ void main() {
25581
25976
  const s = this.set;
25582
25977
  return !s.lines.length && !s.boxes.length && !s.labels.length && !s.polylines.length && !s.linefills.length;
25583
25978
  }
25979
+ /** Tooltip hit-rects of the labels drawn by the LAST `render` call (same coords as `ctx`). */
25980
+ labelTipRegions() {
25981
+ return this.tipRegions;
25982
+ }
25584
25983
  /** Draw the whole set into `ctx` using the supplied coordinate closures. */
25585
25984
  render(ctx, W, H, xOf, yOf) {
25985
+ this.tipRegions = [];
25586
25986
  if (this.isEmpty()) return;
25587
25987
  ctx.save();
25588
25988
  this.drawLinefills(ctx, W, H, xOf, yOf);
@@ -25930,13 +26330,35 @@ void main() {
25930
26330
  if (this.isPointShape(lb.style)) {
25931
26331
  if (!lb.noFill) this.drawLabelShape(ctx, lb.style, px, py, fontPx, color);
25932
26332
  if (lb.text) this.drawLabelText(ctx, lb, px, py + fontPx, fontPx);
25933
- } else if (lb.style === "none" || lb.style === "text_outline" || lb.noFill) {
25934
- if (lb.text) this.drawLabelText(ctx, lb, px, py, fontPx, lb.style === "text_outline");
26333
+ if (lb.tooltip) {
26334
+ const r = Math.max(4, fontPx * 0.6) + 3;
26335
+ this.tipRegions.push({ left: px - r, top: py - r, right: px + r, bottom: py + r, text: lb.tooltip });
26336
+ }
26337
+ } else if (lb.style === "none" || lb.style === "text_outline") {
26338
+ if (lb.text) {
26339
+ this.drawLabelText(ctx, lb, px, py, fontPx, lb.style === "text_outline");
26340
+ if (lb.tooltip) this.tipRegions.push(this.textRegion(ctx, lb, px, py, fontPx, lb.tooltip));
26341
+ }
25935
26342
  } else {
25936
- this.drawBubble(ctx, lb, px, py, fontPx, color);
26343
+ const r = this.drawBubble(ctx, lb, px, py, fontPx, color);
26344
+ if (lb.tooltip) this.tipRegions.push({ left: r.x, top: r.y, right: r.x + r.w, bottom: r.y + r.h, text: lb.tooltip });
25937
26345
  }
25938
26346
  }
25939
26347
  }
26348
+ /** Canvas font for a label's text — family + Pine `text_formatting` (bold/italic). */
26349
+ labelFont(lb, fontPx) {
26350
+ const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
26351
+ return `${lb.italic ? "italic " : ""}${lb.bold ? "bold " : ""}${fontPx}px ${family}`;
26352
+ }
26353
+ /** Hover rect of a text-only label (style none/text_outline/noFill), centered like drawLabelText. */
26354
+ textRegion(ctx, lb, cx, cy, fontPx, text) {
26355
+ const font = this.labelFont(lb, fontPx);
26356
+ const lines = (lb.text ?? "").split("\n");
26357
+ const w = Math.max(1, ...lines.map((l) => this.measure(ctx, font, l)));
26358
+ const h = fontPx * 1.25 * lines.length;
26359
+ const left = lb.textAlign === "left" ? cx : lb.textAlign === "right" ? cx - w : cx - w / 2;
26360
+ return { left, top: cy - h / 2, right: left + w, bottom: cy + h / 2, text };
26361
+ }
25940
26362
  isPointShape(style) {
25941
26363
  switch (style) {
25942
26364
  case "circle":
@@ -25955,8 +26377,7 @@ void main() {
25955
26377
  }
25956
26378
  }
25957
26379
  drawLabelText(ctx, lb, cx, cy, fontPx, outline = false) {
25958
- const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
25959
- ctx.font = `${fontPx}px ${family}`;
26380
+ ctx.font = this.labelFont(lb, fontPx);
25960
26381
  ctx.textAlign = lb.textAlign === "left" ? "left" : lb.textAlign === "right" ? "right" : "center";
25961
26382
  ctx.textBaseline = "middle";
25962
26383
  const lines = lb.text.split("\n");
@@ -26036,9 +26457,9 @@ void main() {
26036
26457
  ctx.fill();
26037
26458
  }
26038
26459
  }
26460
+ /** Draw a bubble label; returns the bubble body rect (for tooltip hit-testing). */
26039
26461
  drawBubble(ctx, lb, px, py, fontPx, color) {
26040
- const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
26041
- ctx.font = `${fontPx}px ${family}`;
26462
+ ctx.font = this.labelFont(lb, fontPx);
26042
26463
  const lines = (lb.text ?? "").split("\n");
26043
26464
  const padX = 6;
26044
26465
  const padY = 4;
@@ -26096,38 +26517,51 @@ void main() {
26096
26517
  by = py - h - ptr;
26097
26518
  pointer = "down";
26098
26519
  }
26099
- ctx.fillStyle = color;
26100
- this.roundRect(ctx, bx, by, w, h, 4);
26101
- ctx.fill();
26102
- if (pointer !== "none") {
26103
- ctx.beginPath();
26104
- if (pointer === "down") {
26105
- ctx.moveTo(px - ptr, by + h);
26106
- ctx.lineTo(px + ptr, by + h);
26107
- ctx.lineTo(px, py);
26108
- } else if (pointer === "up") {
26109
- ctx.moveTo(px - ptr, by);
26110
- ctx.lineTo(px + ptr, by);
26111
- ctx.lineTo(px, py);
26112
- } else if (pointer === "left") {
26113
- ctx.moveTo(bx, py - ptr);
26114
- ctx.lineTo(bx, py + ptr);
26115
- ctx.lineTo(px, py);
26116
- } else {
26117
- ctx.moveTo(bx + w, py - ptr);
26118
- ctx.lineTo(bx + w, py + ptr);
26119
- ctx.lineTo(px, py);
26120
- }
26121
- ctx.closePath();
26520
+ if (!lb.noFill) {
26521
+ ctx.fillStyle = color;
26522
+ this.roundRect(ctx, bx, by, w, h, 4);
26122
26523
  ctx.fill();
26524
+ if (pointer !== "none") {
26525
+ ctx.beginPath();
26526
+ if (pointer === "down") {
26527
+ ctx.moveTo(px - ptr, by + h);
26528
+ ctx.lineTo(px + ptr, by + h);
26529
+ ctx.lineTo(px, py);
26530
+ } else if (pointer === "up") {
26531
+ ctx.moveTo(px - ptr, by);
26532
+ ctx.lineTo(px + ptr, by);
26533
+ ctx.lineTo(px, py);
26534
+ } else if (pointer === "left") {
26535
+ ctx.moveTo(bx, py - ptr);
26536
+ ctx.lineTo(bx, py + ptr);
26537
+ ctx.lineTo(px, py);
26538
+ } else {
26539
+ ctx.moveTo(bx + w, py - ptr);
26540
+ ctx.lineTo(bx + w, py + ptr);
26541
+ ctx.lineTo(px, py);
26542
+ }
26543
+ ctx.closePath();
26544
+ ctx.fill();
26545
+ }
26123
26546
  }
26124
26547
  if (lb.text) {
26125
- ctx.fillStyle = lb.textColor ?? contrastColor(color);
26126
- ctx.textAlign = "center";
26548
+ ctx.fillStyle = lb.textColor ?? (lb.noFill ? this.deps.theme.textColor : contrastColor(color));
26127
26549
  ctx.textBaseline = "middle";
26550
+ let tx;
26551
+ if (lb.textAlign === "left") {
26552
+ ctx.textAlign = "left";
26553
+ tx = bx + padX;
26554
+ } else if (lb.textAlign === "right") {
26555
+ ctx.textAlign = "right";
26556
+ tx = bx + w - padX;
26557
+ } else {
26558
+ ctx.textAlign = "center";
26559
+ tx = bx + w / 2;
26560
+ }
26128
26561
  const startY = by + padY + lineH / 2;
26129
- for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], bx + w / 2, startY + i * lineH);
26562
+ for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], tx, startY + i * lineH);
26130
26563
  }
26564
+ return { x: bx, y: by, w, h };
26131
26565
  }
26132
26566
  roundRect(ctx, x, y, w, h, r) {
26133
26567
  const rr = Math.min(r, w / 2, h / 2);
@@ -26365,6 +26799,8 @@ void main() {
26365
26799
  this.axisTextColor = DARK_THEME.textColor;
26366
26800
  // Shared Pine-drawing renderer (line/box/label/polyline/linefill); widthCache persists.
26367
26801
  this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
26802
+ // Tooltip hit-rects of every label drawn this frame, in plot coords (rebuilt per render).
26803
+ this.labelTips = [];
26368
26804
  }
26369
26805
  mount(canvas) {
26370
26806
  this.canvas = canvas;
@@ -26406,6 +26842,7 @@ void main() {
26406
26842
  const dataW = coords.width;
26407
26843
  const dataH = coords.height;
26408
26844
  this.axisTextColor = surface?.textColor ?? theme.textColor;
26845
+ this.labelTips = [];
26409
26846
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
26410
26847
  ctx.clearRect(0, 0, fullW, fullH);
26411
26848
  if (surface && (fullW > dataW || fullH > dataH)) {
@@ -26515,6 +26952,17 @@ void main() {
26515
26952
  (price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
26516
26953
  );
26517
26954
  ctx.restore();
26955
+ for (const r of this.drawScene.labelTipRegions()) {
26956
+ this.labelTips.push({ ...r, top: r.top + pane.bounds.top, bottom: r.bottom + pane.bounds.top });
26957
+ }
26958
+ }
26959
+ /** Tooltip of the topmost label under a plot-space point, or null. Fed by the last render. */
26960
+ labelTooltipAt(x, y) {
26961
+ for (let i = this.labelTips.length - 1; i >= 0; i -= 1) {
26962
+ const r = this.labelTips[i];
26963
+ if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return r.text;
26964
+ }
26965
+ return null;
26518
26966
  }
26519
26967
  // ── axes ──
26520
26968
  drawPriceAxes(ctx, scene, coords, theme, dataW, panes) {
@@ -26731,6 +27179,68 @@ void main() {
26731
27179
  return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
26732
27180
  }
26733
27181
 
27182
+ // src/renderers/native/chrome/LabelTooltip.ts
27183
+ var HOVER_DELAY_MS = 350;
27184
+ var LabelTooltip = class {
27185
+ constructor(plot, deps) {
27186
+ this.plot = plot;
27187
+ this.deps = deps;
27188
+ this.tip = null;
27189
+ this.timer = null;
27190
+ /** Text of the currently open OR armed tip — dedupes moves inside one label. */
27191
+ this.current = null;
27192
+ this.onMove = (e) => {
27193
+ if (e.pointerType !== "mouse") return;
27194
+ const rect = this.plot.getBoundingClientRect();
27195
+ const x = e.clientX - rect.left;
27196
+ const y = e.clientY - rect.top;
27197
+ const text = this.deps.lookup(x, y);
27198
+ if (!text) {
27199
+ this.clear();
27200
+ return;
27201
+ }
27202
+ if (text === this.current) return;
27203
+ this.clear();
27204
+ this.current = text;
27205
+ this.timer = window.setTimeout(() => this.show(text, x, y), HOVER_DELAY_MS);
27206
+ };
27207
+ this.onLeave = () => {
27208
+ this.clear();
27209
+ };
27210
+ plot.addEventListener("pointermove", this.onMove);
27211
+ plot.addEventListener("pointerleave", this.onLeave);
27212
+ plot.addEventListener("pointerdown", this.onLeave);
27213
+ }
27214
+ destroy() {
27215
+ this.plot.removeEventListener("pointermove", this.onMove);
27216
+ this.plot.removeEventListener("pointerleave", this.onLeave);
27217
+ this.plot.removeEventListener("pointerdown", this.onLeave);
27218
+ this.clear();
27219
+ }
27220
+ show(text, x, y) {
27221
+ const doc = this.plot.ownerDocument;
27222
+ const tip = doc.createElement("div");
27223
+ tip.textContent = text;
27224
+ 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;";
27225
+ applyChromeTokens(tip, this.deps.theme());
27226
+ this.plot.appendChild(tip);
27227
+ const pw = this.plot.clientWidth;
27228
+ const ph = this.plot.clientHeight;
27229
+ tip.style.left = `${Math.max(0, Math.min(x + 12, pw - tip.offsetWidth - 4))}px`;
27230
+ tip.style.top = `${Math.max(0, Math.min(y + 16, ph - tip.offsetHeight - 4))}px`;
27231
+ this.tip = tip;
27232
+ }
27233
+ clear() {
27234
+ if (this.timer !== null) {
27235
+ clearTimeout(this.timer);
27236
+ this.timer = null;
27237
+ }
27238
+ this.tip?.remove();
27239
+ this.tip = null;
27240
+ this.current = null;
27241
+ }
27242
+ };
27243
+
26734
27244
  // src/renderers/native/chrome/CrosshairRenderer.ts
26735
27245
  var CrosshairRenderer = class {
26736
27246
  constructor() {
@@ -32522,19 +33032,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
32522
33032
  for (const model of models) {
32523
33033
  const off = offsetOf(model.id);
32524
33034
  for (const s of model.series) {
32525
- if (s.kind === "candle" || s.kind === "bar") {
32526
- for (let i = i0; i <= i1; i += 1) {
32527
- const b = s.bars[i - off];
32528
- if (b) {
32529
- consider(b.high);
32530
- consider(b.low);
32531
- }
32532
- }
32533
- } else if (isLineLikeSeries(s)) {
32534
- for (let i = i0; i <= i1; i += 1) consider(s.points[i - off]?.value);
32535
- if (s.kind === "histogram" || s.kind === "columns") consider(s.style?.base ?? 0);
32536
- else if (s.style?.base != null) consider(s.style.base);
32537
- }
33035
+ if (s.overlay === true) continue;
33036
+ considerSeries(s, i0, i1, off, consider);
32538
33037
  }
32539
33038
  for (const pl of model.priceLines) consider(pl.price);
32540
33039
  }
@@ -32556,6 +33055,36 @@ ${overlayScrollbarCss(".vela-sd-pane")}
32556
33055
  const span = max2 - min2;
32557
33056
  return { min: min2 - span * MARGIN_BOTTOM, max: max2 + span * MARGIN_TOP };
32558
33057
  }
33058
+ function considerSeries(s, i0, i1, off, consider) {
33059
+ if (s.kind === "candle" || s.kind === "bar") {
33060
+ for (let i = i0; i <= i1; i += 1) {
33061
+ const b = s.bars[i - off];
33062
+ if (b) {
33063
+ consider(b.high);
33064
+ consider(b.low);
33065
+ }
33066
+ }
33067
+ } else if (isLineLikeSeries(s)) {
33068
+ for (let i = i0; i <= i1; i += 1) consider(s.points[i - off]?.value);
33069
+ if (s.kind === "histogram" || s.kind === "columns") consider(s.style?.base ?? 0);
33070
+ else if (s.style?.base != null) consider(s.style.base);
33071
+ }
33072
+ }
33073
+ function overlaySeriesRange(models, i0, i1, offsetOf = () => 0) {
33074
+ let min2 = Infinity;
33075
+ let max2 = -Infinity;
33076
+ const consider = (v) => {
33077
+ if (v != null && Number.isFinite(v)) {
33078
+ if (v < min2) min2 = v;
33079
+ if (v > max2) max2 = v;
33080
+ }
33081
+ };
33082
+ for (const model of models) {
33083
+ const off = offsetOf(model.id);
33084
+ for (const s of model.series) if (s.overlay === true) considerSeries(s, i0, i1, off, consider);
33085
+ }
33086
+ return min2 === Infinity ? null : { min: min2, max: max2 };
33087
+ }
32559
33088
  function expandScaleByPixels(scale, heightPx, abovePx, belowPx) {
32560
33089
  if (abovePx <= 0 && belowPx <= 0) return scale;
32561
33090
  const content = heightPx - abovePx - belowPx;
@@ -33338,6 +33867,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33338
33867
  this.glowAmount = 0;
33339
33868
  // WebGL2 neon-glow intensity (canvas2d ignores it)
33340
33869
  this.chrome = new ChromeRenderer();
33870
+ /** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
33871
+ this.labelTooltip = null;
33341
33872
  this.crosshairLayer = new CrosshairRenderer();
33342
33873
  /** 1 Hz repaint pump so the price-axis countdown-to-bar-close ticks; null when off. */
33343
33874
  this.countdownTimer = null;
@@ -33348,6 +33879,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33348
33879
  this.indicatorValuesOn = true;
33349
33880
  /** Host-contributed legend actions — held here so a rebuild of the legend re-wires them. */
33350
33881
  this.legendActionsProvider = null;
33882
+ /** Host-contributed legend callouts — held here so a rebuild of the legend re-wires them. */
33883
+ this.legendCalloutsProvider = null;
33351
33884
  /** Host override of the legend's fold toggle — held here so a remount re-applies it. */
33352
33885
  this.legendOverviewAction = null;
33353
33886
  // ── keyboard navigation / accessibility (item 11) ──
@@ -34456,6 +34989,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34456
34989
  this.plot.appendChild(this.scrollButton);
34457
34990
  this.plot.addEventListener("pointermove", this.onScrollProximityMove);
34458
34991
  this.plot.addEventListener("pointerleave", this.onScrollProximityLeave);
34992
+ this.labelTooltip = new LabelTooltip(this.plot, {
34993
+ theme: () => this.chromeTheme(),
34994
+ lookup: (x, y) => this.chrome.labelTooltipAt(x, y)
34995
+ });
34459
34996
  this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
34460
34997
  projector: () => this.drawingProjector(),
34461
34998
  dpr: () => this.coords.dpr,
@@ -34495,9 +35032,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34495
35032
  this.inputsUI.setDialogHost(this.dialogHost);
34496
35033
  this.inputsUI.setSymbolPicker(this.symbolPicker);
34497
35034
  this.inputsUI.setLegendActions(this.legendActionsProvider);
35035
+ this.inputsUI.setLegendCallouts(this.legendCalloutsProvider);
34498
35036
  this.inputsUI.setLegendOverviewAction(this.legendOverviewAction);
34499
35037
  this.inputsUI.setOnChange((c) => {
34500
- for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value });
35038
+ for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value, ...c.kind ? { kind: c.kind } : {} });
34501
35039
  });
34502
35040
  this.inputsUI.setOnRemove((id) => {
34503
35041
  for (const cb of this.removeIndicatorCbs) cb(id);
@@ -34687,6 +35225,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34687
35225
  this.settingsButton = null;
34688
35226
  this.plot?.removeEventListener("pointermove", this.onScrollProximityMove);
34689
35227
  this.plot?.removeEventListener("pointerleave", this.onScrollProximityLeave);
35228
+ this.labelTooltip?.destroy();
35229
+ this.labelTooltip = null;
34690
35230
  this.scrollButton?.remove();
34691
35231
  this.scrollButton = null;
34692
35232
  for (const l of this.extLayers) l.instance.destroy?.();
@@ -34927,7 +35467,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34927
35467
  else this.scene.assignIndicatorZ(model.id);
34928
35468
  this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
34929
35469
  native: !!model.native,
34930
- ...model.shorttitle ? { settingsTitle: model.title } : {}
35470
+ ...model.props ? { props: model.props, propValues: model.propValues ?? {} } : {}
34931
35471
  });
34932
35472
  this.syncTables(model);
34933
35473
  if (model.native?.type === "volume") {
@@ -34978,8 +35518,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34978
35518
  this.paneControls?.refresh();
34979
35519
  this.scheduler.invalidate(4 /* Full */);
34980
35520
  }
34981
- setIndicatorInputs(handle, values) {
34982
- this.inputsUI.setValues(handle.id, values);
35521
+ setIndicatorInputs(handle, values, props) {
35522
+ this.inputsUI.setValues(handle.id, values, props);
34983
35523
  }
34984
35524
  setSymbolPicker(picker) {
34985
35525
  this.symbolPicker = picker;
@@ -34989,6 +35529,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34989
35529
  this.legendActionsProvider = provider;
34990
35530
  this.inputsUI?.setLegendActions(provider);
34991
35531
  }
35532
+ setLegendCallouts(provider) {
35533
+ this.legendCalloutsProvider = provider;
35534
+ this.inputsUI?.setLegendCallouts(provider);
35535
+ }
34992
35536
  setLegendOverviewAction(action) {
34993
35537
  this.legendOverviewAction = action;
34994
35538
  this.inputsUI?.setLegendOverviewAction(action);
@@ -35969,7 +36513,11 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35969
36513
  }
35970
36514
  const models = this.scene.indicatorsForPane(pane.id);
35971
36515
  const masterModels = models.filter((m) => m.ownScale !== true);
35972
- const dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
36516
+ let dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
36517
+ if (pane === pricePane) {
36518
+ const or2 = overlaySeriesRange(this.scene.indicators.values(), i0, i1, (id) => this.scene.offsetOf(id));
36519
+ if (or2) dr = dr ? { min: Math.min(dr.min, or2.min), max: Math.max(dr.max, or2.max) } : or2;
36520
+ }
35973
36521
  const includeCandles = pane.kind === "price" && !this.scene.candlesHidden;
35974
36522
  pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
35975
36523
  pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
@@ -36243,6 +36791,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36243
36791
  for (const m of models) {
36244
36792
  const off = this.scene.offsetOf(m.id);
36245
36793
  for (const s of m.series) {
36794
+ if (s.overlay === true) continue;
36246
36795
  if (isLineLikeSeries(s)) {
36247
36796
  const v = s.points[i0 - off]?.value;
36248
36797
  if (v != null && Number.isFinite(v)) return v;
@@ -37148,6 +37697,19 @@ ${overlayScrollbarCss(".vela-sd-pane")}
37148
37697
  function legendActions() {
37149
37698
  return [...legendRegistry.values()].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
37150
37699
  }
37700
+ var calloutRegistry = /* @__PURE__ */ new Map();
37701
+ function registerLegendCallout(desc) {
37702
+ calloutRegistry.set(desc.id, desc);
37703
+ return () => {
37704
+ if (calloutRegistry.get(desc.id) === desc) calloutRegistry.delete(desc.id);
37705
+ };
37706
+ }
37707
+ function unregisterLegendCallout(id) {
37708
+ calloutRegistry.delete(id);
37709
+ }
37710
+ function legendCallouts() {
37711
+ return [...calloutRegistry.values()].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
37712
+ }
37151
37713
  var stateHandlers = /* @__PURE__ */ new Map();
37152
37714
  function registerStatePersistence(handler) {
37153
37715
  stateHandlers.set(handler.key, handler);
@@ -38491,6 +39053,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
38491
39053
  exports.iconMarkup = iconMarkup;
38492
39054
  exports.inputVisible = inputVisible;
38493
39055
  exports.legendActions = legendActions;
39056
+ exports.legendCallouts = legendCallouts;
38494
39057
  exports.nativeIndicatorDescriptors = nativeIndicatorDescriptors;
38495
39058
  exports.nativeIndicatorTypes = nativeIndicatorTypes;
38496
39059
  exports.normalizeSettingsRow = normalizeSettingsRow;
@@ -38499,6 +39062,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
38499
39062
  exports.registerDrawingType = registerDrawingType;
38500
39063
  exports.registerIcon = registerIcon;
38501
39064
  exports.registerLegendAction = registerLegendAction;
39065
+ exports.registerLegendCallout = registerLegendCallout;
38502
39066
  exports.registerNativeIndicator = registerNativeIndicator;
38503
39067
  exports.registerRendererDefaults = registerRendererDefaults;
38504
39068
  exports.registerRendererLayer = registerRendererLayer;
@@ -38524,6 +39088,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
38524
39088
  exports.unregisterChartType = unregisterChartType;
38525
39089
  exports.unregisterDefaultEngine = unregisterDefaultEngine;
38526
39090
  exports.unregisterLegendAction = unregisterLegendAction;
39091
+ exports.unregisterLegendCallout = unregisterLegendCallout;
38527
39092
  exports.unregisterNativeIndicator = unregisterNativeIndicator;
38528
39093
  exports.unregisterRendererDefaults = unregisterRendererDefaults;
38529
39094
  exports.unregisterRendererLayer = unregisterRendererLayer;