@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
package/dist/index.cjs CHANGED
@@ -138,6 +138,7 @@ var IndicatorHandleImpl = class {
138
138
  constructor(id, title, controller, source) {
139
139
  this.controller = controller;
140
140
  this.schema = [];
141
+ this.propsSchema = [];
141
142
  this.visibleState = true;
142
143
  this.bus = new TypedEventBus();
143
144
  this.id = id;
@@ -147,6 +148,9 @@ var IndicatorHandleImpl = class {
147
148
  get inputs() {
148
149
  return this.schema;
149
150
  }
151
+ get props() {
152
+ return this.propsSchema;
153
+ }
150
154
  get visible() {
151
155
  return this.visibleState;
152
156
  }
@@ -156,6 +160,12 @@ var IndicatorHandleImpl = class {
156
160
  setInputs(values) {
157
161
  this.controller.applyInputs(this.id, values);
158
162
  }
163
+ setProp(key, value) {
164
+ this.controller.applyProps(this.id, { [key]: value });
165
+ }
166
+ setProps(values) {
167
+ this.controller.applyProps(this.id, values);
168
+ }
159
169
  setVisible(visible) {
160
170
  this.controller.setVisible(this.id, visible);
161
171
  }
@@ -175,6 +185,9 @@ var IndicatorHandleImpl = class {
175
185
  setSchema(schema) {
176
186
  this.schema = schema;
177
187
  }
188
+ setPropsSchema(schema) {
189
+ this.propsSchema = schema;
190
+ }
178
191
  /** Sync the public `visible` getter when the orchestrator changes visibility (API or legend eye). */
179
192
  setVisibleState(visible) {
180
193
  this.visibleState = visible;
@@ -188,6 +201,8 @@ var IndicatorHandleImpl = class {
188
201
  function summarizeModel(model) {
189
202
  const series = {};
190
203
  for (const s of model.series) series[s.kind] = (series[s.kind] ?? 0) + 1;
204
+ const countOverlay = (items) => items?.filter((i) => i.overlay === true).length ?? 0;
205
+ const forcedOverlay = countOverlay(model.series) + countOverlay(model.fills) + countOverlay(model.backgrounds) + countOverlay(model.lines) + countOverlay(model.boxes) + countOverlay(model.labels) + countOverlay(model.polylines) + countOverlay(model.linefills) + countOverlay(model.tables);
191
206
  return {
192
207
  id: model.id,
193
208
  title: model.title,
@@ -207,7 +222,8 @@ function summarizeModel(model) {
207
222
  tables: model.tables?.length ?? 0,
208
223
  barColors: model.barColors?.length ?? 0,
209
224
  trades: model.trades?.length ?? 0,
210
- inputs: model.inputs.length
225
+ inputs: model.inputs.length,
226
+ forcedOverlay
211
227
  };
212
228
  }
213
229
  function inspectModels(models) {
@@ -228,7 +244,8 @@ function inspectModels(models) {
228
244
  linefills: sum((s) => s.linefills),
229
245
  tables: sum((s) => s.tables),
230
246
  barColors: sum((s) => s.barColors),
231
- trades: sum((s) => s.trades)
247
+ trades: sum((s) => s.trades),
248
+ forcedOverlay: sum((s) => s.forcedOverlay)
232
249
  }
233
250
  };
234
251
  }
@@ -498,11 +515,11 @@ registerIcon(
498
515
  );
499
516
  registerIcon(
500
517
  "market-pre",
501
- svg24('<path d="M12 2v8"/><path d="m4.93 10.93 1.41 1.41"/><path d="M2 18h2"/><path d="M20 18h2"/><path d="m19.07 10.93-1.41 1.41"/><path d="M22 22H2"/><path d="m16 6-4 4-4-4"/><path d="M16 18a4 4 0 0 0-8 0"/>')
518
+ svg24('<path d="M12 2v8"/><path d="m4.93 10.93 1.41 1.41"/><path d="M2 18h2"/><path d="M20 18h2"/><path d="m19.07 10.93-1.41 1.41"/><path d="M22 22H2"/><path d="m8 6 4-4 4 4"/><path d="M16 18a4 4 0 0 0-8 0"/>')
502
519
  );
503
520
  registerIcon(
504
521
  "market-post",
505
- svg24('<path d="M12 10V2"/><path d="m4.93 10.93 1.41 1.41"/><path d="M2 18h2"/><path d="M20 18h2"/><path d="m19.07 10.93-1.41 1.41"/><path d="M22 22H2"/><path d="m16 18-4-4-4 4"/><path d="M16 6a4 4 0 0 0-8 0"/>')
522
+ svg24('<path d="M12 10V2"/><path d="m4.93 10.93 1.41 1.41"/><path d="M2 18h2"/><path d="M20 18h2"/><path d="m19.07 10.93-1.41 1.41"/><path d="M22 22H2"/><path d="m16 14-4 4-4-4"/><path d="M16 6a4 4 0 0 0-8 0"/>')
506
523
  );
507
524
  registerIcon("market-closed", svg24('<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>'));
508
525
  registerIcon(
@@ -7342,7 +7359,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
7342
7359
  for (const engine of engines) this.registerEngine(engine.language, engine);
7343
7360
  this.defaultLanguage = config.defaultLanguage;
7344
7361
  this.renderer.mount(container, config.theme);
7345
- this.renderer.onInputChange((e) => this.applyInputs(e.indicatorId, { [e.key]: e.value }));
7362
+ this.renderer.onInputChange(
7363
+ (e) => e.kind === "prop" ? this.applyProps(e.indicatorId, { [e.key]: e.value }) : this.applyInputs(e.indicatorId, { [e.key]: e.value })
7364
+ );
7346
7365
  this.renderer.onRemoveIndicator((id) => this.removeIndicator(id));
7347
7366
  this.renderer.onToggleIndicatorVisible?.((id, visible) => this.setVisible(id, visible));
7348
7367
  this.paneActionUnsub = this.renderer.onPaneAction?.((a) => this.handlePaneAction(a)) ?? null;
@@ -8022,7 +8041,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
8022
8041
  const title = options.title ?? "Indicator";
8023
8042
  const handle = new IndicatorHandleImpl(id, title, this, source);
8024
8043
  this.handles.set(id, handle);
8025
- this.registry.add({ id, title, source, options, inputValues: { ...options.inputs ?? {} } });
8044
+ this.registry.add({ id, title, source, options, inputValues: { ...options.inputs ?? {} }, propValues: { ...options.props ?? {} } });
8026
8045
  void this.startIndicator(id, source, options, handle);
8027
8046
  return handle;
8028
8047
  }
@@ -8045,7 +8064,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
8045
8064
  return handle;
8046
8065
  }
8047
8066
  const inputValues = { ...descriptor.defaultInputs(), ...options.inputs ?? {} };
8048
- this.registry.add({ id, title, source: type, inputValues, native: { type, instance: descriptor.create(), descriptor } });
8067
+ this.registry.add({ id, title, source: type, inputValues, propValues: {}, native: { type, instance: descriptor.create(), descriptor } });
8049
8068
  handle.setSchema(descriptor.inputsSchema());
8050
8069
  void this.startNativeIndicator(id, handle);
8051
8070
  return handle;
@@ -8098,6 +8117,19 @@ var EngineOrchestrator = class _EngineOrchestrator {
8098
8117
  if (record.session) record.session.update(record.inputValues);
8099
8118
  else if (record.native && !record.hidden) record.native.instance.setInputs(record.inputValues);
8100
8119
  }
8120
+ /** IndicatorController: re-run an indicator with merged declaration-prop overrides.
8121
+ * Same lifecycle as {@link applyInputs} — a prop change replays the whole script.
8122
+ * Script indicators only: natives have no declaration props. */
8123
+ applyProps(id, values) {
8124
+ const record = this.registry.get(id);
8125
+ if (!record?.session) return;
8126
+ record.propValues = { ...record.propValues, ...values };
8127
+ if (record.renderHandle) this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
8128
+ record.pendingStructural = true;
8129
+ if (!record.hidden) this.setLoading(record, true);
8130
+ record.pendingCause = "inputs";
8131
+ record.session.update(record.inputValues, record.propValues);
8132
+ }
8101
8133
  /** IndicatorController: tear down an indicator and (if now empty) its pane. */
8102
8134
  /** Live handles of every indicator on the chart (script + native), insertion order. */
8103
8135
  listIndicators() {
@@ -8246,6 +8278,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
8246
8278
  for (const input of prepared.inputs) defaults2[input.key] = input.defval;
8247
8279
  record.inputValues = { ...defaults2, ...record.inputValues };
8248
8280
  handle.setSchema(prepared.inputs);
8281
+ const propDefaults = {};
8282
+ for (const prop of prepared.props ?? []) propDefaults[prop.key] = prop.defval;
8283
+ record.propValues = { ...propDefaults, ...record.propValues };
8284
+ handle.setPropsSchema(prepared.props ?? []);
8249
8285
  this.mountLoadingPlaceholder(id, record);
8250
8286
  this.executeIndicator(id, handle);
8251
8287
  } catch (err) {
@@ -8269,6 +8305,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
8269
8305
  getBars: () => this.bars,
8270
8306
  fetchSeries: (sym, tf, range) => this.fetchSeries(sym, tf, range),
8271
8307
  inputs: record.inputValues,
8308
+ props: record.propValues,
8272
8309
  visibleRange: this.currentVisibleRange(),
8273
8310
  mode,
8274
8311
  // Mid-backfill session starts (add / re-show / price-style re-execute) let the
@@ -8362,6 +8399,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
8362
8399
  const meta = record.prepared.meta;
8363
8400
  const model = {
8364
8401
  id,
8402
+ // Deliberately the FULL title, never a shorttitle: while the script loads the
8403
+ // legend identifies it by its full name; the compact shorttitle arrives with
8404
+ // the first computed model and takes over from there.
8365
8405
  title: record.options?.title ?? meta.title,
8366
8406
  overlay: meta.overlay,
8367
8407
  paneHint: meta.overlay ? "price" : "new",
@@ -8370,7 +8410,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
8370
8410
  backgrounds: [],
8371
8411
  priceLines: [],
8372
8412
  inputs: record.prepared.inputs,
8373
- inputValues: record.inputValues
8413
+ inputValues: record.inputValues,
8414
+ ...record.prepared.props ? { props: record.prepared.props, propValues: record.propValues } : {}
8374
8415
  };
8375
8416
  const paneId = this.routePane(id, model, record.options ?? {});
8376
8417
  this.placeModel(model, id, paneId);
@@ -8603,7 +8644,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
8603
8644
  record.model = model;
8604
8645
  if (record.pendingStructural) {
8605
8646
  record.renderHandle = this.renderer.mountIndicator(model);
8606
- this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues);
8647
+ this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
8607
8648
  record.pendingStructural = false;
8608
8649
  } else {
8609
8650
  this.renderer.updateIndicator(record.renderHandle, modelToValuePatch(model));
@@ -8630,16 +8671,17 @@ var EngineOrchestrator = class _EngineOrchestrator {
8630
8671
  placeModel(model, id, paneId) {
8631
8672
  model.id = id;
8632
8673
  model.paneId = paneId;
8633
- for (const series of model.series) series.paneId = paneId;
8634
- for (const fill of model.fills) fill.paneId = paneId;
8635
- for (const bg of model.backgrounds) bg.paneId = paneId;
8674
+ const paneOf = (item) => item.overlay === true ? "price" : paneId;
8675
+ for (const series of model.series) series.paneId = paneOf(series);
8676
+ for (const fill of model.fills) fill.paneId = paneOf(fill);
8677
+ for (const bg of model.backgrounds) bg.paneId = paneOf(bg);
8636
8678
  for (const line of model.priceLines) line.paneId = paneId;
8637
- if (model.lines) for (const ln of model.lines) ln.paneId = paneId;
8638
- if (model.boxes) for (const bx of model.boxes) bx.paneId = paneId;
8639
- if (model.labels) for (const lb of model.labels) lb.paneId = paneId;
8640
- if (model.polylines) for (const pl of model.polylines) pl.paneId = paneId;
8641
- if (model.linefills) for (const lf of model.linefills) lf.paneId = paneId;
8642
- if (model.tables) for (const tb of model.tables) tb.paneId = paneId;
8679
+ if (model.lines) for (const ln of model.lines) ln.paneId = paneOf(ln);
8680
+ if (model.boxes) for (const bx of model.boxes) bx.paneId = paneOf(bx);
8681
+ if (model.labels) for (const lb of model.labels) lb.paneId = paneOf(lb);
8682
+ if (model.polylines) for (const pl of model.polylines) pl.paneId = paneOf(pl);
8683
+ if (model.linefills) for (const lf of model.linefills) lf.paneId = paneOf(lf);
8684
+ if (model.tables) for (const tb of model.tables) tb.paneId = paneOf(tb);
8643
8685
  }
8644
8686
  emitContextChanged(id) {
8645
8687
  if (!this.registry.get(id)?.session?.getContext) return;
@@ -8792,6 +8834,15 @@ var RendererControl = class {
8792
8834
  setLegendActions(provider) {
8793
8835
  this.renderer.setLegendActions?.(provider);
8794
8836
  }
8837
+ /**
8838
+ * Wire the legend rows' HOST-CONTRIBUTED callout bubbles (the shells route the
8839
+ * plugin registry through this; see `registerLegendCallout`). Silent on a renderer
8840
+ * without the seam — contributed callouts simply never show there, same graceful
8841
+ * degradation as {@link setLegendActions}.
8842
+ */
8843
+ setLegendCallouts(provider) {
8844
+ this.renderer.setLegendCallouts?.(provider);
8845
+ }
8795
8846
  /**
8796
8847
  * Replace the indicator legend's fold toggle with a host action (or restore it with
8797
8848
  * `null`) — multi-chart shells point the chip at their indicator overview instead of
@@ -10379,13 +10430,6 @@ var Menu = class {
10379
10430
  }
10380
10431
  };
10381
10432
 
10382
- // src/core/model/inputs.ts
10383
- function inputVisible(when, values) {
10384
- if (!when) return true;
10385
- const conds = Array.isArray(when) ? when : [when];
10386
- return conds.every((c) => c.anyOf ? c.anyOf.some((x) => x === values[c.key]) : values[c.key] === c.equals);
10387
- }
10388
-
10389
10433
  // src/ui/tokens.ts
10390
10434
  var STATIC_ID = "vela-ui-tokens";
10391
10435
  var STATIC_DECLS = Object.entries(STATIC_TOKENS).map(([k, v]) => ` ${k}: ${v};`).join("\n");
@@ -10631,6 +10675,199 @@ var Popover = class {
10631
10675
  }
10632
10676
  };
10633
10677
 
10678
+ // src/ui/components/callout-bubble/controller.ts
10679
+ function closesPanel(item) {
10680
+ return item.close !== false;
10681
+ }
10682
+ function calloutPanelRows(items) {
10683
+ const rows = [];
10684
+ for (const item of items) {
10685
+ if (item.type === "text") {
10686
+ rows.push({ type: "text", text: item.text });
10687
+ continue;
10688
+ }
10689
+ const last = rows[rows.length - 1];
10690
+ if (last && last.type === "buttons") last.buttons.push(item);
10691
+ else rows.push({ type: "buttons", buttons: [item] });
10692
+ }
10693
+ return rows;
10694
+ }
10695
+
10696
+ // src/ui/components/callout-bubble/styles.ts
10697
+ var CALLOUT_STYLE_ID = "vela-ui-callout-bubble";
10698
+ var CALLOUT_CSS = `
10699
+ .vela-callout {
10700
+ display: inline-grid;
10701
+ place-items: center;
10702
+ border-radius: 50%;
10703
+ flex: none;
10704
+ line-height: 0;
10705
+ box-sizing: border-box;
10706
+ cursor: default;
10707
+ user-select: none;
10708
+ -webkit-user-select: none;
10709
+ }
10710
+ .vela-callout[role='button'] { cursor: pointer; }
10711
+ .vela-callout svg { display: block; }
10712
+ /* The deployed panel \u2014 carries the kit's elevated-card look itself (the popover
10713
+ shell is bare positioning chrome). */
10714
+ .vela-callout-panel {
10715
+ display: flex;
10716
+ flex-direction: column;
10717
+ gap: 8px;
10718
+ padding: 10px 12px;
10719
+ max-width: 280px;
10720
+ box-sizing: border-box;
10721
+ background: var(--vela-surface-elev);
10722
+ border: 1px solid var(--vela-border-strong);
10723
+ border-radius: 6px;
10724
+ box-shadow: var(--vela-shadow);
10725
+ color: var(--vela-fg);
10726
+ font: var(--vela-font-size-md) var(--vela-font);
10727
+ }
10728
+ .vela-callout-title { font-weight: 600; color: var(--vela-fg-bright); }
10729
+ .vela-callout-text { color: var(--vela-fg-muted); line-height: 1.45; white-space: pre-line; }
10730
+ .vela-callout-actions { display: flex; flex-wrap: wrap; gap: 8px; }
10731
+ .vela-callout-btn {
10732
+ cursor: pointer;
10733
+ height: 26px;
10734
+ padding: 0 10px;
10735
+ border-radius: 5px;
10736
+ border: 1px solid var(--vela-border);
10737
+ background: transparent;
10738
+ color: var(--vela-fg);
10739
+ font-size: var(--vela-font-size-md);
10740
+ font-family: inherit;
10741
+ transition: background var(--vela-dur-fast) ease, color var(--vela-dur-fast) ease, opacity var(--vela-dur-fast) ease, border-color var(--vela-dur-fast) ease;
10742
+ }
10743
+ .vela-callout-btn:hover { background: var(--vela-hover); color: var(--vela-fg-bright); border-color: var(--vela-fg-muted); }
10744
+ .vela-callout-btn-primary { border-color: var(--vela-selected-bg); background: var(--vela-selected-bg); color: var(--vela-selected-fg); }
10745
+ .vela-callout-btn-primary:hover { background: var(--vela-selected-bg); color: var(--vela-selected-fg); opacity: 0.85; border-color: var(--vela-selected-bg); }
10746
+ `;
10747
+
10748
+ // src/ui/components/callout-bubble/view.ts
10749
+ var CalloutBubble = class {
10750
+ constructor(opts) {
10751
+ this.pop = null;
10752
+ this.spec = { icon: opts.icon, background: opts.background, label: opts.label };
10753
+ if (opts.color !== void 0) this.spec.color = opts.color;
10754
+ if (opts.panel !== void 0) this.spec.panel = opts.panel;
10755
+ this.size = opts.size ?? 16;
10756
+ this.host = opts.host;
10757
+ this.theme = opts.theme;
10758
+ this.boundary = opts.boundary;
10759
+ const doc = (opts.host ?? document.body).ownerDocument;
10760
+ injectStyles(CALLOUT_STYLE_ID, CALLOUT_CSS, doc);
10761
+ this.el = doc.createElement("span");
10762
+ this.el.className = "vela-callout";
10763
+ this.el.style.width = `${this.size}px`;
10764
+ this.el.style.height = `${this.size}px`;
10765
+ this.el.addEventListener("click", (e) => {
10766
+ if (!this.spec.panel) return;
10767
+ e.stopPropagation();
10768
+ this.toggle();
10769
+ });
10770
+ this.el.addEventListener("dblclick", (e) => {
10771
+ if (this.spec.panel) e.stopPropagation();
10772
+ });
10773
+ this.el.addEventListener("keydown", (e) => {
10774
+ if (!this.spec.panel || e.key !== "Enter" && e.key !== " ") return;
10775
+ e.preventDefault();
10776
+ this.toggle();
10777
+ });
10778
+ this.dress();
10779
+ }
10780
+ /** Re-dress the bubble (a status change: new icon, tint, label, panel). */
10781
+ set(spec) {
10782
+ this.spec = { ...this.spec, ...spec };
10783
+ if ("panel" in spec) this.pop?.hide();
10784
+ this.dress();
10785
+ }
10786
+ /** Whether the deployed panel is currently open. */
10787
+ get open() {
10788
+ return this.pop?.open ?? false;
10789
+ }
10790
+ destroy() {
10791
+ this.pop?.destroy();
10792
+ this.pop = null;
10793
+ this.el.remove();
10794
+ }
10795
+ dress() {
10796
+ const clickable = this.spec.panel !== void 0;
10797
+ this.el.style.background = this.spec.background;
10798
+ this.el.style.color = this.spec.color ?? "";
10799
+ this.el.innerHTML = iconAt(this.spec.icon, this.size - 4);
10800
+ this.el.setAttribute("aria-label", this.spec.label);
10801
+ if (clickable) {
10802
+ this.el.setAttribute("role", "button");
10803
+ this.el.tabIndex = 0;
10804
+ } else {
10805
+ this.el.removeAttribute("role");
10806
+ this.el.removeAttribute("tabindex");
10807
+ }
10808
+ }
10809
+ toggle() {
10810
+ if (this.pop?.open) {
10811
+ this.pop.hide();
10812
+ return;
10813
+ }
10814
+ const panel = this.spec.panel;
10815
+ if (!panel) return;
10816
+ this.pop?.destroy();
10817
+ this.pop = new Popover({
10818
+ trigger: this.el,
10819
+ gap: 6,
10820
+ content: (body) => this.buildPanel(body, panel),
10821
+ ...this.host ? { host: this.host } : {},
10822
+ ...this.theme ? { theme: this.theme() } : {},
10823
+ ...this.boundary ? { boundary: this.boundary } : {}
10824
+ });
10825
+ this.pop.show();
10826
+ }
10827
+ buildPanel(body, panel) {
10828
+ const doc = body.ownerDocument;
10829
+ const root = doc.createElement("div");
10830
+ root.className = "vela-callout-panel";
10831
+ if (panel.title) {
10832
+ const title = doc.createElement("div");
10833
+ title.className = "vela-callout-title";
10834
+ title.textContent = panel.title;
10835
+ root.appendChild(title);
10836
+ }
10837
+ for (const row of calloutPanelRows(panel.items)) {
10838
+ if (row.type === "text") {
10839
+ const text = doc.createElement("div");
10840
+ text.className = "vela-callout-text";
10841
+ text.textContent = row.text;
10842
+ root.appendChild(text);
10843
+ continue;
10844
+ }
10845
+ const actions = doc.createElement("div");
10846
+ actions.className = "vela-callout-actions";
10847
+ for (const item of row.buttons) {
10848
+ const btn2 = doc.createElement("button");
10849
+ btn2.type = "button";
10850
+ btn2.className = "vela-callout-btn" + (item.primary ? " vela-callout-btn-primary" : "");
10851
+ btn2.textContent = item.label;
10852
+ btn2.addEventListener("click", () => {
10853
+ item.run();
10854
+ if (closesPanel(item)) this.pop?.hide();
10855
+ });
10856
+ actions.appendChild(btn2);
10857
+ }
10858
+ root.appendChild(actions);
10859
+ }
10860
+ body.appendChild(root);
10861
+ }
10862
+ };
10863
+
10864
+ // src/core/model/inputs.ts
10865
+ function inputVisible(when, values) {
10866
+ if (!when) return true;
10867
+ const conds = Array.isArray(when) ? when : [when];
10868
+ return conds.every((c) => c.anyOf ? c.anyOf.some((x) => x === values[c.key]) : values[c.key] === c.equals);
10869
+ }
10870
+
10634
10871
  // src/ui/components/select/controller.ts
10635
10872
  function selectController(opts) {
10636
10873
  const options = opts.options;
@@ -12545,6 +12782,10 @@ function buildFieldControl(desc) {
12545
12782
  }
12546
12783
 
12547
12784
  // src/renderers/shared/IndicatorInputsDialog.ts
12785
+ var PROPS_TAB = "Properties";
12786
+ function bagOf(row, decl) {
12787
+ return decl.prop ? row.propValues : row.values;
12788
+ }
12548
12789
  var SOURCES = ["close", "open", "high", "low", "hl2", "hlc3", "ohlc4", "volume"];
12549
12790
  var IndicatorInputsDialog = class {
12550
12791
  constructor(host) {
@@ -12555,6 +12796,7 @@ var IndicatorInputsDialog = class {
12555
12796
  this.openId = null;
12556
12797
  this.row = null;
12557
12798
  this.snapshot = null;
12799
+ this.propSnapshot = null;
12558
12800
  this.dialogTips = [];
12559
12801
  /** Re-applies every `when` gate against current values; set while the dialog is open. */
12560
12802
  this.refreshVisibility = null;
@@ -12577,10 +12819,11 @@ var IndicatorInputsDialog = class {
12577
12819
  }
12578
12820
  open(row) {
12579
12821
  this.close();
12580
- if (row.inputs.length === 0) return;
12822
+ if (row.inputs.length === 0 && row.props.length === 0) return;
12581
12823
  this.row = row;
12582
12824
  this.openId = row.id;
12583
12825
  this.snapshot = { ...row.values };
12826
+ this.propSnapshot = { ...row.propValues };
12584
12827
  ensureDialogStyles();
12585
12828
  const t = this.host.theme();
12586
12829
  const border = "var(--vela-border)";
@@ -12588,7 +12831,7 @@ var IndicatorInputsDialog = class {
12588
12831
  const host = this.host.dialogHost() ?? this.host.container;
12589
12832
  const ui = new Dialog({
12590
12833
  host,
12591
- title: row.settingsTitle,
12834
+ title: row.title,
12592
12835
  // Non-modal: live-edit dialog — a modal machine locks pointer events on the
12593
12836
  // whole body, killing the chart and the body-portaled popovers.
12594
12837
  modal: false,
@@ -12600,12 +12843,16 @@ var IndicatorInputsDialog = class {
12600
12843
  closeOnEscape: false,
12601
12844
  footer: (foot) => {
12602
12845
  foot.append(
12846
+ this.resetAction(),
12603
12847
  this.dialogButton("Cancel", false, () => this.revertAndClose()),
12604
12848
  this.dialogButton("Ok", true, () => this.close())
12605
12849
  );
12606
12850
  },
12851
+ // Stale-guard: destroying a dialog fires its machine's onOpenChange(false)
12852
+ // asynchronously — after a reset rebuild that notification must not close
12853
+ // the replacement dialog.
12607
12854
  onOpenChange: (open2) => {
12608
- if (!open2) this.close();
12855
+ if (!open2 && this.uiDialog === ui) this.close();
12609
12856
  }
12610
12857
  });
12611
12858
  applyChromeTokens(ui.panel, t);
@@ -12616,6 +12863,16 @@ var IndicatorInputsDialog = class {
12616
12863
  text: () => "Close"
12617
12864
  }));
12618
12865
  const tabDefs = tabInputs(row.inputs);
12866
+ if (row.props.length > 0) {
12867
+ const propDecls = row.props.map((p) => {
12868
+ const d = { ...p, prop: true };
12869
+ delete d.tab;
12870
+ return d;
12871
+ });
12872
+ const existing = tabDefs.find((t2) => t2.name === PROPS_TAB);
12873
+ if (existing) existing.inputs.push(...propDecls);
12874
+ else tabDefs.push({ name: PROPS_TAB, inputs: propDecls });
12875
+ }
12619
12876
  const tabs = document.createElement("div");
12620
12877
  tabs.style.cssText = `display:flex;gap:12px;padding:0 20px;border-bottom:1px solid ${border};flex:0 0 auto;`;
12621
12878
  const tabEls = [];
@@ -12718,9 +12975,10 @@ var IndicatorInputsDialog = class {
12718
12975
  this.openId = null;
12719
12976
  this.row = null;
12720
12977
  this.snapshot = null;
12978
+ this.propSnapshot = null;
12721
12979
  ui?.destroy();
12722
12980
  }
12723
- /** Restore every input to its open-time value (re-running the indicator), then close. */
12981
+ /** Restore every input and prop to its open-time value (re-running the indicator), then close. */
12724
12982
  revertAndClose() {
12725
12983
  const row = this.row;
12726
12984
  const snap = this.snapshot;
@@ -12734,12 +12992,55 @@ var IndicatorInputsDialog = class {
12734
12992
  }
12735
12993
  }
12736
12994
  }
12995
+ const propSnap = this.propSnapshot;
12996
+ if (row && propSnap) {
12997
+ for (const p of row.props) {
12998
+ const snapped = propSnap[p.key];
12999
+ const before = snapped !== void 0 ? snapped : p.defval;
13000
+ if (row.propValues[p.key] !== before) {
13001
+ row.propValues[p.key] = before;
13002
+ this.host.onChange?.({ indicatorId: row.id, key: p.key, value: before, kind: "prop" });
13003
+ }
13004
+ }
13005
+ }
12737
13006
  this.close();
12738
13007
  }
13008
+ /** The footer's reset button — same chip as Cancel, pinned to the LEFT edge
13009
+ * (`margin-right:auto` against the footer's flex-end keeps Cancel/Ok right). */
13010
+ resetAction() {
13011
+ const b = this.dialogButton("Reset defaults", false, () => this.resetToDefaults());
13012
+ b.style.marginRight = "auto";
13013
+ return b;
13014
+ }
13015
+ /** Restore every input to its declared default (re-running the indicator), then
13016
+ * re-open the form so each control re-reads the restored values — the same
13017
+ * rebuild-after-reset move as the chart-settings dialog. The open-time snapshot
13018
+ * survives the rebuild, so Cancel after a reset still reverts the whole session. */
13019
+ resetToDefaults() {
13020
+ const row = this.row;
13021
+ if (!row) return;
13022
+ const snap = this.snapshot;
13023
+ const propSnap = this.propSnapshot;
13024
+ for (const inp of row.inputs) {
13025
+ if (row.values[inp.key] !== inp.defval) {
13026
+ row.values[inp.key] = inp.defval;
13027
+ this.host.onChange?.({ indicatorId: row.id, key: inp.key, value: inp.defval });
13028
+ }
13029
+ }
13030
+ for (const p of row.props) {
13031
+ if (row.propValues[p.key] !== p.defval) {
13032
+ row.propValues[p.key] = p.defval;
13033
+ this.host.onChange?.({ indicatorId: row.id, key: p.key, value: p.defval, kind: "prop" });
13034
+ }
13035
+ }
13036
+ this.open(row);
13037
+ if (snap) this.snapshot = snap;
13038
+ if (propSnap) this.propSnapshot = propSnap;
13039
+ }
12739
13040
  /** Write one edit through: store it, notify the host, and re-apply the `when` gates. */
12740
- commit(row, key, value) {
12741
- row.values[key] = value;
12742
- this.host.onChange?.({ indicatorId: row.id, key, value });
13041
+ commit(row, decl, value) {
13042
+ bagOf(row, decl)[decl.key] = value;
13043
+ this.host.onChange?.({ indicatorId: row.id, key: decl.key, value, ...decl.prop ? { kind: "prop" } : {} });
12743
13044
  this.refreshVisibility?.();
12744
13045
  }
12745
13046
  /** One settings row (or several `inline=` inputs) placed into a section's grid. */
@@ -12766,7 +13067,7 @@ var IndicatorInputsDialog = class {
12766
13067
  const id = idOf(lead);
12767
13068
  const info = lead.tooltip ? this.infoButton(lead.tooltip) : void 0;
12768
13069
  if (lead.type === "bool") {
12769
- const current = Boolean(row.values[lead.key] ?? lead.defval);
13070
+ const current = Boolean(bagOf(row, lead)[lead.key] ?? lead.defval);
12770
13071
  return append(fieldRow({
12771
13072
  label: nameOf(lead),
12772
13073
  id,
@@ -12775,7 +13076,7 @@ var IndicatorInputsDialog = class {
12775
13076
  toggle: {
12776
13077
  id,
12777
13078
  checked: current,
12778
- onChange: (v) => this.commit(row, lead.key, v)
13079
+ onChange: (v) => this.commit(row, lead, v)
12779
13080
  }
12780
13081
  }));
12781
13082
  }
@@ -12800,8 +13101,8 @@ var IndicatorInputsDialog = class {
12800
13101
  }
12801
13102
  /** Build the typed control for one input, committing edits live via `onChange`. */
12802
13103
  buildControl(row, inp, id) {
12803
- const current = row.values[inp.key] ?? inp.defval;
12804
- const emit = (value) => this.commit(row, inp.key, value);
13104
+ const current = bagOf(row, inp)[inp.key] ?? inp.defval;
13105
+ const emit = (value) => this.commit(row, inp, value);
12805
13106
  if (inp.type === "bool") {
12806
13107
  return buildFieldControl({ kind: "switch", id, checked: Boolean(current), onChange: (v) => emit(v) }).el;
12807
13108
  }
@@ -12812,7 +13113,7 @@ var IndicatorInputsDialog = class {
12812
13113
  kind: "color",
12813
13114
  id,
12814
13115
  theme: this.host.theme(),
12815
- get: () => String(row.values[inp.key] ?? inp.defval),
13116
+ get: () => String(bagOf(row, inp)[inp.key] ?? inp.defval),
12816
13117
  onChange: (v) => emit(v)
12817
13118
  }).el;
12818
13119
  }
@@ -13375,6 +13676,7 @@ function timeParts(ts) {
13375
13676
  }
13376
13677
 
13377
13678
  // src/renderers/shared/InputsUI.ts
13679
+ var LEGEND_AT_TOP_ATTR = "data-vela-pane-at-top";
13378
13680
  var STATUS_KEYFRAMES = "@keyframes vela-ind-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.3;transform:scale(.6)}}";
13379
13681
  var LEGEND_ICON_PX2 = 16;
13380
13682
  var LEGEND_CTL_PX = 18;
@@ -13413,10 +13715,14 @@ var InputsUI = class {
13413
13715
  this.moveApi = null;
13414
13716
  /** Host-contributed legend actions, resolved PER ROW at render time (see setLegendActions). */
13415
13717
  this.legendActions = null;
13718
+ /** Host-contributed callout bubbles, resolved PER ROW at render time (see setLegendCallouts). */
13719
+ this.legendCallouts = null;
13416
13720
  /** Chrome-tooltip disposers, per row id — a tip open at removal must not outlive its row. */
13417
13721
  this.rowTips = /* @__PURE__ */ new Map();
13418
13722
  /** Same, for the contributed extras only (rebuilt independently by setLegendActions). */
13419
13723
  this.extrasTips = /* @__PURE__ */ new Map();
13724
+ /** Same, for the contributed callouts only (rebuilt independently by setLegendCallouts). */
13725
+ this.calloutTips = /* @__PURE__ */ new Map();
13420
13726
  /** Open "Move to" menu (kept so it can be torn down). */
13421
13727
  this.moveMenu = null;
13422
13728
  this.moveTargets = [];
@@ -13574,6 +13880,42 @@ var InputsUI = class {
13574
13880
  extrasEl.appendChild(btn2);
13575
13881
  }
13576
13882
  }
13883
+ /**
13884
+ * Wire the host-contributed callout bubbles. Same contract as
13885
+ * {@link setLegendActions}: re-calling replaces the provider and re-projects the
13886
+ * rows already on screen.
13887
+ */
13888
+ setLegendCallouts(provider) {
13889
+ this.legendCallouts = provider;
13890
+ for (const row of this.rows.values()) this.renderCallouts(row);
13891
+ }
13892
+ /** (Re)build one row's callout bubbles — tinted icon circles beside the title whose
13893
+ * click (when the view carries content) deploys a panel of text and actions. */
13894
+ renderCallouts(row) {
13895
+ this.disposeTips(this.calloutTips, row.id);
13896
+ for (const bubble of row.callouts) bubble.destroy();
13897
+ row.callouts = [];
13898
+ row.calloutsEl.replaceChildren();
13899
+ const views = this.legendCallouts?.(row.id) ?? [];
13900
+ row.calloutsEl.style.display = views.length > 0 ? "inline-flex" : "none";
13901
+ for (const view of views) {
13902
+ const bubble = new CalloutBubble({
13903
+ icon: view.icon,
13904
+ background: view.background,
13905
+ ...view.color !== void 0 ? { color: view.color } : {},
13906
+ label: view.tooltip,
13907
+ ...view.content !== void 0 ? { panel: view.content } : {},
13908
+ // The panel portals into the plot host and self-tokens: it must work on
13909
+ // a bare chart, where no `.vela-ui` token ancestor exists.
13910
+ host: this.container,
13911
+ theme: () => this.theme
13912
+ });
13913
+ bubble.el.dataset.legendCallout = view.id;
13914
+ this.tip(this.calloutTips, row.id, bubble.el, view.tooltip);
13915
+ row.callouts.push(bubble);
13916
+ row.calloutsEl.appendChild(bubble.el);
13917
+ }
13918
+ }
13577
13919
  /** Reposition the per-pane legend containers after a layout change. */
13578
13920
  reposition() {
13579
13921
  for (const [paneId, lg] of this.legends) this.positionLegend(lg, paneId);
@@ -13741,6 +14083,7 @@ var InputsUI = class {
13741
14083
  }
13742
14084
  positionLegend(lg, paneId) {
13743
14085
  const bounds = this.paneBoundsOf ? this.paneBoundsOf(paneId) : { top: 0, height: Infinity };
14086
+ lg.toggleAttribute(LEGEND_AT_TOP_ATTR, bounds.top === 0);
13744
14087
  lg.style.display = bounds.height < 4 || !this.titlesVisible ? "none" : "flex";
13745
14088
  const collapsed = this.paneCollapse.has(paneId);
13746
14089
  const masterId = this.paneCollapse.get(paneId) ?? null;
@@ -13816,13 +14159,13 @@ var InputsUI = class {
13816
14159
  }
13817
14160
  /** Create or update an indicator's legend row (in the legend for its pane). */
13818
14161
  upsert(id, title, inputs, values, paneId = "price", opts = {}) {
13819
- const settingsTitle = opts.settingsTitle ?? title;
13820
14162
  const existing = this.rows.get(id);
13821
14163
  if (existing) {
13822
14164
  existing.title = title;
13823
- existing.settingsTitle = settingsTitle;
13824
14165
  existing.inputs = inputs;
13825
14166
  existing.values = { ...values };
14167
+ existing.props = opts.props ?? [];
14168
+ existing.propValues = { ...opts.propValues ?? {} };
13826
14169
  existing.titleEl.textContent = title;
13827
14170
  if (existing.paneId !== paneId) {
13828
14171
  existing.paneId = paneId;
@@ -13874,6 +14217,9 @@ var InputsUI = class {
13874
14217
  titleWrap.appendChild(beta);
13875
14218
  }
13876
14219
  el.appendChild(titleWrap);
14220
+ const calloutsEl = document.createElement("span");
14221
+ calloutsEl.style.cssText = `display:none;align-items:center;gap:4px;flex:none;margin-left:${LEGEND_TITLE_STATUS_GAP_PX}px;`;
14222
+ el.appendChild(calloutsEl);
13877
14223
  el.appendChild(statusEl);
13878
14224
  const valuesEl = document.createElement("span");
13879
14225
  valuesEl.style.cssText = `display:none;align-items:center;gap:5px;margin-left:${LEGEND_TITLE_VALUES_GAP_PX}px;white-space:nowrap;font-variant-numeric:tabular-nums;`;
@@ -13890,13 +14236,13 @@ var InputsUI = class {
13890
14236
  eye.className = "vela-ind-ctl";
13891
14237
  eye.style.cssText = LEGEND_CTL_CSS;
13892
14238
  eye.addEventListener("click", () => {
13893
- const row = this.rows.get(id);
13894
- this.onToggleVisible?.(id, Boolean(row?.hidden));
14239
+ const row2 = this.rows.get(id);
14240
+ this.onToggleVisible?.(id, Boolean(row2?.hidden));
13895
14241
  });
13896
14242
  controlsEl.appendChild(eye);
13897
14243
  eyeEl = eye;
13898
14244
  }
13899
- if (inputs.length > 0) {
14245
+ if (inputs.length > 0 || (opts.props ?? []).length > 0) {
13900
14246
  const gear = document.createElement("button");
13901
14247
  gear.type = "button";
13902
14248
  gear.setAttribute("aria-label", "Settings");
@@ -13936,7 +14282,9 @@ var InputsUI = class {
13936
14282
  controlsEl.appendChild(close);
13937
14283
  el.appendChild(controlsEl);
13938
14284
  this.attach(this.legendFor(paneId), el, !!opts.native);
13939
- this.rows.set(id, { id, title, settingsTitle, inputs, values: { ...values }, el, titleEl, statusEl, valuesEl, plotValues: [], plotValuesKey: "", showValues: null, highlighted: false, paneId, hidden: false, eyeEl, controlsEl, extrasEl, native: !!opts.native });
14285
+ const row = { id, title, inputs, values: { ...values }, props: opts.props ?? [], propValues: { ...opts.propValues ?? {} }, el, titleEl, statusEl, valuesEl, plotValues: [], plotValuesKey: "", showValues: null, highlighted: false, paneId, hidden: false, eyeEl, controlsEl, extrasEl, calloutsEl, callouts: [], native: !!opts.native };
14286
+ this.rows.set(id, row);
14287
+ this.renderCallouts(row);
13940
14288
  this.syncFoldToggle();
13941
14289
  }
13942
14290
  /** Place a row in its pane's legend — native rows PREPEND (pinned to the top), Pine rows append. */
@@ -13945,9 +14293,11 @@ var InputsUI = class {
13945
14293
  else container.appendChild(el);
13946
14294
  }
13947
14295
  /** Reflect programmatic input changes (so a re-opened dialog shows current values). */
13948
- setValues(id, values) {
14296
+ setValues(id, values, props) {
13949
14297
  const row = this.rows.get(id);
13950
- if (row) row.values = { ...row.values, ...values };
14298
+ if (!row) return;
14299
+ row.values = { ...row.values, ...values };
14300
+ if (props) row.propValues = { ...row.propValues, ...props };
13951
14301
  }
13952
14302
  /**
13953
14303
  * Reflect an indicator's live status in its legend row: `'loading'` shows three pulsing
@@ -14029,8 +14379,13 @@ var InputsUI = class {
14029
14379
  syncRowActions(row) {
14030
14380
  const open2 = row.highlighted;
14031
14381
  row.controlsEl.style.display = open2 || row.hidden ? "inline-flex" : "none";
14032
- if (open2) row.el.appendChild(row.statusEl);
14033
- else row.el.insertBefore(row.statusEl, row.valuesEl);
14382
+ if (open2) {
14383
+ row.el.appendChild(row.statusEl);
14384
+ row.el.appendChild(row.calloutsEl);
14385
+ } else {
14386
+ row.el.insertBefore(row.statusEl, row.valuesEl);
14387
+ row.el.insertBefore(row.calloutsEl, row.statusEl);
14388
+ }
14034
14389
  for (const child of Array.from(row.controlsEl.children)) {
14035
14390
  if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
14036
14391
  if (child === row.extrasEl) {
@@ -14048,10 +14403,12 @@ var InputsUI = class {
14048
14403
  }
14049
14404
  remove(id) {
14050
14405
  const row = this.rows.get(id);
14406
+ for (const bubble of row?.callouts ?? []) bubble.destroy();
14051
14407
  row?.el.remove();
14052
14408
  this.rows.delete(id);
14053
14409
  this.disposeTips(this.rowTips, id);
14054
14410
  this.disposeTips(this.extrasTips, id);
14411
+ this.disposeTips(this.calloutTips, id);
14055
14412
  if (this.selectedId === id) this.selectedId = null;
14056
14413
  if (this.inputsDialog.openId === id) this.inputsDialog.close();
14057
14414
  this.syncFoldToggle();
@@ -14071,6 +14428,8 @@ var InputsUI = class {
14071
14428
  this.rowMenu = null;
14072
14429
  for (const id of [...this.rowTips.keys()]) this.disposeTips(this.rowTips, id);
14073
14430
  for (const id of [...this.extrasTips.keys()]) this.disposeTips(this.extrasTips, id);
14431
+ for (const id of [...this.calloutTips.keys()]) this.disposeTips(this.calloutTips, id);
14432
+ for (const row of this.rows.values()) for (const bubble of row.callouts) bubble.destroy();
14074
14433
  for (const lg of this.legends.values()) lg.remove();
14075
14434
  this.legends.clear();
14076
14435
  this.rows.clear();
@@ -14425,6 +14784,32 @@ var SIZE_PX3 = {
14425
14784
  large: 16,
14426
14785
  huge: 20
14427
14786
  };
14787
+ function fontPxOf(size) {
14788
+ if (typeof size === "number") return size > 0 ? size : SIZE_PX3.auto;
14789
+ return SIZE_PX3[size] ?? SIZE_PX3.auto;
14790
+ }
14791
+ function tableHasContent(t) {
14792
+ return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
14793
+ }
14794
+ function mergeRenderPlan(t) {
14795
+ const span = /* @__PURE__ */ new Map();
14796
+ const omit = /* @__PURE__ */ new Set();
14797
+ for (const m of t.merges) {
14798
+ span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
14799
+ for (let r = m.startRow; r <= m.endRow; r += 1) {
14800
+ for (let c = m.startCol; c <= m.endCol; c += 1) {
14801
+ if (r !== m.startRow || c !== m.startCol) omit.add(`${r}:${c}`);
14802
+ }
14803
+ }
14804
+ }
14805
+ for (let r = 0; r < t.rows; r += 1) {
14806
+ for (let c = 0; c < t.columns; c += 1) {
14807
+ if (t.cells[r]?.[c]?.merged && !span.has(`${r}:${c}`)) omit.add(`${r}:${c}`);
14808
+ }
14809
+ }
14810
+ for (const key of span.keys()) omit.delete(key);
14811
+ return { span, omit };
14812
+ }
14428
14813
  var TableOverlay = class {
14429
14814
  constructor(container, theme, paneBounds) {
14430
14815
  this.container = container;
@@ -14445,7 +14830,9 @@ var TableOverlay = class {
14445
14830
  update(tables) {
14446
14831
  this.lastTables = tables;
14447
14832
  this.root.replaceChildren();
14448
- for (const t of tables) this.root.appendChild(this.renderTable(t));
14833
+ for (const t of tables) {
14834
+ if (tableHasContent(t)) this.root.appendChild(this.renderTable(t));
14835
+ }
14449
14836
  }
14450
14837
  /** Re-render at the current pane geometry — after layout settles or on resize. */
14451
14838
  reposition() {
@@ -14460,39 +14847,36 @@ var TableOverlay = class {
14460
14847
  this.root.remove();
14461
14848
  }
14462
14849
  renderTable(t) {
14850
+ const b = this.paneBounds(t.paneId);
14463
14851
  const wrap = document.createElement("div");
14464
14852
  wrap.style.position = "absolute";
14465
- this.anchor(wrap, t.position, this.paneBounds(t.paneId));
14853
+ if (t.frameColor && t.frameWidth > 0) wrap.style.border = `${t.frameWidth}px solid ${t.frameColor}`;
14854
+ this.anchor(wrap, t.position, b);
14466
14855
  const table = document.createElement("table");
14467
14856
  Object.assign(table.style, {
14468
14857
  borderCollapse: "collapse",
14469
14858
  background: t.bgColor ?? "transparent",
14470
14859
  fontFamily: this.theme.fontFamily || "sans-serif",
14471
- // Frame as an inset shadow (not a `border`) so it stays independent of the
14472
- // collapsed cell borders even when frame_width != border_width.
14473
- boxShadow: t.frameColor && t.frameWidth > 0 ? `inset 0 0 0 ${t.frameWidth}px ${t.frameColor}` : "none",
14474
14860
  border: "none",
14475
14861
  tableLayout: "auto",
14476
14862
  // Re-enable pointer events on the table only (root is none) so cell tooltips work.
14477
14863
  pointerEvents: "auto"
14478
14864
  });
14479
- const span = /* @__PURE__ */ new Map();
14480
- const skip = /* @__PURE__ */ new Set();
14481
- for (const m of t.merges) {
14482
- span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
14483
- for (let r = m.startRow; r <= m.endRow; r += 1) {
14484
- for (let c = m.startCol; c <= m.endCol; c += 1) {
14485
- if (r !== m.startRow || c !== m.startCol) skip.add(`${r}:${c}`);
14486
- }
14487
- }
14488
- }
14865
+ const { span, omit } = mergeRenderPlan(t);
14866
+ const plotW = Math.max(0, (this.root.clientWidth || this.container.clientWidth) - b.rightAxis);
14867
+ const paneH = b.height;
14489
14868
  const cellBorder = t.borderColor && t.borderWidth > 0 ? `${t.borderWidth}px solid ${t.borderColor}` : "none";
14490
14869
  for (let r = 0; r < t.rows; r += 1) {
14491
14870
  const tr = document.createElement("tr");
14492
14871
  for (let c = 0; c < t.columns; c += 1) {
14872
+ if (omit.has(`${r}:${c}`)) continue;
14493
14873
  const cell = t.cells[r]?.[c] ?? null;
14494
- if (skip.has(`${r}:${c}`) || cell?.merged) continue;
14495
14874
  const td = document.createElement("td");
14875
+ if (cell === null) {
14876
+ Object.assign(td.style, { padding: "0", border: "none" });
14877
+ tr.appendChild(td);
14878
+ continue;
14879
+ }
14496
14880
  const sp = span.get(`${r}:${c}`);
14497
14881
  if (sp) {
14498
14882
  if (sp.cs > 1) td.colSpan = sp.cs;
@@ -14501,18 +14885,22 @@ var TableOverlay = class {
14501
14885
  Object.assign(td.style, {
14502
14886
  border: cellBorder,
14503
14887
  padding: "2px 6px",
14504
- background: cell?.bgColor ?? "transparent",
14505
- color: cell?.textColor ?? this.theme.textColor,
14506
- textAlign: cell?.hAlign ?? "center",
14507
- verticalAlign: cell?.vAlign === "top" ? "top" : cell?.vAlign === "bottom" ? "bottom" : "middle",
14508
- fontSize: `${SIZE_PX3[cell?.textSize ?? "normal"]}px`,
14509
- fontFamily: cell?.fontFamily === "monospace" ? "monospace" : "inherit",
14510
- fontWeight: cell?.bold ? "bold" : "normal",
14511
- fontStyle: cell?.italic ? "italic" : "normal",
14512
- whiteSpace: "pre-line"
14888
+ background: cell.bgColor ?? "transparent",
14889
+ color: cell.textColor ?? this.theme.textColor,
14890
+ textAlign: cell.hAlign,
14891
+ verticalAlign: cell.vAlign === "top" ? "top" : cell.vAlign === "bottom" ? "bottom" : "middle",
14892
+ fontSize: `${fontPxOf(cell.textSize)}px`,
14893
+ fontFamily: cell.fontFamily === "monospace" ? "monospace" : "inherit",
14894
+ fontWeight: cell.bold ? "bold" : "normal",
14895
+ fontStyle: cell.italic ? "italic" : "normal",
14896
+ // Pine cell text never wraps; `\n` still breaks lines. Wrapping
14897
+ // used to collapse unicode sparklines and ━━━ dividers.
14898
+ whiteSpace: "pre"
14513
14899
  });
14514
- if (cell?.tooltip) td.title = cell.tooltip;
14515
- td.textContent = cell?.text ?? "";
14900
+ if (cell.width) td.style.width = `${cell.width / 100 * plotW}px`;
14901
+ if (cell.height) td.style.height = `${cell.height / 100 * paneH}px`;
14902
+ if (cell.tooltip) td.title = cell.tooltip;
14903
+ td.textContent = cell.text ?? "";
14516
14904
  tr.appendChild(td);
14517
14905
  }
14518
14906
  table.appendChild(tr);
@@ -15397,8 +15785,14 @@ var WebGL2Backend = class {
15397
15785
  return sc === pane.scale ? pane : { ...pane, scale: sc };
15398
15786
  };
15399
15787
  b.alpha = this.modelAlpha;
15400
- for (const m of models) for (const bgSpan of m.backgrounds) this.emitBackground(b, bgSpan, pane, coords);
15401
- for (const m of models) for (const f of m.fills) this.emitFill(b, m, f, effPane(m), coords, i0, i1, scene.offsetOf(m.id));
15788
+ for (const m of models) for (const bgSpan of m.backgrounds) if (bgSpan.overlay !== true) this.emitBackground(b, bgSpan, pane, coords);
15789
+ for (const m of models) for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, effPane(m), coords, i0, i1, scene.offsetOf(m.id));
15790
+ if (isPrice) {
15791
+ for (const m of scene.indicators.values()) {
15792
+ for (const bgSpan of m.backgrounds) if (bgSpan.overlay === true) this.emitBackground(b, bgSpan, pane, coords);
15793
+ for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, i1, scene.offsetOf(m.id));
15794
+ }
15795
+ }
15402
15796
  const drawCandles = isPrice && !scene.candlesHidden;
15403
15797
  let candleDrawn = false;
15404
15798
  for (const m of models) {
@@ -15412,7 +15806,7 @@ var WebGL2Backend = class {
15412
15806
  b.alpha = this.modelAlpha;
15413
15807
  const off = scene.offsetOf(m.id);
15414
15808
  const mp = effPane(m);
15415
- for (const s of m.series) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
15809
+ for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
15416
15810
  }
15417
15811
  if (drawCandles && !candleDrawn) {
15418
15812
  drawSlicesUpTo(scene.candleZ);
@@ -15421,6 +15815,12 @@ var WebGL2Backend = class {
15421
15815
  }
15422
15816
  drawSlicesUpTo(Infinity);
15423
15817
  b.alpha = this.modelAlpha;
15818
+ if (isPrice) {
15819
+ for (const m of scene.indicators.values()) {
15820
+ const off = scene.offsetOf(m.id);
15821
+ for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0, i1, theme, off);
15822
+ }
15823
+ }
15424
15824
  for (const m of models) {
15425
15825
  const mp = effPane(m);
15426
15826
  for (const pl of m.priceLines) this.emitHline(b, pl, mp, coords, dataW, theme);
@@ -15467,7 +15867,7 @@ var WebGL2Backend = class {
15467
15867
  for (const pane of scene.orderedPanes()) {
15468
15868
  const b = this.glowBatch;
15469
15869
  b.reset();
15470
- this.emitGlowSources(b, scene.indicatorsForPane(pane.id), pane, coords, i0, i1, (id) => scene.offsetOf(id));
15870
+ this.emitGlowSources(b, scene, pane, coords, i0, i1);
15471
15871
  if (b.vertexCount === 0) continue;
15472
15872
  const topH = Math.round(pane.bounds.top * dpr * 0.5);
15473
15873
  const botH = Math.round((pane.bounds.top + pane.bounds.height) * dpr * 0.5);
@@ -15536,15 +15936,23 @@ var WebGL2Backend = class {
15536
15936
  gl.bindVertexArray(null);
15537
15937
  return true;
15538
15938
  }
15539
- /** The "neon" elements that glow: line/area/step lines + point markers (not candles/fills/bars). */
15540
- emitGlowSources(b, models, pane, coords, i0, i1, offsetOf = () => 0) {
15541
- for (const m of models) {
15542
- const off = offsetOf(m.id);
15543
- for (const s of m.series) {
15544
- if (!isLineLikeSeries(s) || s.visible === false) continue;
15545
- if (s.kind === "histogram" || s.kind === "columns") continue;
15546
- if (s.kind === "circles" || s.kind === "cross") this.emitPointMarkers(b, s, pane, coords, i0, i1, off);
15547
- else this.emitPolyline(b, s, pane, coords, i0, i1, s.kind === "step", off);
15939
+ /** The "neon" elements that glow: line/area/step lines + point markers (not candles/fills/bars).
15940
+ * Routing mirrors the main pass: own series per pane, force_overlay series on the price pane. */
15941
+ emitGlowSources(b, scene, pane, coords, i0, i1) {
15942
+ const emitOne = (s, off) => {
15943
+ if (!isLineLikeSeries(s) || s.visible === false) return;
15944
+ if (s.kind === "histogram" || s.kind === "columns") return;
15945
+ if (s.kind === "circles" || s.kind === "cross") this.emitPointMarkers(b, s, pane, coords, i0, i1, off);
15946
+ else this.emitPolyline(b, s, pane, coords, i0, i1, s.kind === "step", off);
15947
+ };
15948
+ for (const m of scene.indicatorsForPane(pane.id)) {
15949
+ const off = scene.offsetOf(m.id);
15950
+ for (const s of m.series) if (s.overlay !== true) emitOne(s, off);
15951
+ }
15952
+ if (pane.kind === "price") {
15953
+ for (const m of scene.indicators.values()) {
15954
+ const off = scene.offsetOf(m.id);
15955
+ for (const s of m.series) if (s.overlay === true) emitOne(s, off);
15548
15956
  }
15549
15957
  }
15550
15958
  }
@@ -15827,9 +16235,9 @@ var WebGL2Backend = class {
15827
16235
  b.alpha = this.candleBodyAlpha;
15828
16236
  b.rect(g.bodyX, bodyTop, g.bodyW, bodyH, c);
15829
16237
  }
15830
- if (cs.borderVisible || bc || fading && cs.bodyVisible) {
16238
+ if (cs.borderVisible || fading && cs.bodyVisible) {
15831
16239
  b.alpha = this.candleStructureAlpha;
15832
- const bord = cs.borderVisible || bc ? parseColor((isUp ? cs.borderUpColor : cs.borderDownColor) ?? dir) : c;
16240
+ const bord = cs.borderVisible ? parseColor((isUp ? cs.borderUpColor : cs.borderDownColor) ?? bodyColorStr) : c;
15833
16241
  const bw = Math.max(0, g.bodyW - 1);
15834
16242
  const bh = Math.max(0, bodyH - 1);
15835
16243
  b.rectStroke(g.bodyX + 0.5, bodyTop + 0.5, bw, bh, 1, bord);
@@ -17381,8 +17789,14 @@ var Canvas2dBackend = class {
17381
17789
  return sc === pane.scale ? pane : { ...pane, scale: sc };
17382
17790
  };
17383
17791
  ctx.globalAlpha = this.modelAlpha;
17384
- for (const m of models) for (const bg of m.backgrounds) this.drawBackground(ctx, bg, pane, coords);
17385
- for (const m of models) for (const f of m.fills) this.drawFill(ctx, m, f, effPane(m), coords, i0, i1, scene.offsetOf(m.id));
17792
+ for (const m of models) for (const bg of m.backgrounds) if (bg.overlay !== true) this.drawBackground(ctx, bg, pane, coords);
17793
+ for (const m of models) for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, effPane(m), coords, i0, i1, scene.offsetOf(m.id));
17794
+ if (isPrice) {
17795
+ for (const m of scene.indicators.values()) {
17796
+ for (const bg of m.backgrounds) if (bg.overlay === true) this.drawBackground(ctx, bg, pane, coords);
17797
+ for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, scene.offsetOf(m.id));
17798
+ }
17799
+ }
17386
17800
  const slices = scene.drawingSlices.get(pane.id) ?? [];
17387
17801
  let si = 0;
17388
17802
  const drawSlicesUpTo = (z) => {
@@ -17404,7 +17818,7 @@ var Canvas2dBackend = class {
17404
17818
  ctx.globalAlpha = this.modelAlpha;
17405
17819
  const off = scene.offsetOf(m.id);
17406
17820
  const mp = effPane(m);
17407
- for (const s of m.series) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
17821
+ for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
17408
17822
  }
17409
17823
  if (drawCandles && !candleDrawn) {
17410
17824
  drawSlicesUpTo(scene.candleZ);
@@ -17412,6 +17826,13 @@ var Canvas2dBackend = class {
17412
17826
  this.drawPriceSeries(ctx, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
17413
17827
  }
17414
17828
  drawSlicesUpTo(Infinity);
17829
+ if (isPrice) {
17830
+ ctx.globalAlpha = this.modelAlpha;
17831
+ for (const m of scene.indicators.values()) {
17832
+ const off = scene.offsetOf(m.id);
17833
+ for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0, i1, theme, off);
17834
+ }
17835
+ }
17415
17836
  ctx.globalAlpha = this.modelAlpha;
17416
17837
  for (const m of models) {
17417
17838
  const mp = effPane(m);
@@ -17683,9 +18104,9 @@ var Canvas2dBackend = class {
17683
18104
  ctx.fillStyle = color;
17684
18105
  ctx.fillRect(g.bodyX, top, g.bodyW, bodyH);
17685
18106
  }
17686
- if (cs.borderVisible || bc || fading && cs.bodyVisible) {
18107
+ if (cs.borderVisible || fading && cs.bodyVisible) {
17687
18108
  ctx.globalAlpha = this.candleStructureAlpha;
17688
- ctx.strokeStyle = cs.borderVisible || bc ? (up ? cs.borderUpColor : cs.borderDownColor) ?? dir : color;
18109
+ ctx.strokeStyle = cs.borderVisible ? (up ? cs.borderUpColor : cs.borderDownColor) ?? color : color;
17689
18110
  ctx.lineWidth = 1;
17690
18111
  const bw = Math.max(0, g.bodyW - 1);
17691
18112
  const bh = Math.max(0, bodyH - 1);
@@ -18117,6 +18538,8 @@ var DrawingSceneRenderer = class {
18117
18538
  this.set = set;
18118
18539
  /** measureText width cache, keyed by `${font} ${text}`, persists across frames. */
18119
18540
  this.widthCache = /* @__PURE__ */ new Map();
18541
+ /** Tooltip hit-rects captured while drawing the CURRENT set (rebuilt per render). */
18542
+ this.tipRegions = [];
18120
18543
  /**
18121
18544
  * Index offset of the CURRENT set's model: its `xloc:'bar_index'` coordinates count
18122
18545
  * from the model's anchor bar, so they shift by this to land on chart logical indices.
@@ -18137,8 +18560,13 @@ var DrawingSceneRenderer = class {
18137
18560
  const s = this.set;
18138
18561
  return !s.lines.length && !s.boxes.length && !s.labels.length && !s.polylines.length && !s.linefills.length;
18139
18562
  }
18563
+ /** Tooltip hit-rects of the labels drawn by the LAST `render` call (same coords as `ctx`). */
18564
+ labelTipRegions() {
18565
+ return this.tipRegions;
18566
+ }
18140
18567
  /** Draw the whole set into `ctx` using the supplied coordinate closures. */
18141
18568
  render(ctx, W, H, xOf, yOf) {
18569
+ this.tipRegions = [];
18142
18570
  if (this.isEmpty()) return;
18143
18571
  ctx.save();
18144
18572
  this.drawLinefills(ctx, W, H, xOf, yOf);
@@ -18486,13 +18914,35 @@ var DrawingSceneRenderer = class {
18486
18914
  if (this.isPointShape(lb.style)) {
18487
18915
  if (!lb.noFill) this.drawLabelShape(ctx, lb.style, px, py, fontPx, color);
18488
18916
  if (lb.text) this.drawLabelText(ctx, lb, px, py + fontPx, fontPx);
18489
- } else if (lb.style === "none" || lb.style === "text_outline" || lb.noFill) {
18490
- if (lb.text) this.drawLabelText(ctx, lb, px, py, fontPx, lb.style === "text_outline");
18917
+ if (lb.tooltip) {
18918
+ const r = Math.max(4, fontPx * 0.6) + 3;
18919
+ this.tipRegions.push({ left: px - r, top: py - r, right: px + r, bottom: py + r, text: lb.tooltip });
18920
+ }
18921
+ } else if (lb.style === "none" || lb.style === "text_outline") {
18922
+ if (lb.text) {
18923
+ this.drawLabelText(ctx, lb, px, py, fontPx, lb.style === "text_outline");
18924
+ if (lb.tooltip) this.tipRegions.push(this.textRegion(ctx, lb, px, py, fontPx, lb.tooltip));
18925
+ }
18491
18926
  } else {
18492
- this.drawBubble(ctx, lb, px, py, fontPx, color);
18927
+ const r = this.drawBubble(ctx, lb, px, py, fontPx, color);
18928
+ if (lb.tooltip) this.tipRegions.push({ left: r.x, top: r.y, right: r.x + r.w, bottom: r.y + r.h, text: lb.tooltip });
18493
18929
  }
18494
18930
  }
18495
18931
  }
18932
+ /** Canvas font for a label's text — family + Pine `text_formatting` (bold/italic). */
18933
+ labelFont(lb, fontPx) {
18934
+ const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
18935
+ return `${lb.italic ? "italic " : ""}${lb.bold ? "bold " : ""}${fontPx}px ${family}`;
18936
+ }
18937
+ /** Hover rect of a text-only label (style none/text_outline/noFill), centered like drawLabelText. */
18938
+ textRegion(ctx, lb, cx, cy, fontPx, text) {
18939
+ const font = this.labelFont(lb, fontPx);
18940
+ const lines = (lb.text ?? "").split("\n");
18941
+ const w = Math.max(1, ...lines.map((l) => this.measure(ctx, font, l)));
18942
+ const h = fontPx * 1.25 * lines.length;
18943
+ const left = lb.textAlign === "left" ? cx : lb.textAlign === "right" ? cx - w : cx - w / 2;
18944
+ return { left, top: cy - h / 2, right: left + w, bottom: cy + h / 2, text };
18945
+ }
18496
18946
  isPointShape(style) {
18497
18947
  switch (style) {
18498
18948
  case "circle":
@@ -18511,8 +18961,7 @@ var DrawingSceneRenderer = class {
18511
18961
  }
18512
18962
  }
18513
18963
  drawLabelText(ctx, lb, cx, cy, fontPx, outline = false) {
18514
- const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
18515
- ctx.font = `${fontPx}px ${family}`;
18964
+ ctx.font = this.labelFont(lb, fontPx);
18516
18965
  ctx.textAlign = lb.textAlign === "left" ? "left" : lb.textAlign === "right" ? "right" : "center";
18517
18966
  ctx.textBaseline = "middle";
18518
18967
  const lines = lb.text.split("\n");
@@ -18592,9 +19041,9 @@ var DrawingSceneRenderer = class {
18592
19041
  ctx.fill();
18593
19042
  }
18594
19043
  }
19044
+ /** Draw a bubble label; returns the bubble body rect (for tooltip hit-testing). */
18595
19045
  drawBubble(ctx, lb, px, py, fontPx, color) {
18596
- const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
18597
- ctx.font = `${fontPx}px ${family}`;
19046
+ ctx.font = this.labelFont(lb, fontPx);
18598
19047
  const lines = (lb.text ?? "").split("\n");
18599
19048
  const padX = 6;
18600
19049
  const padY = 4;
@@ -18652,38 +19101,51 @@ var DrawingSceneRenderer = class {
18652
19101
  by = py - h - ptr;
18653
19102
  pointer = "down";
18654
19103
  }
18655
- ctx.fillStyle = color;
18656
- this.roundRect(ctx, bx, by, w, h, 4);
18657
- ctx.fill();
18658
- if (pointer !== "none") {
18659
- ctx.beginPath();
18660
- if (pointer === "down") {
18661
- ctx.moveTo(px - ptr, by + h);
18662
- ctx.lineTo(px + ptr, by + h);
18663
- ctx.lineTo(px, py);
18664
- } else if (pointer === "up") {
18665
- ctx.moveTo(px - ptr, by);
18666
- ctx.lineTo(px + ptr, by);
18667
- ctx.lineTo(px, py);
18668
- } else if (pointer === "left") {
18669
- ctx.moveTo(bx, py - ptr);
18670
- ctx.lineTo(bx, py + ptr);
18671
- ctx.lineTo(px, py);
18672
- } else {
18673
- ctx.moveTo(bx + w, py - ptr);
18674
- ctx.lineTo(bx + w, py + ptr);
18675
- ctx.lineTo(px, py);
18676
- }
18677
- ctx.closePath();
19104
+ if (!lb.noFill) {
19105
+ ctx.fillStyle = color;
19106
+ this.roundRect(ctx, bx, by, w, h, 4);
18678
19107
  ctx.fill();
19108
+ if (pointer !== "none") {
19109
+ ctx.beginPath();
19110
+ if (pointer === "down") {
19111
+ ctx.moveTo(px - ptr, by + h);
19112
+ ctx.lineTo(px + ptr, by + h);
19113
+ ctx.lineTo(px, py);
19114
+ } else if (pointer === "up") {
19115
+ ctx.moveTo(px - ptr, by);
19116
+ ctx.lineTo(px + ptr, by);
19117
+ ctx.lineTo(px, py);
19118
+ } else if (pointer === "left") {
19119
+ ctx.moveTo(bx, py - ptr);
19120
+ ctx.lineTo(bx, py + ptr);
19121
+ ctx.lineTo(px, py);
19122
+ } else {
19123
+ ctx.moveTo(bx + w, py - ptr);
19124
+ ctx.lineTo(bx + w, py + ptr);
19125
+ ctx.lineTo(px, py);
19126
+ }
19127
+ ctx.closePath();
19128
+ ctx.fill();
19129
+ }
18679
19130
  }
18680
19131
  if (lb.text) {
18681
- ctx.fillStyle = lb.textColor ?? contrastColor(color);
18682
- ctx.textAlign = "center";
19132
+ ctx.fillStyle = lb.textColor ?? (lb.noFill ? this.deps.theme.textColor : contrastColor(color));
18683
19133
  ctx.textBaseline = "middle";
19134
+ let tx;
19135
+ if (lb.textAlign === "left") {
19136
+ ctx.textAlign = "left";
19137
+ tx = bx + padX;
19138
+ } else if (lb.textAlign === "right") {
19139
+ ctx.textAlign = "right";
19140
+ tx = bx + w - padX;
19141
+ } else {
19142
+ ctx.textAlign = "center";
19143
+ tx = bx + w / 2;
19144
+ }
18684
19145
  const startY = by + padY + lineH / 2;
18685
- for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], bx + w / 2, startY + i * lineH);
19146
+ for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], tx, startY + i * lineH);
18686
19147
  }
19148
+ return { x: bx, y: by, w, h };
18687
19149
  }
18688
19150
  roundRect(ctx, x, y, w, h, r) {
18689
19151
  const rr = Math.min(r, w / 2, h / 2);
@@ -18921,6 +19383,8 @@ var ChromeRenderer = class {
18921
19383
  this.axisTextColor = DARK_THEME.textColor;
18922
19384
  // Shared Pine-drawing renderer (line/box/label/polyline/linefill); widthCache persists.
18923
19385
  this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
19386
+ // Tooltip hit-rects of every label drawn this frame, in plot coords (rebuilt per render).
19387
+ this.labelTips = [];
18924
19388
  }
18925
19389
  mount(canvas) {
18926
19390
  this.canvas = canvas;
@@ -18962,6 +19426,7 @@ var ChromeRenderer = class {
18962
19426
  const dataW = coords.width;
18963
19427
  const dataH = coords.height;
18964
19428
  this.axisTextColor = surface?.textColor ?? theme.textColor;
19429
+ this.labelTips = [];
18965
19430
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
18966
19431
  ctx.clearRect(0, 0, fullW, fullH);
18967
19432
  if (surface && (fullW > dataW || fullH > dataH)) {
@@ -19071,6 +19536,17 @@ var ChromeRenderer = class {
19071
19536
  (price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
19072
19537
  );
19073
19538
  ctx.restore();
19539
+ for (const r of this.drawScene.labelTipRegions()) {
19540
+ this.labelTips.push({ ...r, top: r.top + pane.bounds.top, bottom: r.bottom + pane.bounds.top });
19541
+ }
19542
+ }
19543
+ /** Tooltip of the topmost label under a plot-space point, or null. Fed by the last render. */
19544
+ labelTooltipAt(x, y) {
19545
+ for (let i = this.labelTips.length - 1; i >= 0; i -= 1) {
19546
+ const r = this.labelTips[i];
19547
+ if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return r.text;
19548
+ }
19549
+ return null;
19074
19550
  }
19075
19551
  // ── axes ──
19076
19552
  drawPriceAxes(ctx, scene, coords, theme, dataW, panes) {
@@ -19287,6 +19763,68 @@ function formatCountdown(ms) {
19287
19763
  return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
19288
19764
  }
19289
19765
 
19766
+ // src/renderers/native/chrome/LabelTooltip.ts
19767
+ var HOVER_DELAY_MS = 350;
19768
+ var LabelTooltip = class {
19769
+ constructor(plot, deps) {
19770
+ this.plot = plot;
19771
+ this.deps = deps;
19772
+ this.tip = null;
19773
+ this.timer = null;
19774
+ /** Text of the currently open OR armed tip — dedupes moves inside one label. */
19775
+ this.current = null;
19776
+ this.onMove = (e) => {
19777
+ if (e.pointerType !== "mouse") return;
19778
+ const rect = this.plot.getBoundingClientRect();
19779
+ const x = e.clientX - rect.left;
19780
+ const y = e.clientY - rect.top;
19781
+ const text = this.deps.lookup(x, y);
19782
+ if (!text) {
19783
+ this.clear();
19784
+ return;
19785
+ }
19786
+ if (text === this.current) return;
19787
+ this.clear();
19788
+ this.current = text;
19789
+ this.timer = window.setTimeout(() => this.show(text, x, y), HOVER_DELAY_MS);
19790
+ };
19791
+ this.onLeave = () => {
19792
+ this.clear();
19793
+ };
19794
+ plot.addEventListener("pointermove", this.onMove);
19795
+ plot.addEventListener("pointerleave", this.onLeave);
19796
+ plot.addEventListener("pointerdown", this.onLeave);
19797
+ }
19798
+ destroy() {
19799
+ this.plot.removeEventListener("pointermove", this.onMove);
19800
+ this.plot.removeEventListener("pointerleave", this.onLeave);
19801
+ this.plot.removeEventListener("pointerdown", this.onLeave);
19802
+ this.clear();
19803
+ }
19804
+ show(text, x, y) {
19805
+ const doc = this.plot.ownerDocument;
19806
+ const tip = doc.createElement("div");
19807
+ tip.textContent = text;
19808
+ tip.style.cssText = "position:absolute;z-index:var(--vela-z-tooltip);pointer-events:none;background:var(--vela-bg);border:1px solid var(--vela-border);color:var(--vela-fg);border-radius:var(--vela-radius-md);padding:4px 9px;box-shadow:var(--vela-shadow);font:var(--vela-font-size-md) var(--vela-font);max-width:260px;";
19809
+ applyChromeTokens(tip, this.deps.theme());
19810
+ this.plot.appendChild(tip);
19811
+ const pw = this.plot.clientWidth;
19812
+ const ph = this.plot.clientHeight;
19813
+ tip.style.left = `${Math.max(0, Math.min(x + 12, pw - tip.offsetWidth - 4))}px`;
19814
+ tip.style.top = `${Math.max(0, Math.min(y + 16, ph - tip.offsetHeight - 4))}px`;
19815
+ this.tip = tip;
19816
+ }
19817
+ clear() {
19818
+ if (this.timer !== null) {
19819
+ clearTimeout(this.timer);
19820
+ this.timer = null;
19821
+ }
19822
+ this.tip?.remove();
19823
+ this.tip = null;
19824
+ this.current = null;
19825
+ }
19826
+ };
19827
+
19290
19828
  // src/renderers/native/chrome/CrosshairRenderer.ts
19291
19829
  var CrosshairRenderer = class {
19292
19830
  constructor() {
@@ -25078,19 +25616,8 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
25078
25616
  for (const model of models) {
25079
25617
  const off = offsetOf(model.id);
25080
25618
  for (const s of model.series) {
25081
- if (s.kind === "candle" || s.kind === "bar") {
25082
- for (let i = i0; i <= i1; i += 1) {
25083
- const b = s.bars[i - off];
25084
- if (b) {
25085
- consider(b.high);
25086
- consider(b.low);
25087
- }
25088
- }
25089
- } else if (isLineLikeSeries(s)) {
25090
- for (let i = i0; i <= i1; i += 1) consider(s.points[i - off]?.value);
25091
- if (s.kind === "histogram" || s.kind === "columns") consider(s.style?.base ?? 0);
25092
- else if (s.style?.base != null) consider(s.style.base);
25093
- }
25619
+ if (s.overlay === true) continue;
25620
+ considerSeries(s, i0, i1, off, consider);
25094
25621
  }
25095
25622
  for (const pl of model.priceLines) consider(pl.price);
25096
25623
  }
@@ -25112,6 +25639,36 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
25112
25639
  const span = max - min;
25113
25640
  return { min: min - span * MARGIN_BOTTOM, max: max + span * MARGIN_TOP };
25114
25641
  }
25642
+ function considerSeries(s, i0, i1, off, consider) {
25643
+ if (s.kind === "candle" || s.kind === "bar") {
25644
+ for (let i = i0; i <= i1; i += 1) {
25645
+ const b = s.bars[i - off];
25646
+ if (b) {
25647
+ consider(b.high);
25648
+ consider(b.low);
25649
+ }
25650
+ }
25651
+ } else if (isLineLikeSeries(s)) {
25652
+ for (let i = i0; i <= i1; i += 1) consider(s.points[i - off]?.value);
25653
+ if (s.kind === "histogram" || s.kind === "columns") consider(s.style?.base ?? 0);
25654
+ else if (s.style?.base != null) consider(s.style.base);
25655
+ }
25656
+ }
25657
+ function overlaySeriesRange(models, i0, i1, offsetOf = () => 0) {
25658
+ let min = Infinity;
25659
+ let max = -Infinity;
25660
+ const consider = (v) => {
25661
+ if (v != null && Number.isFinite(v)) {
25662
+ if (v < min) min = v;
25663
+ if (v > max) max = v;
25664
+ }
25665
+ };
25666
+ for (const model of models) {
25667
+ const off = offsetOf(model.id);
25668
+ for (const s of model.series) if (s.overlay === true) considerSeries(s, i0, i1, off, consider);
25669
+ }
25670
+ return min === Infinity ? null : { min, max };
25671
+ }
25115
25672
  function expandScaleByPixels(scale, heightPx, abovePx, belowPx) {
25116
25673
  if (abovePx <= 0 && belowPx <= 0) return scale;
25117
25674
  const content = heightPx - abovePx - belowPx;
@@ -25166,6 +25723,11 @@ function resizeSplit(split, dyTotal, minPx = MIN_PANE_PX) {
25166
25723
  }
25167
25724
 
25168
25725
  // src/renderers/native/backdrop/BackdropRenderer.ts
25726
+ function clipHighlightRect(x1, x2, left, right) {
25727
+ const x = Math.max(left, x1);
25728
+ const end = Math.min(right, x2);
25729
+ return end > x ? { x, width: end - x } : null;
25730
+ }
25169
25731
  var BackdropRenderer = class {
25170
25732
  constructor() {
25171
25733
  this.canvas = null;
@@ -25198,17 +25760,22 @@ var BackdropRenderer = class {
25198
25760
  /** Renderer-owned session highlight bands: full-height (all panes), behind the grid.
25199
25761
  * Session-zone washes (pre/post-market) paint first, host highlights on top. */
25200
25762
  drawHighlights(ctx, scene, coords) {
25201
- const bands = [...scene.sessionHighlightBands(), ...scene.highlights];
25202
- if (bands.length === 0) return;
25763
+ const sessions = scene.sessionHighlightBands();
25764
+ if (sessions.length > 0) {
25765
+ const left = Math.max(0, coords.logicalToX(-0.5));
25766
+ const right = Math.min(coords.width, coords.logicalToX(coords.barCount - 0.5));
25767
+ this.drawHighlightSet(ctx, sessions, coords, left, right);
25768
+ }
25769
+ this.drawHighlightSet(ctx, scene.highlights, coords, 0, coords.width);
25770
+ }
25771
+ drawHighlightSet(ctx, bands, coords, left, right) {
25203
25772
  for (const band of bands) {
25204
25773
  const x1 = coords.timeToX(band.from);
25205
25774
  const x2 = coords.timeToX(band.to);
25206
- if (x2 < 0 || x1 > coords.width || x2 <= x1) continue;
25207
- const cx = Math.max(0, x1);
25208
- const cw = Math.min(coords.width, x2) - cx;
25209
- if (cw <= 0) continue;
25775
+ const rect = clipHighlightRect(x1, x2, left, right);
25776
+ if (!rect) continue;
25210
25777
  ctx.fillStyle = band.color;
25211
- ctx.fillRect(cx, 0, cw, coords.height);
25778
+ ctx.fillRect(rect.x, 0, rect.width, coords.height);
25212
25779
  }
25213
25780
  }
25214
25781
  // ── grid ── vert/horz gate on `scene.showGrid` AND their own per-axis visibility
@@ -25884,6 +26451,8 @@ var NativeRenderer = class {
25884
26451
  this.glowAmount = 0;
25885
26452
  // WebGL2 neon-glow intensity (canvas2d ignores it)
25886
26453
  this.chrome = new ChromeRenderer();
26454
+ /** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
26455
+ this.labelTooltip = null;
25887
26456
  this.crosshairLayer = new CrosshairRenderer();
25888
26457
  /** 1 Hz repaint pump so the price-axis countdown-to-bar-close ticks; null when off. */
25889
26458
  this.countdownTimer = null;
@@ -25894,6 +26463,8 @@ var NativeRenderer = class {
25894
26463
  this.indicatorValuesOn = true;
25895
26464
  /** Host-contributed legend actions — held here so a rebuild of the legend re-wires them. */
25896
26465
  this.legendActionsProvider = null;
26466
+ /** Host-contributed legend callouts — held here so a rebuild of the legend re-wires them. */
26467
+ this.legendCalloutsProvider = null;
25897
26468
  /** Host override of the legend's fold toggle — held here so a remount re-applies it. */
25898
26469
  this.legendOverviewAction = null;
25899
26470
  // ── keyboard navigation / accessibility (item 11) ──
@@ -27002,6 +27573,10 @@ var NativeRenderer = class {
27002
27573
  this.plot.appendChild(this.scrollButton);
27003
27574
  this.plot.addEventListener("pointermove", this.onScrollProximityMove);
27004
27575
  this.plot.addEventListener("pointerleave", this.onScrollProximityLeave);
27576
+ this.labelTooltip = new LabelTooltip(this.plot, {
27577
+ theme: () => this.chromeTheme(),
27578
+ lookup: (x, y) => this.chrome.labelTooltipAt(x, y)
27579
+ });
27005
27580
  this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
27006
27581
  projector: () => this.drawingProjector(),
27007
27582
  dpr: () => this.coords.dpr,
@@ -27041,9 +27616,10 @@ var NativeRenderer = class {
27041
27616
  this.inputsUI.setDialogHost(this.dialogHost);
27042
27617
  this.inputsUI.setSymbolPicker(this.symbolPicker);
27043
27618
  this.inputsUI.setLegendActions(this.legendActionsProvider);
27619
+ this.inputsUI.setLegendCallouts(this.legendCalloutsProvider);
27044
27620
  this.inputsUI.setLegendOverviewAction(this.legendOverviewAction);
27045
27621
  this.inputsUI.setOnChange((c) => {
27046
- for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value });
27622
+ for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value, ...c.kind ? { kind: c.kind } : {} });
27047
27623
  });
27048
27624
  this.inputsUI.setOnRemove((id) => {
27049
27625
  for (const cb of this.removeIndicatorCbs) cb(id);
@@ -27233,6 +27809,8 @@ var NativeRenderer = class {
27233
27809
  this.settingsButton = null;
27234
27810
  this.plot?.removeEventListener("pointermove", this.onScrollProximityMove);
27235
27811
  this.plot?.removeEventListener("pointerleave", this.onScrollProximityLeave);
27812
+ this.labelTooltip?.destroy();
27813
+ this.labelTooltip = null;
27236
27814
  this.scrollButton?.remove();
27237
27815
  this.scrollButton = null;
27238
27816
  for (const l of this.extLayers) l.instance.destroy?.();
@@ -27473,7 +28051,7 @@ var NativeRenderer = class {
27473
28051
  else this.scene.assignIndicatorZ(model.id);
27474
28052
  this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
27475
28053
  native: !!model.native,
27476
- ...model.shorttitle ? { settingsTitle: model.title } : {}
28054
+ ...model.props ? { props: model.props, propValues: model.propValues ?? {} } : {}
27477
28055
  });
27478
28056
  this.syncTables(model);
27479
28057
  if (model.native?.type === "volume") {
@@ -27524,8 +28102,8 @@ var NativeRenderer = class {
27524
28102
  this.paneControls?.refresh();
27525
28103
  this.scheduler.invalidate(4 /* Full */);
27526
28104
  }
27527
- setIndicatorInputs(handle, values) {
27528
- this.inputsUI.setValues(handle.id, values);
28105
+ setIndicatorInputs(handle, values, props) {
28106
+ this.inputsUI.setValues(handle.id, values, props);
27529
28107
  }
27530
28108
  setSymbolPicker(picker) {
27531
28109
  this.symbolPicker = picker;
@@ -27535,6 +28113,10 @@ var NativeRenderer = class {
27535
28113
  this.legendActionsProvider = provider;
27536
28114
  this.inputsUI?.setLegendActions(provider);
27537
28115
  }
28116
+ setLegendCallouts(provider) {
28117
+ this.legendCalloutsProvider = provider;
28118
+ this.inputsUI?.setLegendCallouts(provider);
28119
+ }
27538
28120
  setLegendOverviewAction(action) {
27539
28121
  this.legendOverviewAction = action;
27540
28122
  this.inputsUI?.setLegendOverviewAction(action);
@@ -28515,7 +29097,11 @@ var NativeRenderer = class {
28515
29097
  }
28516
29098
  const models = this.scene.indicatorsForPane(pane.id);
28517
29099
  const masterModels = models.filter((m) => m.ownScale !== true);
28518
- const dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
29100
+ let dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
29101
+ if (pane === pricePane) {
29102
+ const or = overlaySeriesRange(this.scene.indicators.values(), i0, i1, (id) => this.scene.offsetOf(id));
29103
+ if (or) dr = dr ? { min: Math.min(dr.min, or.min), max: Math.max(dr.max, or.max) } : or;
29104
+ }
28519
29105
  const includeCandles = pane.kind === "price" && !this.scene.candlesHidden;
28520
29106
  pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
28521
29107
  pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
@@ -28789,6 +29375,7 @@ var NativeRenderer = class {
28789
29375
  for (const m of models) {
28790
29376
  const off = this.scene.offsetOf(m.id);
28791
29377
  for (const s of m.series) {
29378
+ if (s.overlay === true) continue;
28792
29379
  if (isLineLikeSeries(s)) {
28793
29380
  const v = s.points[i0 - off]?.value;
28794
29381
  if (v != null && Number.isFinite(v)) return v;
@@ -29694,6 +30281,19 @@ function unregisterLegendAction(id) {
29694
30281
  function legendActions() {
29695
30282
  return [...legendRegistry.values()].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
29696
30283
  }
30284
+ var calloutRegistry = /* @__PURE__ */ new Map();
30285
+ function registerLegendCallout(desc) {
30286
+ calloutRegistry.set(desc.id, desc);
30287
+ return () => {
30288
+ if (calloutRegistry.get(desc.id) === desc) calloutRegistry.delete(desc.id);
30289
+ };
30290
+ }
30291
+ function unregisterLegendCallout(id) {
30292
+ calloutRegistry.delete(id);
30293
+ }
30294
+ function legendCallouts() {
30295
+ return [...calloutRegistry.values()].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
30296
+ }
29697
30297
  var stateHandlers = /* @__PURE__ */ new Map();
29698
30298
  function registerStatePersistence(handler) {
29699
30299
  stateHandlers.set(handler.key, handler);
@@ -29802,6 +30402,7 @@ exports.getNativeIndicator = getNativeIndicator;
29802
30402
  exports.iconMarkup = iconMarkup;
29803
30403
  exports.inputVisible = inputVisible;
29804
30404
  exports.legendActions = legendActions;
30405
+ exports.legendCallouts = legendCallouts;
29805
30406
  exports.nativeIndicatorDescriptors = nativeIndicatorDescriptors;
29806
30407
  exports.nativeIndicatorTypes = nativeIndicatorTypes;
29807
30408
  exports.normalizeSettingsRow = normalizeSettingsRow;
@@ -29810,6 +30411,7 @@ exports.registerDefaultEngine = registerDefaultEngine;
29810
30411
  exports.registerDrawingType = registerDrawingType;
29811
30412
  exports.registerIcon = registerIcon;
29812
30413
  exports.registerLegendAction = registerLegendAction;
30414
+ exports.registerLegendCallout = registerLegendCallout;
29813
30415
  exports.registerNativeIndicator = registerNativeIndicator;
29814
30416
  exports.registerRendererDefaults = registerRendererDefaults;
29815
30417
  exports.registerRendererLayer = registerRendererLayer;
@@ -29834,6 +30436,7 @@ exports.topbarActionOverride = topbarActionOverride;
29834
30436
  exports.unregisterChartType = unregisterChartType;
29835
30437
  exports.unregisterDefaultEngine = unregisterDefaultEngine;
29836
30438
  exports.unregisterLegendAction = unregisterLegendAction;
30439
+ exports.unregisterLegendCallout = unregisterLegendCallout;
29837
30440
  exports.unregisterNativeIndicator = unregisterNativeIndicator;
29838
30441
  exports.unregisterRendererDefaults = unregisterRendererDefaults;
29839
30442
  exports.unregisterRendererLayer = unregisterRendererLayer;