@luxalgo/vela 0.6.7 → 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-6WFKTAT4.js → chunk-6WDDVMBJ.js} +548 -147
  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-WVP4XV5A.js → chunk-STHSKXOR.js} +407 -159
  8. package/dist/{chunk-PN5KWFZ4.js → chunk-WFSZBX3R.js} +2 -2
  9. package/dist/{contributions-Z12BrWCy.d.ts → contributions-C1U2Krwg.d.cts} +112 -7
  10. package/dist/{contributions-Hv4_cOJo.d.cts → contributions-D7PVZO2i.d.ts} +112 -7
  11. package/dist/index.cjs +755 -152
  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-9koBjc5H.d.ts → plugin-7bkF32Rk.d.ts} +3 -3
  18. package/dist/{plugin-BXhDHTYn.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/{bottombar-Br4rzx_R.d.ts → statusline-DOPiT6I6.d.cts} +91 -6
  30. package/dist/{bottombar-Div5zibZ.d.cts → statusline-zdF4eZLr.d.ts} +91 -6
  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 +755 -152
  36. package/dist/vela.global.min.js +93 -44
  37. package/dist/widget.cjs +1144 -271
  38. package/dist/widget.d.cts +12 -85
  39. package/dist/widget.d.ts +12 -85
  40. package/dist/widget.js +7 -7
  41. package/dist/workspace.cjs +1144 -271
  42. package/dist/workspace.d.cts +65 -11
  43. package/dist/workspace.d.ts +65 -11
  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,
@@ -20016,12 +20259,16 @@ ${overlayScrollbarCss(".vela-dialog *")}
20016
20259
  closeOnEscape: false,
20017
20260
  footer: (foot) => {
20018
20261
  foot.append(
20262
+ this.resetAction(),
20019
20263
  this.dialogButton("Cancel", false, () => this.revertAndClose()),
20020
20264
  this.dialogButton("Ok", true, () => this.close())
20021
20265
  );
20022
20266
  },
20267
+ // Stale-guard: destroying a dialog fires its machine's onOpenChange(false)
20268
+ // asynchronously — after a reset rebuild that notification must not close
20269
+ // the replacement dialog.
20023
20270
  onOpenChange: (open2) => {
20024
- if (!open2) this.close();
20271
+ if (!open2 && this.uiDialog === ui) this.close();
20025
20272
  }
20026
20273
  });
20027
20274
  applyChromeTokens(ui.panel, t);
@@ -20032,6 +20279,16 @@ ${overlayScrollbarCss(".vela-dialog *")}
20032
20279
  text: () => "Close"
20033
20280
  }));
20034
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
+ }
20035
20292
  const tabs = document.createElement("div");
20036
20293
  tabs.style.cssText = `display:flex;gap:12px;padding:0 20px;border-bottom:1px solid ${border};flex:0 0 auto;`;
20037
20294
  const tabEls = [];
@@ -20134,9 +20391,10 @@ ${overlayScrollbarCss(".vela-dialog *")}
20134
20391
  this.openId = null;
20135
20392
  this.row = null;
20136
20393
  this.snapshot = null;
20394
+ this.propSnapshot = null;
20137
20395
  ui?.destroy();
20138
20396
  }
20139
- /** 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. */
20140
20398
  revertAndClose() {
20141
20399
  const row = this.row;
20142
20400
  const snap = this.snapshot;
@@ -20150,12 +20408,55 @@ ${overlayScrollbarCss(".vela-dialog *")}
20150
20408
  }
20151
20409
  }
20152
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
+ }
20153
20422
  this.close();
20154
20423
  }
20424
+ /** The footer's reset button — same chip as Cancel, pinned to the LEFT edge
20425
+ * (`margin-right:auto` against the footer's flex-end keeps Cancel/Ok right). */
20426
+ resetAction() {
20427
+ const b = this.dialogButton("Reset defaults", false, () => this.resetToDefaults());
20428
+ b.style.marginRight = "auto";
20429
+ return b;
20430
+ }
20431
+ /** Restore every input to its declared default (re-running the indicator), then
20432
+ * re-open the form so each control re-reads the restored values — the same
20433
+ * rebuild-after-reset move as the chart-settings dialog. The open-time snapshot
20434
+ * survives the rebuild, so Cancel after a reset still reverts the whole session. */
20435
+ resetToDefaults() {
20436
+ const row = this.row;
20437
+ if (!row) return;
20438
+ const snap = this.snapshot;
20439
+ const propSnap = this.propSnapshot;
20440
+ for (const inp of row.inputs) {
20441
+ if (row.values[inp.key] !== inp.defval) {
20442
+ row.values[inp.key] = inp.defval;
20443
+ this.host.onChange?.({ indicatorId: row.id, key: inp.key, value: inp.defval });
20444
+ }
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
+ }
20452
+ this.open(row);
20453
+ if (snap) this.snapshot = snap;
20454
+ if (propSnap) this.propSnapshot = propSnap;
20455
+ }
20155
20456
  /** Write one edit through: store it, notify the host, and re-apply the `when` gates. */
20156
- commit(row, key, value) {
20157
- row.values[key] = value;
20158
- 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" } : {} });
20159
20460
  this.refreshVisibility?.();
20160
20461
  }
20161
20462
  /** One settings row (or several `inline=` inputs) placed into a section's grid. */
@@ -20182,7 +20483,7 @@ ${overlayScrollbarCss(".vela-dialog *")}
20182
20483
  const id = idOf(lead);
20183
20484
  const info = lead.tooltip ? this.infoButton(lead.tooltip) : void 0;
20184
20485
  if (lead.type === "bool") {
20185
- const current = Boolean(row.values[lead.key] ?? lead.defval);
20486
+ const current = Boolean(bagOf(row, lead)[lead.key] ?? lead.defval);
20186
20487
  return append(fieldRow({
20187
20488
  label: nameOf(lead),
20188
20489
  id,
@@ -20191,7 +20492,7 @@ ${overlayScrollbarCss(".vela-dialog *")}
20191
20492
  toggle: {
20192
20493
  id,
20193
20494
  checked: current,
20194
- onChange: (v) => this.commit(row, lead.key, v)
20495
+ onChange: (v) => this.commit(row, lead, v)
20195
20496
  }
20196
20497
  }));
20197
20498
  }
@@ -20216,8 +20517,8 @@ ${overlayScrollbarCss(".vela-dialog *")}
20216
20517
  }
20217
20518
  /** Build the typed control for one input, committing edits live via `onChange`. */
20218
20519
  buildControl(row, inp, id) {
20219
- const current = row.values[inp.key] ?? inp.defval;
20220
- 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);
20221
20522
  if (inp.type === "bool") {
20222
20523
  return buildFieldControl({ kind: "switch", id, checked: Boolean(current), onChange: (v) => emit(v) }).el;
20223
20524
  }
@@ -20228,7 +20529,7 @@ ${overlayScrollbarCss(".vela-dialog *")}
20228
20529
  kind: "color",
20229
20530
  id,
20230
20531
  theme: this.host.theme(),
20231
- get: () => String(row.values[inp.key] ?? inp.defval),
20532
+ get: () => String(bagOf(row, inp)[inp.key] ?? inp.defval),
20232
20533
  onChange: (v) => emit(v)
20233
20534
  }).el;
20234
20535
  }
@@ -20791,6 +21092,7 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
20791
21092
  }
20792
21093
 
20793
21094
  // src/renderers/shared/InputsUI.ts
21095
+ var LEGEND_AT_TOP_ATTR = "data-vela-pane-at-top";
20794
21096
  var STATUS_KEYFRAMES = "@keyframes vela-ind-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.3;transform:scale(.6)}}";
20795
21097
  var LEGEND_ICON_PX2 = 16;
20796
21098
  var LEGEND_CTL_PX = 18;
@@ -20829,10 +21131,14 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
20829
21131
  this.moveApi = null;
20830
21132
  /** Host-contributed legend actions, resolved PER ROW at render time (see setLegendActions). */
20831
21133
  this.legendActions = null;
21134
+ /** Host-contributed callout bubbles, resolved PER ROW at render time (see setLegendCallouts). */
21135
+ this.legendCallouts = null;
20832
21136
  /** Chrome-tooltip disposers, per row id — a tip open at removal must not outlive its row. */
20833
21137
  this.rowTips = /* @__PURE__ */ new Map();
20834
21138
  /** Same, for the contributed extras only (rebuilt independently by setLegendActions). */
20835
21139
  this.extrasTips = /* @__PURE__ */ new Map();
21140
+ /** Same, for the contributed callouts only (rebuilt independently by setLegendCallouts). */
21141
+ this.calloutTips = /* @__PURE__ */ new Map();
20836
21142
  /** Open "Move to" menu (kept so it can be torn down). */
20837
21143
  this.moveMenu = null;
20838
21144
  this.moveTargets = [];
@@ -20990,6 +21296,42 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
20990
21296
  extrasEl.appendChild(btn2);
20991
21297
  }
20992
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
+ }
20993
21335
  /** Reposition the per-pane legend containers after a layout change. */
20994
21336
  reposition() {
20995
21337
  for (const [paneId, lg] of this.legends) this.positionLegend(lg, paneId);
@@ -21157,6 +21499,7 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21157
21499
  }
21158
21500
  positionLegend(lg, paneId) {
21159
21501
  const bounds = this.paneBoundsOf ? this.paneBoundsOf(paneId) : { top: 0, height: Infinity };
21502
+ lg.toggleAttribute(LEGEND_AT_TOP_ATTR, bounds.top === 0);
21160
21503
  lg.style.display = bounds.height < 4 || !this.titlesVisible ? "none" : "flex";
21161
21504
  const collapsed = this.paneCollapse.has(paneId);
21162
21505
  const masterId = this.paneCollapse.get(paneId) ?? null;
@@ -21232,13 +21575,13 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21232
21575
  }
21233
21576
  /** Create or update an indicator's legend row (in the legend for its pane). */
21234
21577
  upsert(id, title, inputs, values, paneId = "price", opts = {}) {
21235
- const settingsTitle = opts.settingsTitle ?? title;
21236
21578
  const existing = this.rows.get(id);
21237
21579
  if (existing) {
21238
21580
  existing.title = title;
21239
- existing.settingsTitle = settingsTitle;
21240
21581
  existing.inputs = inputs;
21241
21582
  existing.values = { ...values };
21583
+ existing.props = opts.props ?? [];
21584
+ existing.propValues = { ...opts.propValues ?? {} };
21242
21585
  existing.titleEl.textContent = title;
21243
21586
  if (existing.paneId !== paneId) {
21244
21587
  existing.paneId = paneId;
@@ -21290,6 +21633,9 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21290
21633
  titleWrap.appendChild(beta);
21291
21634
  }
21292
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);
21293
21639
  el.appendChild(statusEl);
21294
21640
  const valuesEl = document.createElement("span");
21295
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;`;
@@ -21306,13 +21652,13 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21306
21652
  eye.className = "vela-ind-ctl";
21307
21653
  eye.style.cssText = LEGEND_CTL_CSS;
21308
21654
  eye.addEventListener("click", () => {
21309
- const row = this.rows.get(id);
21310
- this.onToggleVisible?.(id, Boolean(row?.hidden));
21655
+ const row2 = this.rows.get(id);
21656
+ this.onToggleVisible?.(id, Boolean(row2?.hidden));
21311
21657
  });
21312
21658
  controlsEl.appendChild(eye);
21313
21659
  eyeEl = eye;
21314
21660
  }
21315
- if (inputs.length > 0) {
21661
+ if (inputs.length > 0 || (opts.props ?? []).length > 0) {
21316
21662
  const gear = document.createElement("button");
21317
21663
  gear.type = "button";
21318
21664
  gear.setAttribute("aria-label", "Settings");
@@ -21352,7 +21698,9 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21352
21698
  controlsEl.appendChild(close);
21353
21699
  el.appendChild(controlsEl);
21354
21700
  this.attach(this.legendFor(paneId), el, !!opts.native);
21355
- 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);
21356
21704
  this.syncFoldToggle();
21357
21705
  }
21358
21706
  /** Place a row in its pane's legend — native rows PREPEND (pinned to the top), Pine rows append. */
@@ -21361,9 +21709,11 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21361
21709
  else container.appendChild(el);
21362
21710
  }
21363
21711
  /** Reflect programmatic input changes (so a re-opened dialog shows current values). */
21364
- setValues(id, values) {
21712
+ setValues(id, values, props) {
21365
21713
  const row = this.rows.get(id);
21366
- 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 };
21367
21717
  }
21368
21718
  /**
21369
21719
  * Reflect an indicator's live status in its legend row: `'loading'` shows three pulsing
@@ -21445,8 +21795,13 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21445
21795
  syncRowActions(row) {
21446
21796
  const open2 = row.highlighted;
21447
21797
  row.controlsEl.style.display = open2 || row.hidden ? "inline-flex" : "none";
21448
- if (open2) row.el.appendChild(row.statusEl);
21449
- 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
+ }
21450
21805
  for (const child of Array.from(row.controlsEl.children)) {
21451
21806
  if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
21452
21807
  if (child === row.extrasEl) {
@@ -21464,10 +21819,12 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21464
21819
  }
21465
21820
  remove(id) {
21466
21821
  const row = this.rows.get(id);
21822
+ for (const bubble of row?.callouts ?? []) bubble.destroy();
21467
21823
  row?.el.remove();
21468
21824
  this.rows.delete(id);
21469
21825
  this.disposeTips(this.rowTips, id);
21470
21826
  this.disposeTips(this.extrasTips, id);
21827
+ this.disposeTips(this.calloutTips, id);
21471
21828
  if (this.selectedId === id) this.selectedId = null;
21472
21829
  if (this.inputsDialog.openId === id) this.inputsDialog.close();
21473
21830
  this.syncFoldToggle();
@@ -21487,6 +21844,8 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21487
21844
  this.rowMenu = null;
21488
21845
  for (const id of [...this.rowTips.keys()]) this.disposeTips(this.rowTips, id);
21489
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();
21490
21849
  for (const lg of this.legends.values()) lg.remove();
21491
21850
  this.legends.clear();
21492
21851
  this.rows.clear();
@@ -21841,6 +22200,32 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21841
22200
  large: 16,
21842
22201
  huge: 20
21843
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
+ }
21844
22229
  var TableOverlay = class {
21845
22230
  constructor(container, theme, paneBounds) {
21846
22231
  this.container = container;
@@ -21861,7 +22246,9 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21861
22246
  update(tables) {
21862
22247
  this.lastTables = tables;
21863
22248
  this.root.replaceChildren();
21864
- 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
+ }
21865
22252
  }
21866
22253
  /** Re-render at the current pane geometry — after layout settles or on resize. */
21867
22254
  reposition() {
@@ -21876,39 +22263,36 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21876
22263
  this.root.remove();
21877
22264
  }
21878
22265
  renderTable(t) {
22266
+ const b = this.paneBounds(t.paneId);
21879
22267
  const wrap2 = document.createElement("div");
21880
22268
  wrap2.style.position = "absolute";
21881
- 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);
21882
22271
  const table = document.createElement("table");
21883
22272
  Object.assign(table.style, {
21884
22273
  borderCollapse: "collapse",
21885
22274
  background: t.bgColor ?? "transparent",
21886
22275
  fontFamily: this.theme.fontFamily || "sans-serif",
21887
- // Frame as an inset shadow (not a `border`) so it stays independent of the
21888
- // collapsed cell borders even when frame_width != border_width.
21889
- boxShadow: t.frameColor && t.frameWidth > 0 ? `inset 0 0 0 ${t.frameWidth}px ${t.frameColor}` : "none",
21890
22276
  border: "none",
21891
22277
  tableLayout: "auto",
21892
22278
  // Re-enable pointer events on the table only (root is none) so cell tooltips work.
21893
22279
  pointerEvents: "auto"
21894
22280
  });
21895
- const span = /* @__PURE__ */ new Map();
21896
- const skip = /* @__PURE__ */ new Set();
21897
- for (const m of t.merges) {
21898
- span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
21899
- for (let r = m.startRow; r <= m.endRow; r += 1) {
21900
- for (let c = m.startCol; c <= m.endCol; c += 1) {
21901
- if (r !== m.startRow || c !== m.startCol) skip.add(`${r}:${c}`);
21902
- }
21903
- }
21904
- }
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;
21905
22284
  const cellBorder = t.borderColor && t.borderWidth > 0 ? `${t.borderWidth}px solid ${t.borderColor}` : "none";
21906
22285
  for (let r = 0; r < t.rows; r += 1) {
21907
22286
  const tr = document.createElement("tr");
21908
22287
  for (let c = 0; c < t.columns; c += 1) {
22288
+ if (omit.has(`${r}:${c}`)) continue;
21909
22289
  const cell = t.cells[r]?.[c] ?? null;
21910
- if (skip.has(`${r}:${c}`) || cell?.merged) continue;
21911
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
+ }
21912
22296
  const sp = span.get(`${r}:${c}`);
21913
22297
  if (sp) {
21914
22298
  if (sp.cs > 1) td.colSpan = sp.cs;
@@ -21917,18 +22301,22 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
21917
22301
  Object.assign(td.style, {
21918
22302
  border: cellBorder,
21919
22303
  padding: "2px 6px",
21920
- background: cell?.bgColor ?? "transparent",
21921
- color: cell?.textColor ?? this.theme.textColor,
21922
- textAlign: cell?.hAlign ?? "center",
21923
- verticalAlign: cell?.vAlign === "top" ? "top" : cell?.vAlign === "bottom" ? "bottom" : "middle",
21924
- fontSize: `${SIZE_PX3[cell?.textSize ?? "normal"]}px`,
21925
- fontFamily: cell?.fontFamily === "monospace" ? "monospace" : "inherit",
21926
- fontWeight: cell?.bold ? "bold" : "normal",
21927
- fontStyle: cell?.italic ? "italic" : "normal",
21928
- 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"
21929
22315
  });
21930
- if (cell?.tooltip) td.title = cell.tooltip;
21931
- 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 ?? "";
21932
22320
  tr.appendChild(td);
21933
22321
  }
21934
22322
  table.appendChild(tr);
@@ -22813,8 +23201,14 @@ void main() {
22813
23201
  return sc === pane.scale ? pane : { ...pane, scale: sc };
22814
23202
  };
22815
23203
  b.alpha = this.modelAlpha;
22816
- for (const m of models) for (const bgSpan of m.backgrounds) this.emitBackground(b, bgSpan, pane, coords);
22817
- 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
+ }
22818
23212
  const drawCandles = isPrice && !scene.candlesHidden;
22819
23213
  let candleDrawn = false;
22820
23214
  for (const m of models) {
@@ -22828,7 +23222,7 @@ void main() {
22828
23222
  b.alpha = this.modelAlpha;
22829
23223
  const off = scene.offsetOf(m.id);
22830
23224
  const mp = effPane(m);
22831
- 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);
22832
23226
  }
22833
23227
  if (drawCandles && !candleDrawn) {
22834
23228
  drawSlicesUpTo(scene.candleZ);
@@ -22837,6 +23231,12 @@ void main() {
22837
23231
  }
22838
23232
  drawSlicesUpTo(Infinity);
22839
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
+ }
22840
23240
  for (const m of models) {
22841
23241
  const mp = effPane(m);
22842
23242
  for (const pl of m.priceLines) this.emitHline(b, pl, mp, coords, dataW, theme);
@@ -22883,7 +23283,7 @@ void main() {
22883
23283
  for (const pane of scene.orderedPanes()) {
22884
23284
  const b = this.glowBatch;
22885
23285
  b.reset();
22886
- this.emitGlowSources(b, scene.indicatorsForPane(pane.id), pane, coords, i0, i1, (id) => scene.offsetOf(id));
23286
+ this.emitGlowSources(b, scene, pane, coords, i0, i1);
22887
23287
  if (b.vertexCount === 0) continue;
22888
23288
  const topH = Math.round(pane.bounds.top * dpr * 0.5);
22889
23289
  const botH = Math.round((pane.bounds.top + pane.bounds.height) * dpr * 0.5);
@@ -22952,15 +23352,23 @@ void main() {
22952
23352
  gl.bindVertexArray(null);
22953
23353
  return true;
22954
23354
  }
22955
- /** The "neon" elements that glow: line/area/step lines + point markers (not candles/fills/bars). */
22956
- emitGlowSources(b, models, pane, coords, i0, i1, offsetOf = () => 0) {
22957
- for (const m of models) {
22958
- const off = offsetOf(m.id);
22959
- for (const s of m.series) {
22960
- if (!isLineLikeSeries(s) || s.visible === false) continue;
22961
- if (s.kind === "histogram" || s.kind === "columns") continue;
22962
- if (s.kind === "circles" || s.kind === "cross") this.emitPointMarkers(b, s, pane, coords, i0, i1, off);
22963
- 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);
22964
23372
  }
22965
23373
  }
22966
23374
  }
@@ -23243,9 +23651,9 @@ void main() {
23243
23651
  b.alpha = this.candleBodyAlpha;
23244
23652
  b.rect(g.bodyX, bodyTop, g.bodyW, bodyH, c);
23245
23653
  }
23246
- if (cs.borderVisible || bc || fading && cs.bodyVisible) {
23654
+ if (cs.borderVisible || fading && cs.bodyVisible) {
23247
23655
  b.alpha = this.candleStructureAlpha;
23248
- 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;
23249
23657
  const bw = Math.max(0, g.bodyW - 1);
23250
23658
  const bh = Math.max(0, bodyH - 1);
23251
23659
  b.rectStroke(g.bodyX + 0.5, bodyTop + 0.5, bw, bh, 1, bord);
@@ -24797,8 +25205,14 @@ void main() {
24797
25205
  return sc === pane.scale ? pane : { ...pane, scale: sc };
24798
25206
  };
24799
25207
  ctx.globalAlpha = this.modelAlpha;
24800
- for (const m of models) for (const bg of m.backgrounds) this.drawBackground(ctx, bg, pane, coords);
24801
- 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
+ }
24802
25216
  const slices = scene.drawingSlices.get(pane.id) ?? [];
24803
25217
  let si = 0;
24804
25218
  const drawSlicesUpTo = (z) => {
@@ -24820,7 +25234,7 @@ void main() {
24820
25234
  ctx.globalAlpha = this.modelAlpha;
24821
25235
  const off = scene.offsetOf(m.id);
24822
25236
  const mp = effPane(m);
24823
- 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);
24824
25238
  }
24825
25239
  if (drawCandles && !candleDrawn) {
24826
25240
  drawSlicesUpTo(scene.candleZ);
@@ -24828,6 +25242,13 @@ void main() {
24828
25242
  this.drawPriceSeries(ctx, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
24829
25243
  }
24830
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
+ }
24831
25252
  ctx.globalAlpha = this.modelAlpha;
24832
25253
  for (const m of models) {
24833
25254
  const mp = effPane(m);
@@ -25099,9 +25520,9 @@ void main() {
25099
25520
  ctx.fillStyle = color;
25100
25521
  ctx.fillRect(g.bodyX, top, g.bodyW, bodyH);
25101
25522
  }
25102
- if (cs.borderVisible || bc || fading && cs.bodyVisible) {
25523
+ if (cs.borderVisible || fading && cs.bodyVisible) {
25103
25524
  ctx.globalAlpha = this.candleStructureAlpha;
25104
- ctx.strokeStyle = cs.borderVisible || bc ? (up ? cs.borderUpColor : cs.borderDownColor) ?? dir : color;
25525
+ ctx.strokeStyle = cs.borderVisible ? (up ? cs.borderUpColor : cs.borderDownColor) ?? color : color;
25105
25526
  ctx.lineWidth = 1;
25106
25527
  const bw = Math.max(0, g.bodyW - 1);
25107
25528
  const bh = Math.max(0, bodyH - 1);
@@ -25533,6 +25954,8 @@ void main() {
25533
25954
  this.set = set;
25534
25955
  /** measureText width cache, keyed by `${font} ${text}`, persists across frames. */
25535
25956
  this.widthCache = /* @__PURE__ */ new Map();
25957
+ /** Tooltip hit-rects captured while drawing the CURRENT set (rebuilt per render). */
25958
+ this.tipRegions = [];
25536
25959
  /**
25537
25960
  * Index offset of the CURRENT set's model: its `xloc:'bar_index'` coordinates count
25538
25961
  * from the model's anchor bar, so they shift by this to land on chart logical indices.
@@ -25553,8 +25976,13 @@ void main() {
25553
25976
  const s = this.set;
25554
25977
  return !s.lines.length && !s.boxes.length && !s.labels.length && !s.polylines.length && !s.linefills.length;
25555
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
+ }
25556
25983
  /** Draw the whole set into `ctx` using the supplied coordinate closures. */
25557
25984
  render(ctx, W, H, xOf, yOf) {
25985
+ this.tipRegions = [];
25558
25986
  if (this.isEmpty()) return;
25559
25987
  ctx.save();
25560
25988
  this.drawLinefills(ctx, W, H, xOf, yOf);
@@ -25902,13 +26330,35 @@ void main() {
25902
26330
  if (this.isPointShape(lb.style)) {
25903
26331
  if (!lb.noFill) this.drawLabelShape(ctx, lb.style, px, py, fontPx, color);
25904
26332
  if (lb.text) this.drawLabelText(ctx, lb, px, py + fontPx, fontPx);
25905
- } else if (lb.style === "none" || lb.style === "text_outline" || lb.noFill) {
25906
- 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
+ }
25907
26342
  } else {
25908
- 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 });
25909
26345
  }
25910
26346
  }
25911
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
+ }
25912
26362
  isPointShape(style) {
25913
26363
  switch (style) {
25914
26364
  case "circle":
@@ -25927,8 +26377,7 @@ void main() {
25927
26377
  }
25928
26378
  }
25929
26379
  drawLabelText(ctx, lb, cx, cy, fontPx, outline = false) {
25930
- const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
25931
- ctx.font = `${fontPx}px ${family}`;
26380
+ ctx.font = this.labelFont(lb, fontPx);
25932
26381
  ctx.textAlign = lb.textAlign === "left" ? "left" : lb.textAlign === "right" ? "right" : "center";
25933
26382
  ctx.textBaseline = "middle";
25934
26383
  const lines = lb.text.split("\n");
@@ -26008,9 +26457,9 @@ void main() {
26008
26457
  ctx.fill();
26009
26458
  }
26010
26459
  }
26460
+ /** Draw a bubble label; returns the bubble body rect (for tooltip hit-testing). */
26011
26461
  drawBubble(ctx, lb, px, py, fontPx, color) {
26012
- const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
26013
- ctx.font = `${fontPx}px ${family}`;
26462
+ ctx.font = this.labelFont(lb, fontPx);
26014
26463
  const lines = (lb.text ?? "").split("\n");
26015
26464
  const padX = 6;
26016
26465
  const padY = 4;
@@ -26068,38 +26517,51 @@ void main() {
26068
26517
  by = py - h - ptr;
26069
26518
  pointer = "down";
26070
26519
  }
26071
- ctx.fillStyle = color;
26072
- this.roundRect(ctx, bx, by, w, h, 4);
26073
- ctx.fill();
26074
- if (pointer !== "none") {
26075
- ctx.beginPath();
26076
- if (pointer === "down") {
26077
- ctx.moveTo(px - ptr, by + h);
26078
- ctx.lineTo(px + ptr, by + h);
26079
- ctx.lineTo(px, py);
26080
- } else if (pointer === "up") {
26081
- ctx.moveTo(px - ptr, by);
26082
- ctx.lineTo(px + ptr, by);
26083
- ctx.lineTo(px, py);
26084
- } else if (pointer === "left") {
26085
- ctx.moveTo(bx, py - ptr);
26086
- ctx.lineTo(bx, py + ptr);
26087
- ctx.lineTo(px, py);
26088
- } else {
26089
- ctx.moveTo(bx + w, py - ptr);
26090
- ctx.lineTo(bx + w, py + ptr);
26091
- ctx.lineTo(px, py);
26092
- }
26093
- ctx.closePath();
26520
+ if (!lb.noFill) {
26521
+ ctx.fillStyle = color;
26522
+ this.roundRect(ctx, bx, by, w, h, 4);
26094
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
+ }
26095
26546
  }
26096
26547
  if (lb.text) {
26097
- ctx.fillStyle = lb.textColor ?? contrastColor(color);
26098
- ctx.textAlign = "center";
26548
+ ctx.fillStyle = lb.textColor ?? (lb.noFill ? this.deps.theme.textColor : contrastColor(color));
26099
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
+ }
26100
26561
  const startY = by + padY + lineH / 2;
26101
- 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);
26102
26563
  }
26564
+ return { x: bx, y: by, w, h };
26103
26565
  }
26104
26566
  roundRect(ctx, x, y, w, h, r) {
26105
26567
  const rr = Math.min(r, w / 2, h / 2);
@@ -26337,6 +26799,8 @@ void main() {
26337
26799
  this.axisTextColor = DARK_THEME.textColor;
26338
26800
  // Shared Pine-drawing renderer (line/box/label/polyline/linefill); widthCache persists.
26339
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 = [];
26340
26804
  }
26341
26805
  mount(canvas) {
26342
26806
  this.canvas = canvas;
@@ -26378,6 +26842,7 @@ void main() {
26378
26842
  const dataW = coords.width;
26379
26843
  const dataH = coords.height;
26380
26844
  this.axisTextColor = surface?.textColor ?? theme.textColor;
26845
+ this.labelTips = [];
26381
26846
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
26382
26847
  ctx.clearRect(0, 0, fullW, fullH);
26383
26848
  if (surface && (fullW > dataW || fullH > dataH)) {
@@ -26487,6 +26952,17 @@ void main() {
26487
26952
  (price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
26488
26953
  );
26489
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;
26490
26966
  }
26491
26967
  // ── axes ──
26492
26968
  drawPriceAxes(ctx, scene, coords, theme, dataW, panes) {
@@ -26703,6 +27179,68 @@ void main() {
26703
27179
  return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
26704
27180
  }
26705
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
+
26706
27244
  // src/renderers/native/chrome/CrosshairRenderer.ts
26707
27245
  var CrosshairRenderer = class {
26708
27246
  constructor() {
@@ -32494,19 +33032,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
32494
33032
  for (const model of models) {
32495
33033
  const off = offsetOf(model.id);
32496
33034
  for (const s of model.series) {
32497
- if (s.kind === "candle" || s.kind === "bar") {
32498
- for (let i = i0; i <= i1; i += 1) {
32499
- const b = s.bars[i - off];
32500
- if (b) {
32501
- consider(b.high);
32502
- consider(b.low);
32503
- }
32504
- }
32505
- } else if (isLineLikeSeries(s)) {
32506
- for (let i = i0; i <= i1; i += 1) consider(s.points[i - off]?.value);
32507
- if (s.kind === "histogram" || s.kind === "columns") consider(s.style?.base ?? 0);
32508
- else if (s.style?.base != null) consider(s.style.base);
32509
- }
33035
+ if (s.overlay === true) continue;
33036
+ considerSeries(s, i0, i1, off, consider);
32510
33037
  }
32511
33038
  for (const pl of model.priceLines) consider(pl.price);
32512
33039
  }
@@ -32528,6 +33055,36 @@ ${overlayScrollbarCss(".vela-sd-pane")}
32528
33055
  const span = max2 - min2;
32529
33056
  return { min: min2 - span * MARGIN_BOTTOM, max: max2 + span * MARGIN_TOP };
32530
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
+ }
32531
33088
  function expandScaleByPixels(scale, heightPx, abovePx, belowPx) {
32532
33089
  if (abovePx <= 0 && belowPx <= 0) return scale;
32533
33090
  const content = heightPx - abovePx - belowPx;
@@ -32582,6 +33139,11 @@ ${overlayScrollbarCss(".vela-sd-pane")}
32582
33139
  }
32583
33140
 
32584
33141
  // src/renderers/native/backdrop/BackdropRenderer.ts
33142
+ function clipHighlightRect(x1, x2, left, right) {
33143
+ const x = Math.max(left, x1);
33144
+ const end = Math.min(right, x2);
33145
+ return end > x ? { x, width: end - x } : null;
33146
+ }
32585
33147
  var BackdropRenderer = class {
32586
33148
  constructor() {
32587
33149
  this.canvas = null;
@@ -32614,17 +33176,22 @@ ${overlayScrollbarCss(".vela-sd-pane")}
32614
33176
  /** Renderer-owned session highlight bands: full-height (all panes), behind the grid.
32615
33177
  * Session-zone washes (pre/post-market) paint first, host highlights on top. */
32616
33178
  drawHighlights(ctx, scene, coords) {
32617
- const bands = [...scene.sessionHighlightBands(), ...scene.highlights];
32618
- if (bands.length === 0) return;
33179
+ const sessions = scene.sessionHighlightBands();
33180
+ if (sessions.length > 0) {
33181
+ const left = Math.max(0, coords.logicalToX(-0.5));
33182
+ const right = Math.min(coords.width, coords.logicalToX(coords.barCount - 0.5));
33183
+ this.drawHighlightSet(ctx, sessions, coords, left, right);
33184
+ }
33185
+ this.drawHighlightSet(ctx, scene.highlights, coords, 0, coords.width);
33186
+ }
33187
+ drawHighlightSet(ctx, bands, coords, left, right) {
32619
33188
  for (const band of bands) {
32620
33189
  const x1 = coords.timeToX(band.from);
32621
33190
  const x2 = coords.timeToX(band.to);
32622
- if (x2 < 0 || x1 > coords.width || x2 <= x1) continue;
32623
- const cx = Math.max(0, x1);
32624
- const cw = Math.min(coords.width, x2) - cx;
32625
- if (cw <= 0) continue;
33191
+ const rect = clipHighlightRect(x1, x2, left, right);
33192
+ if (!rect) continue;
32626
33193
  ctx.fillStyle = band.color;
32627
- ctx.fillRect(cx, 0, cw, coords.height);
33194
+ ctx.fillRect(rect.x, 0, rect.width, coords.height);
32628
33195
  }
32629
33196
  }
32630
33197
  // ── grid ── vert/horz gate on `scene.showGrid` AND their own per-axis visibility
@@ -33300,6 +33867,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33300
33867
  this.glowAmount = 0;
33301
33868
  // WebGL2 neon-glow intensity (canvas2d ignores it)
33302
33869
  this.chrome = new ChromeRenderer();
33870
+ /** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
33871
+ this.labelTooltip = null;
33303
33872
  this.crosshairLayer = new CrosshairRenderer();
33304
33873
  /** 1 Hz repaint pump so the price-axis countdown-to-bar-close ticks; null when off. */
33305
33874
  this.countdownTimer = null;
@@ -33310,6 +33879,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33310
33879
  this.indicatorValuesOn = true;
33311
33880
  /** Host-contributed legend actions — held here so a rebuild of the legend re-wires them. */
33312
33881
  this.legendActionsProvider = null;
33882
+ /** Host-contributed legend callouts — held here so a rebuild of the legend re-wires them. */
33883
+ this.legendCalloutsProvider = null;
33313
33884
  /** Host override of the legend's fold toggle — held here so a remount re-applies it. */
33314
33885
  this.legendOverviewAction = null;
33315
33886
  // ── keyboard navigation / accessibility (item 11) ──
@@ -34418,6 +34989,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34418
34989
  this.plot.appendChild(this.scrollButton);
34419
34990
  this.plot.addEventListener("pointermove", this.onScrollProximityMove);
34420
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
+ });
34421
34996
  this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
34422
34997
  projector: () => this.drawingProjector(),
34423
34998
  dpr: () => this.coords.dpr,
@@ -34457,9 +35032,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34457
35032
  this.inputsUI.setDialogHost(this.dialogHost);
34458
35033
  this.inputsUI.setSymbolPicker(this.symbolPicker);
34459
35034
  this.inputsUI.setLegendActions(this.legendActionsProvider);
35035
+ this.inputsUI.setLegendCallouts(this.legendCalloutsProvider);
34460
35036
  this.inputsUI.setLegendOverviewAction(this.legendOverviewAction);
34461
35037
  this.inputsUI.setOnChange((c) => {
34462
- 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 } : {} });
34463
35039
  });
34464
35040
  this.inputsUI.setOnRemove((id) => {
34465
35041
  for (const cb of this.removeIndicatorCbs) cb(id);
@@ -34649,6 +35225,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34649
35225
  this.settingsButton = null;
34650
35226
  this.plot?.removeEventListener("pointermove", this.onScrollProximityMove);
34651
35227
  this.plot?.removeEventListener("pointerleave", this.onScrollProximityLeave);
35228
+ this.labelTooltip?.destroy();
35229
+ this.labelTooltip = null;
34652
35230
  this.scrollButton?.remove();
34653
35231
  this.scrollButton = null;
34654
35232
  for (const l of this.extLayers) l.instance.destroy?.();
@@ -34889,7 +35467,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34889
35467
  else this.scene.assignIndicatorZ(model.id);
34890
35468
  this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
34891
35469
  native: !!model.native,
34892
- ...model.shorttitle ? { settingsTitle: model.title } : {}
35470
+ ...model.props ? { props: model.props, propValues: model.propValues ?? {} } : {}
34893
35471
  });
34894
35472
  this.syncTables(model);
34895
35473
  if (model.native?.type === "volume") {
@@ -34940,8 +35518,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34940
35518
  this.paneControls?.refresh();
34941
35519
  this.scheduler.invalidate(4 /* Full */);
34942
35520
  }
34943
- setIndicatorInputs(handle, values) {
34944
- this.inputsUI.setValues(handle.id, values);
35521
+ setIndicatorInputs(handle, values, props) {
35522
+ this.inputsUI.setValues(handle.id, values, props);
34945
35523
  }
34946
35524
  setSymbolPicker(picker) {
34947
35525
  this.symbolPicker = picker;
@@ -34951,6 +35529,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34951
35529
  this.legendActionsProvider = provider;
34952
35530
  this.inputsUI?.setLegendActions(provider);
34953
35531
  }
35532
+ setLegendCallouts(provider) {
35533
+ this.legendCalloutsProvider = provider;
35534
+ this.inputsUI?.setLegendCallouts(provider);
35535
+ }
34954
35536
  setLegendOverviewAction(action) {
34955
35537
  this.legendOverviewAction = action;
34956
35538
  this.inputsUI?.setLegendOverviewAction(action);
@@ -35931,7 +36513,11 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35931
36513
  }
35932
36514
  const models = this.scene.indicatorsForPane(pane.id);
35933
36515
  const masterModels = models.filter((m) => m.ownScale !== true);
35934
- 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
+ }
35935
36521
  const includeCandles = pane.kind === "price" && !this.scene.candlesHidden;
35936
36522
  pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
35937
36523
  pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
@@ -36205,6 +36791,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36205
36791
  for (const m of models) {
36206
36792
  const off = this.scene.offsetOf(m.id);
36207
36793
  for (const s of m.series) {
36794
+ if (s.overlay === true) continue;
36208
36795
  if (isLineLikeSeries(s)) {
36209
36796
  const v = s.points[i0 - off]?.value;
36210
36797
  if (v != null && Number.isFinite(v)) return v;
@@ -37110,6 +37697,19 @@ ${overlayScrollbarCss(".vela-sd-pane")}
37110
37697
  function legendActions() {
37111
37698
  return [...legendRegistry.values()].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
37112
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
+ }
37113
37713
  var stateHandlers = /* @__PURE__ */ new Map();
37114
37714
  function registerStatePersistence(handler) {
37115
37715
  stateHandlers.set(handler.key, handler);
@@ -38453,6 +39053,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
38453
39053
  exports.iconMarkup = iconMarkup;
38454
39054
  exports.inputVisible = inputVisible;
38455
39055
  exports.legendActions = legendActions;
39056
+ exports.legendCallouts = legendCallouts;
38456
39057
  exports.nativeIndicatorDescriptors = nativeIndicatorDescriptors;
38457
39058
  exports.nativeIndicatorTypes = nativeIndicatorTypes;
38458
39059
  exports.normalizeSettingsRow = normalizeSettingsRow;
@@ -38461,6 +39062,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
38461
39062
  exports.registerDrawingType = registerDrawingType;
38462
39063
  exports.registerIcon = registerIcon;
38463
39064
  exports.registerLegendAction = registerLegendAction;
39065
+ exports.registerLegendCallout = registerLegendCallout;
38464
39066
  exports.registerNativeIndicator = registerNativeIndicator;
38465
39067
  exports.registerRendererDefaults = registerRendererDefaults;
38466
39068
  exports.registerRendererLayer = registerRendererLayer;
@@ -38486,6 +39088,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
38486
39088
  exports.unregisterChartType = unregisterChartType;
38487
39089
  exports.unregisterDefaultEngine = unregisterDefaultEngine;
38488
39090
  exports.unregisterLegendAction = unregisterLegendAction;
39091
+ exports.unregisterLegendCallout = unregisterLegendCallout;
38489
39092
  exports.unregisterNativeIndicator = unregisterNativeIndicator;
38490
39093
  exports.unregisterRendererDefaults = unregisterRendererDefaults;
38491
39094
  exports.unregisterRendererLayer = unregisterRendererLayer;