@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
@@ -1,6 +1,6 @@
1
- import { deserializeDrawing, chartTypes, rendererLayers, foldBaseModulation, registerChartType, rendererDefaults, getDrawingType, settingsRowVisible, normalizeSettingsRow, TextLabel, Callout, resetDrawingSettings, registerNativeIndicator, chartType, getNativeIndicator, nativeIndicatorDescriptors, drawingTypes, settingsRowValueKeys, formatDuration, RegressionChannel, FixedRangeVolumeProfile, SegmentDrawing, ArrowMark, GlyphStamp, RadialFib, FibSpiral, DedekindTessellation, MachFigure, GannSquare, FibRatios, MeasureBox, PositionTool, AnchoredVwap, PatternDrawing, Comment, PriceNote, Signpost, Note, PriceLabel, GANN_SQUARE_ARCS, lineSegmentIntersection, DEFAULT_DRAWING_COLOR, GLYPH_OPTIONS, STAMP_SIZE_OPTIONS, LINE_STYLE_OPTIONS, DEDEKIND_CURVATURE_OPTIONS, MACH_NUMBER_OPTIONS, MACH_WAVE_COUNT_OPTIONS, TEXT_SIZE_OPTIONS, createDrawing, inputVisible, tickerModifierIds, CalloutBase, DIRECTION_OPTIONS } from './chunk-L3I2CCYO.js';
2
- import { themeTokens, Dialog, closeOpenPopovers, closeWidthPopover, fieldSection, buildFieldControl, fieldSeparator, fieldGrid, fieldRow, blendOver, splitColor, Menu, TextArea, Popover, NumberInput, buildColorPicker, isPopoverOpen, eventDismissedPopover, toggleSelectList, openPopoverTrigger, fieldGridColumns, FIELD_GAP_PX, STATIC_TOKENS } from './chunk-OICPLNU7.js';
3
- import { icon, iconAt, withAlpha, WARNING, ACCENT, NEUTRAL, BEARISH, BULLISH, isDarkColor, SERIES_LINE, SLATE, CROSSHAIR, TRADE_EXIT, TRADE_SHORT, TRADE_LONG, overlayScrollbarCss, SLATE_DEEP, VALID, INVALID, FIELD_FOCUS_CSS, FIELD_FOCUS_RING, svg24, svg24Solid } from './chunk-PN5KWFZ4.js';
1
+ import { deserializeDrawing, chartTypes, rendererLayers, foldBaseModulation, registerChartType, rendererDefaults, getDrawingType, settingsRowVisible, normalizeSettingsRow, TextLabel, Callout, resetDrawingSettings, registerNativeIndicator, chartType, getNativeIndicator, nativeIndicatorDescriptors, drawingTypes, settingsRowValueKeys, formatDuration, RegressionChannel, FixedRangeVolumeProfile, SegmentDrawing, ArrowMark, GlyphStamp, RadialFib, FibSpiral, DedekindTessellation, MachFigure, GannSquare, FibRatios, MeasureBox, PositionTool, AnchoredVwap, PatternDrawing, Comment, PriceNote, Signpost, Note, PriceLabel, GANN_SQUARE_ARCS, lineSegmentIntersection, DEFAULT_DRAWING_COLOR, GLYPH_OPTIONS, STAMP_SIZE_OPTIONS, LINE_STYLE_OPTIONS, DEDEKIND_CURVATURE_OPTIONS, MACH_NUMBER_OPTIONS, MACH_WAVE_COUNT_OPTIONS, TEXT_SIZE_OPTIONS, createDrawing, inputVisible, tickerModifierIds, CalloutBase, DIRECTION_OPTIONS } from './chunk-EQCHJZOT.js';
2
+ import { themeTokens, Dialog, closeOpenPopovers, closeWidthPopover, fieldSection, buildFieldControl, fieldSeparator, fieldGrid, fieldRow, blendOver, splitColor, CalloutBubble, Menu, TextArea, Popover, NumberInput, buildColorPicker, isPopoverOpen, eventDismissedPopover, toggleSelectList, openPopoverTrigger, fieldGridColumns, FIELD_GAP_PX, STATIC_TOKENS } from './chunk-FCVIG7JG.js';
3
+ import { icon, iconAt, withAlpha, WARNING, ACCENT, NEUTRAL, BEARISH, BULLISH, isDarkColor, SERIES_LINE, SLATE, CROSSHAIR, TRADE_EXIT, TRADE_SHORT, TRADE_LONG, overlayScrollbarCss, SLATE_DEEP, VALID, INVALID, FIELD_FOCUS_CSS, FIELD_FOCUS_RING, svg24, svg24Solid } from './chunk-WFSZBX3R.js';
4
4
 
5
5
  // src/core/events/EventBus.ts
6
6
  var TypedEventBus = class {
@@ -428,6 +428,15 @@ var RendererControl = class {
428
428
  setLegendActions(provider) {
429
429
  this.renderer.setLegendActions?.(provider);
430
430
  }
431
+ /**
432
+ * Wire the legend rows' HOST-CONTRIBUTED callout bubbles (the shells route the
433
+ * plugin registry through this; see `registerLegendCallout`). Silent on a renderer
434
+ * without the seam — contributed callouts simply never show there, same graceful
435
+ * degradation as {@link setLegendActions}.
436
+ */
437
+ setLegendCallouts(provider) {
438
+ this.renderer.setLegendCallouts?.(provider);
439
+ }
431
440
  /**
432
441
  * Replace the indicator legend's fold toggle with a host action (or restore it with
433
442
  * `null`) — multi-chart shells point the chip at their indicator overview instead of
@@ -2898,6 +2907,10 @@ function isLineLikeSeries(spec) {
2898
2907
  }
2899
2908
 
2900
2909
  // src/renderers/shared/IndicatorInputsDialog.ts
2910
+ var PROPS_TAB = "Properties";
2911
+ function bagOf(row, decl) {
2912
+ return decl.prop ? row.propValues : row.values;
2913
+ }
2901
2914
  var SOURCES = ["close", "open", "high", "low", "hl2", "hlc3", "ohlc4", "volume"];
2902
2915
  var IndicatorInputsDialog = class {
2903
2916
  constructor(host) {
@@ -2908,6 +2921,7 @@ var IndicatorInputsDialog = class {
2908
2921
  this.openId = null;
2909
2922
  this.row = null;
2910
2923
  this.snapshot = null;
2924
+ this.propSnapshot = null;
2911
2925
  this.dialogTips = [];
2912
2926
  /** Re-applies every `when` gate against current values; set while the dialog is open. */
2913
2927
  this.refreshVisibility = null;
@@ -2930,10 +2944,11 @@ var IndicatorInputsDialog = class {
2930
2944
  }
2931
2945
  open(row) {
2932
2946
  this.close();
2933
- if (row.inputs.length === 0) return;
2947
+ if (row.inputs.length === 0 && row.props.length === 0) return;
2934
2948
  this.row = row;
2935
2949
  this.openId = row.id;
2936
2950
  this.snapshot = { ...row.values };
2951
+ this.propSnapshot = { ...row.propValues };
2937
2952
  ensureDialogStyles();
2938
2953
  const t = this.host.theme();
2939
2954
  const border = "var(--vela-border)";
@@ -2941,7 +2956,7 @@ var IndicatorInputsDialog = class {
2941
2956
  const host = this.host.dialogHost() ?? this.host.container;
2942
2957
  const ui = new Dialog({
2943
2958
  host,
2944
- title: row.settingsTitle,
2959
+ title: row.title,
2945
2960
  // Non-modal: live-edit dialog — a modal machine locks pointer events on the
2946
2961
  // whole body, killing the chart and the body-portaled popovers.
2947
2962
  modal: false,
@@ -2953,12 +2968,16 @@ var IndicatorInputsDialog = class {
2953
2968
  closeOnEscape: false,
2954
2969
  footer: (foot) => {
2955
2970
  foot.append(
2971
+ this.resetAction(),
2956
2972
  this.dialogButton("Cancel", false, () => this.revertAndClose()),
2957
2973
  this.dialogButton("Ok", true, () => this.close())
2958
2974
  );
2959
2975
  },
2976
+ // Stale-guard: destroying a dialog fires its machine's onOpenChange(false)
2977
+ // asynchronously — after a reset rebuild that notification must not close
2978
+ // the replacement dialog.
2960
2979
  onOpenChange: (open) => {
2961
- if (!open) this.close();
2980
+ if (!open && this.uiDialog === ui) this.close();
2962
2981
  }
2963
2982
  });
2964
2983
  applyChromeTokens(ui.panel, t);
@@ -2969,6 +2988,16 @@ var IndicatorInputsDialog = class {
2969
2988
  text: () => "Close"
2970
2989
  }));
2971
2990
  const tabDefs = tabInputs(row.inputs);
2991
+ if (row.props.length > 0) {
2992
+ const propDecls = row.props.map((p) => {
2993
+ const d = { ...p, prop: true };
2994
+ delete d.tab;
2995
+ return d;
2996
+ });
2997
+ const existing = tabDefs.find((t2) => t2.name === PROPS_TAB);
2998
+ if (existing) existing.inputs.push(...propDecls);
2999
+ else tabDefs.push({ name: PROPS_TAB, inputs: propDecls });
3000
+ }
2972
3001
  const tabs = document.createElement("div");
2973
3002
  tabs.style.cssText = `display:flex;gap:12px;padding:0 20px;border-bottom:1px solid ${border};flex:0 0 auto;`;
2974
3003
  const tabEls = [];
@@ -3071,9 +3100,10 @@ var IndicatorInputsDialog = class {
3071
3100
  this.openId = null;
3072
3101
  this.row = null;
3073
3102
  this.snapshot = null;
3103
+ this.propSnapshot = null;
3074
3104
  ui?.destroy();
3075
3105
  }
3076
- /** Restore every input to its open-time value (re-running the indicator), then close. */
3106
+ /** Restore every input and prop to its open-time value (re-running the indicator), then close. */
3077
3107
  revertAndClose() {
3078
3108
  const row = this.row;
3079
3109
  const snap = this.snapshot;
@@ -3087,12 +3117,55 @@ var IndicatorInputsDialog = class {
3087
3117
  }
3088
3118
  }
3089
3119
  }
3120
+ const propSnap = this.propSnapshot;
3121
+ if (row && propSnap) {
3122
+ for (const p of row.props) {
3123
+ const snapped = propSnap[p.key];
3124
+ const before = snapped !== void 0 ? snapped : p.defval;
3125
+ if (row.propValues[p.key] !== before) {
3126
+ row.propValues[p.key] = before;
3127
+ this.host.onChange?.({ indicatorId: row.id, key: p.key, value: before, kind: "prop" });
3128
+ }
3129
+ }
3130
+ }
3090
3131
  this.close();
3091
3132
  }
3133
+ /** The footer's reset button — same chip as Cancel, pinned to the LEFT edge
3134
+ * (`margin-right:auto` against the footer's flex-end keeps Cancel/Ok right). */
3135
+ resetAction() {
3136
+ const b = this.dialogButton("Reset defaults", false, () => this.resetToDefaults());
3137
+ b.style.marginRight = "auto";
3138
+ return b;
3139
+ }
3140
+ /** Restore every input to its declared default (re-running the indicator), then
3141
+ * re-open the form so each control re-reads the restored values — the same
3142
+ * rebuild-after-reset move as the chart-settings dialog. The open-time snapshot
3143
+ * survives the rebuild, so Cancel after a reset still reverts the whole session. */
3144
+ resetToDefaults() {
3145
+ const row = this.row;
3146
+ if (!row) return;
3147
+ const snap = this.snapshot;
3148
+ const propSnap = this.propSnapshot;
3149
+ for (const inp of row.inputs) {
3150
+ if (row.values[inp.key] !== inp.defval) {
3151
+ row.values[inp.key] = inp.defval;
3152
+ this.host.onChange?.({ indicatorId: row.id, key: inp.key, value: inp.defval });
3153
+ }
3154
+ }
3155
+ for (const p of row.props) {
3156
+ if (row.propValues[p.key] !== p.defval) {
3157
+ row.propValues[p.key] = p.defval;
3158
+ this.host.onChange?.({ indicatorId: row.id, key: p.key, value: p.defval, kind: "prop" });
3159
+ }
3160
+ }
3161
+ this.open(row);
3162
+ if (snap) this.snapshot = snap;
3163
+ if (propSnap) this.propSnapshot = propSnap;
3164
+ }
3092
3165
  /** Write one edit through: store it, notify the host, and re-apply the `when` gates. */
3093
- commit(row, key, value) {
3094
- row.values[key] = value;
3095
- this.host.onChange?.({ indicatorId: row.id, key, value });
3166
+ commit(row, decl, value) {
3167
+ bagOf(row, decl)[decl.key] = value;
3168
+ this.host.onChange?.({ indicatorId: row.id, key: decl.key, value, ...decl.prop ? { kind: "prop" } : {} });
3096
3169
  this.refreshVisibility?.();
3097
3170
  }
3098
3171
  /** One settings row (or several `inline=` inputs) placed into a section's grid. */
@@ -3119,7 +3192,7 @@ var IndicatorInputsDialog = class {
3119
3192
  const id = idOf(lead);
3120
3193
  const info = lead.tooltip ? this.infoButton(lead.tooltip) : void 0;
3121
3194
  if (lead.type === "bool") {
3122
- const current = Boolean(row.values[lead.key] ?? lead.defval);
3195
+ const current = Boolean(bagOf(row, lead)[lead.key] ?? lead.defval);
3123
3196
  return append(fieldRow({
3124
3197
  label: nameOf(lead),
3125
3198
  id,
@@ -3128,7 +3201,7 @@ var IndicatorInputsDialog = class {
3128
3201
  toggle: {
3129
3202
  id,
3130
3203
  checked: current,
3131
- onChange: (v) => this.commit(row, lead.key, v)
3204
+ onChange: (v) => this.commit(row, lead, v)
3132
3205
  }
3133
3206
  }));
3134
3207
  }
@@ -3153,8 +3226,8 @@ var IndicatorInputsDialog = class {
3153
3226
  }
3154
3227
  /** Build the typed control for one input, committing edits live via `onChange`. */
3155
3228
  buildControl(row, inp, id) {
3156
- const current = row.values[inp.key] ?? inp.defval;
3157
- const emit = (value) => this.commit(row, inp.key, value);
3229
+ const current = bagOf(row, inp)[inp.key] ?? inp.defval;
3230
+ const emit = (value) => this.commit(row, inp, value);
3158
3231
  if (inp.type === "bool") {
3159
3232
  return buildFieldControl({ kind: "switch", id, checked: Boolean(current), onChange: (v) => emit(v) }).el;
3160
3233
  }
@@ -3165,7 +3238,7 @@ var IndicatorInputsDialog = class {
3165
3238
  kind: "color",
3166
3239
  id,
3167
3240
  theme: this.host.theme(),
3168
- get: () => String(row.values[inp.key] ?? inp.defval),
3241
+ get: () => String(bagOf(row, inp)[inp.key] ?? inp.defval),
3169
3242
  onChange: (v) => emit(v)
3170
3243
  }).el;
3171
3244
  }
@@ -3728,6 +3801,7 @@ function timeParts(ts) {
3728
3801
  }
3729
3802
 
3730
3803
  // src/renderers/shared/InputsUI.ts
3804
+ var LEGEND_AT_TOP_ATTR = "data-vela-pane-at-top";
3731
3805
  var STATUS_KEYFRAMES = "@keyframes vela-ind-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.3;transform:scale(.6)}}";
3732
3806
  var LEGEND_ICON_PX2 = 16;
3733
3807
  var LEGEND_CTL_PX = 18;
@@ -3766,10 +3840,14 @@ var InputsUI = class {
3766
3840
  this.moveApi = null;
3767
3841
  /** Host-contributed legend actions, resolved PER ROW at render time (see setLegendActions). */
3768
3842
  this.legendActions = null;
3843
+ /** Host-contributed callout bubbles, resolved PER ROW at render time (see setLegendCallouts). */
3844
+ this.legendCallouts = null;
3769
3845
  /** Chrome-tooltip disposers, per row id — a tip open at removal must not outlive its row. */
3770
3846
  this.rowTips = /* @__PURE__ */ new Map();
3771
3847
  /** Same, for the contributed extras only (rebuilt independently by setLegendActions). */
3772
3848
  this.extrasTips = /* @__PURE__ */ new Map();
3849
+ /** Same, for the contributed callouts only (rebuilt independently by setLegendCallouts). */
3850
+ this.calloutTips = /* @__PURE__ */ new Map();
3773
3851
  /** Open "Move to" menu (kept so it can be torn down). */
3774
3852
  this.moveMenu = null;
3775
3853
  this.moveTargets = [];
@@ -3927,6 +4005,42 @@ var InputsUI = class {
3927
4005
  extrasEl.appendChild(btn2);
3928
4006
  }
3929
4007
  }
4008
+ /**
4009
+ * Wire the host-contributed callout bubbles. Same contract as
4010
+ * {@link setLegendActions}: re-calling replaces the provider and re-projects the
4011
+ * rows already on screen.
4012
+ */
4013
+ setLegendCallouts(provider) {
4014
+ this.legendCallouts = provider;
4015
+ for (const row of this.rows.values()) this.renderCallouts(row);
4016
+ }
4017
+ /** (Re)build one row's callout bubbles — tinted icon circles beside the title whose
4018
+ * click (when the view carries content) deploys a panel of text and actions. */
4019
+ renderCallouts(row) {
4020
+ this.disposeTips(this.calloutTips, row.id);
4021
+ for (const bubble of row.callouts) bubble.destroy();
4022
+ row.callouts = [];
4023
+ row.calloutsEl.replaceChildren();
4024
+ const views = this.legendCallouts?.(row.id) ?? [];
4025
+ row.calloutsEl.style.display = views.length > 0 ? "inline-flex" : "none";
4026
+ for (const view of views) {
4027
+ const bubble = new CalloutBubble({
4028
+ icon: view.icon,
4029
+ background: view.background,
4030
+ ...view.color !== void 0 ? { color: view.color } : {},
4031
+ label: view.tooltip,
4032
+ ...view.content !== void 0 ? { panel: view.content } : {},
4033
+ // The panel portals into the plot host and self-tokens: it must work on
4034
+ // a bare chart, where no `.vela-ui` token ancestor exists.
4035
+ host: this.container,
4036
+ theme: () => this.theme
4037
+ });
4038
+ bubble.el.dataset.legendCallout = view.id;
4039
+ this.tip(this.calloutTips, row.id, bubble.el, view.tooltip);
4040
+ row.callouts.push(bubble);
4041
+ row.calloutsEl.appendChild(bubble.el);
4042
+ }
4043
+ }
3930
4044
  /** Reposition the per-pane legend containers after a layout change. */
3931
4045
  reposition() {
3932
4046
  for (const [paneId, lg] of this.legends) this.positionLegend(lg, paneId);
@@ -4094,6 +4208,7 @@ var InputsUI = class {
4094
4208
  }
4095
4209
  positionLegend(lg, paneId) {
4096
4210
  const bounds = this.paneBoundsOf ? this.paneBoundsOf(paneId) : { top: 0, height: Infinity };
4211
+ lg.toggleAttribute(LEGEND_AT_TOP_ATTR, bounds.top === 0);
4097
4212
  lg.style.display = bounds.height < 4 || !this.titlesVisible ? "none" : "flex";
4098
4213
  const collapsed = this.paneCollapse.has(paneId);
4099
4214
  const masterId = this.paneCollapse.get(paneId) ?? null;
@@ -4169,13 +4284,13 @@ var InputsUI = class {
4169
4284
  }
4170
4285
  /** Create or update an indicator's legend row (in the legend for its pane). */
4171
4286
  upsert(id, title, inputs, values, paneId = "price", opts = {}) {
4172
- const settingsTitle = opts.settingsTitle ?? title;
4173
4287
  const existing = this.rows.get(id);
4174
4288
  if (existing) {
4175
4289
  existing.title = title;
4176
- existing.settingsTitle = settingsTitle;
4177
4290
  existing.inputs = inputs;
4178
4291
  existing.values = { ...values };
4292
+ existing.props = opts.props ?? [];
4293
+ existing.propValues = { ...opts.propValues ?? {} };
4179
4294
  existing.titleEl.textContent = title;
4180
4295
  if (existing.paneId !== paneId) {
4181
4296
  existing.paneId = paneId;
@@ -4227,6 +4342,9 @@ var InputsUI = class {
4227
4342
  titleWrap.appendChild(beta);
4228
4343
  }
4229
4344
  el.appendChild(titleWrap);
4345
+ const calloutsEl = document.createElement("span");
4346
+ calloutsEl.style.cssText = `display:none;align-items:center;gap:4px;flex:none;margin-left:${LEGEND_TITLE_STATUS_GAP_PX}px;`;
4347
+ el.appendChild(calloutsEl);
4230
4348
  el.appendChild(statusEl);
4231
4349
  const valuesEl = document.createElement("span");
4232
4350
  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;`;
@@ -4243,13 +4361,13 @@ var InputsUI = class {
4243
4361
  eye.className = "vela-ind-ctl";
4244
4362
  eye.style.cssText = LEGEND_CTL_CSS;
4245
4363
  eye.addEventListener("click", () => {
4246
- const row = this.rows.get(id);
4247
- this.onToggleVisible?.(id, Boolean(row?.hidden));
4364
+ const row2 = this.rows.get(id);
4365
+ this.onToggleVisible?.(id, Boolean(row2?.hidden));
4248
4366
  });
4249
4367
  controlsEl.appendChild(eye);
4250
4368
  eyeEl = eye;
4251
4369
  }
4252
- if (inputs.length > 0) {
4370
+ if (inputs.length > 0 || (opts.props ?? []).length > 0) {
4253
4371
  const gear = document.createElement("button");
4254
4372
  gear.type = "button";
4255
4373
  gear.setAttribute("aria-label", "Settings");
@@ -4289,7 +4407,9 @@ var InputsUI = class {
4289
4407
  controlsEl.appendChild(close);
4290
4408
  el.appendChild(controlsEl);
4291
4409
  this.attach(this.legendFor(paneId), el, !!opts.native);
4292
- 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 });
4410
+ 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 };
4411
+ this.rows.set(id, row);
4412
+ this.renderCallouts(row);
4293
4413
  this.syncFoldToggle();
4294
4414
  }
4295
4415
  /** Place a row in its pane's legend — native rows PREPEND (pinned to the top), Pine rows append. */
@@ -4298,9 +4418,11 @@ var InputsUI = class {
4298
4418
  else container.appendChild(el);
4299
4419
  }
4300
4420
  /** Reflect programmatic input changes (so a re-opened dialog shows current values). */
4301
- setValues(id, values) {
4421
+ setValues(id, values, props) {
4302
4422
  const row = this.rows.get(id);
4303
- if (row) row.values = { ...row.values, ...values };
4423
+ if (!row) return;
4424
+ row.values = { ...row.values, ...values };
4425
+ if (props) row.propValues = { ...row.propValues, ...props };
4304
4426
  }
4305
4427
  /**
4306
4428
  * Reflect an indicator's live status in its legend row: `'loading'` shows three pulsing
@@ -4382,8 +4504,13 @@ var InputsUI = class {
4382
4504
  syncRowActions(row) {
4383
4505
  const open = row.highlighted;
4384
4506
  row.controlsEl.style.display = open || row.hidden ? "inline-flex" : "none";
4385
- if (open) row.el.appendChild(row.statusEl);
4386
- else row.el.insertBefore(row.statusEl, row.valuesEl);
4507
+ if (open) {
4508
+ row.el.appendChild(row.statusEl);
4509
+ row.el.appendChild(row.calloutsEl);
4510
+ } else {
4511
+ row.el.insertBefore(row.statusEl, row.valuesEl);
4512
+ row.el.insertBefore(row.calloutsEl, row.statusEl);
4513
+ }
4387
4514
  for (const child of Array.from(row.controlsEl.children)) {
4388
4515
  if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
4389
4516
  if (child === row.extrasEl) {
@@ -4401,10 +4528,12 @@ var InputsUI = class {
4401
4528
  }
4402
4529
  remove(id) {
4403
4530
  const row = this.rows.get(id);
4531
+ for (const bubble of row?.callouts ?? []) bubble.destroy();
4404
4532
  row?.el.remove();
4405
4533
  this.rows.delete(id);
4406
4534
  this.disposeTips(this.rowTips, id);
4407
4535
  this.disposeTips(this.extrasTips, id);
4536
+ this.disposeTips(this.calloutTips, id);
4408
4537
  if (this.selectedId === id) this.selectedId = null;
4409
4538
  if (this.inputsDialog.openId === id) this.inputsDialog.close();
4410
4539
  this.syncFoldToggle();
@@ -4424,6 +4553,8 @@ var InputsUI = class {
4424
4553
  this.rowMenu = null;
4425
4554
  for (const id of [...this.rowTips.keys()]) this.disposeTips(this.rowTips, id);
4426
4555
  for (const id of [...this.extrasTips.keys()]) this.disposeTips(this.extrasTips, id);
4556
+ for (const id of [...this.calloutTips.keys()]) this.disposeTips(this.calloutTips, id);
4557
+ for (const row of this.rows.values()) for (const bubble of row.callouts) bubble.destroy();
4427
4558
  for (const lg of this.legends.values()) lg.remove();
4428
4559
  this.legends.clear();
4429
4560
  this.rows.clear();
@@ -4778,6 +4909,32 @@ var SIZE_PX = {
4778
4909
  large: 16,
4779
4910
  huge: 20
4780
4911
  };
4912
+ function fontPxOf(size) {
4913
+ if (typeof size === "number") return size > 0 ? size : SIZE_PX.auto;
4914
+ return SIZE_PX[size] ?? SIZE_PX.auto;
4915
+ }
4916
+ function tableHasContent(t) {
4917
+ return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
4918
+ }
4919
+ function mergeRenderPlan(t) {
4920
+ const span = /* @__PURE__ */ new Map();
4921
+ const omit = /* @__PURE__ */ new Set();
4922
+ for (const m of t.merges) {
4923
+ span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
4924
+ for (let r = m.startRow; r <= m.endRow; r += 1) {
4925
+ for (let c = m.startCol; c <= m.endCol; c += 1) {
4926
+ if (r !== m.startRow || c !== m.startCol) omit.add(`${r}:${c}`);
4927
+ }
4928
+ }
4929
+ }
4930
+ for (let r = 0; r < t.rows; r += 1) {
4931
+ for (let c = 0; c < t.columns; c += 1) {
4932
+ if (t.cells[r]?.[c]?.merged && !span.has(`${r}:${c}`)) omit.add(`${r}:${c}`);
4933
+ }
4934
+ }
4935
+ for (const key of span.keys()) omit.delete(key);
4936
+ return { span, omit };
4937
+ }
4781
4938
  var TableOverlay = class {
4782
4939
  constructor(container, theme, paneBounds) {
4783
4940
  this.container = container;
@@ -4798,7 +4955,9 @@ var TableOverlay = class {
4798
4955
  update(tables) {
4799
4956
  this.lastTables = tables;
4800
4957
  this.root.replaceChildren();
4801
- for (const t of tables) this.root.appendChild(this.renderTable(t));
4958
+ for (const t of tables) {
4959
+ if (tableHasContent(t)) this.root.appendChild(this.renderTable(t));
4960
+ }
4802
4961
  }
4803
4962
  /** Re-render at the current pane geometry — after layout settles or on resize. */
4804
4963
  reposition() {
@@ -4813,39 +4972,36 @@ var TableOverlay = class {
4813
4972
  this.root.remove();
4814
4973
  }
4815
4974
  renderTable(t) {
4975
+ const b = this.paneBounds(t.paneId);
4816
4976
  const wrap = document.createElement("div");
4817
4977
  wrap.style.position = "absolute";
4818
- this.anchor(wrap, t.position, this.paneBounds(t.paneId));
4978
+ if (t.frameColor && t.frameWidth > 0) wrap.style.border = `${t.frameWidth}px solid ${t.frameColor}`;
4979
+ this.anchor(wrap, t.position, b);
4819
4980
  const table = document.createElement("table");
4820
4981
  Object.assign(table.style, {
4821
4982
  borderCollapse: "collapse",
4822
4983
  background: t.bgColor ?? "transparent",
4823
4984
  fontFamily: this.theme.fontFamily || "sans-serif",
4824
- // Frame as an inset shadow (not a `border`) so it stays independent of the
4825
- // collapsed cell borders even when frame_width != border_width.
4826
- boxShadow: t.frameColor && t.frameWidth > 0 ? `inset 0 0 0 ${t.frameWidth}px ${t.frameColor}` : "none",
4827
4985
  border: "none",
4828
4986
  tableLayout: "auto",
4829
4987
  // Re-enable pointer events on the table only (root is none) so cell tooltips work.
4830
4988
  pointerEvents: "auto"
4831
4989
  });
4832
- const span = /* @__PURE__ */ new Map();
4833
- const skip = /* @__PURE__ */ new Set();
4834
- for (const m of t.merges) {
4835
- span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
4836
- for (let r = m.startRow; r <= m.endRow; r += 1) {
4837
- for (let c = m.startCol; c <= m.endCol; c += 1) {
4838
- if (r !== m.startRow || c !== m.startCol) skip.add(`${r}:${c}`);
4839
- }
4840
- }
4841
- }
4990
+ const { span, omit } = mergeRenderPlan(t);
4991
+ const plotW = Math.max(0, (this.root.clientWidth || this.container.clientWidth) - b.rightAxis);
4992
+ const paneH = b.height;
4842
4993
  const cellBorder = t.borderColor && t.borderWidth > 0 ? `${t.borderWidth}px solid ${t.borderColor}` : "none";
4843
4994
  for (let r = 0; r < t.rows; r += 1) {
4844
4995
  const tr = document.createElement("tr");
4845
4996
  for (let c = 0; c < t.columns; c += 1) {
4997
+ if (omit.has(`${r}:${c}`)) continue;
4846
4998
  const cell = t.cells[r]?.[c] ?? null;
4847
- if (skip.has(`${r}:${c}`) || cell?.merged) continue;
4848
4999
  const td = document.createElement("td");
5000
+ if (cell === null) {
5001
+ Object.assign(td.style, { padding: "0", border: "none" });
5002
+ tr.appendChild(td);
5003
+ continue;
5004
+ }
4849
5005
  const sp = span.get(`${r}:${c}`);
4850
5006
  if (sp) {
4851
5007
  if (sp.cs > 1) td.colSpan = sp.cs;
@@ -4854,18 +5010,22 @@ var TableOverlay = class {
4854
5010
  Object.assign(td.style, {
4855
5011
  border: cellBorder,
4856
5012
  padding: "2px 6px",
4857
- background: cell?.bgColor ?? "transparent",
4858
- color: cell?.textColor ?? this.theme.textColor,
4859
- textAlign: cell?.hAlign ?? "center",
4860
- verticalAlign: cell?.vAlign === "top" ? "top" : cell?.vAlign === "bottom" ? "bottom" : "middle",
4861
- fontSize: `${SIZE_PX[cell?.textSize ?? "normal"]}px`,
4862
- fontFamily: cell?.fontFamily === "monospace" ? "monospace" : "inherit",
4863
- fontWeight: cell?.bold ? "bold" : "normal",
4864
- fontStyle: cell?.italic ? "italic" : "normal",
4865
- whiteSpace: "pre-line"
5013
+ background: cell.bgColor ?? "transparent",
5014
+ color: cell.textColor ?? this.theme.textColor,
5015
+ textAlign: cell.hAlign,
5016
+ verticalAlign: cell.vAlign === "top" ? "top" : cell.vAlign === "bottom" ? "bottom" : "middle",
5017
+ fontSize: `${fontPxOf(cell.textSize)}px`,
5018
+ fontFamily: cell.fontFamily === "monospace" ? "monospace" : "inherit",
5019
+ fontWeight: cell.bold ? "bold" : "normal",
5020
+ fontStyle: cell.italic ? "italic" : "normal",
5021
+ // Pine cell text never wraps; `\n` still breaks lines. Wrapping
5022
+ // used to collapse unicode sparklines and ━━━ dividers.
5023
+ whiteSpace: "pre"
4866
5024
  });
4867
- if (cell?.tooltip) td.title = cell.tooltip;
4868
- td.textContent = cell?.text ?? "";
5025
+ if (cell.width) td.style.width = `${cell.width / 100 * plotW}px`;
5026
+ if (cell.height) td.style.height = `${cell.height / 100 * paneH}px`;
5027
+ if (cell.tooltip) td.title = cell.tooltip;
5028
+ td.textContent = cell.text ?? "";
4869
5029
  tr.appendChild(td);
4870
5030
  }
4871
5031
  table.appendChild(tr);
@@ -5750,8 +5910,14 @@ var WebGL2Backend = class {
5750
5910
  return sc === pane.scale ? pane : { ...pane, scale: sc };
5751
5911
  };
5752
5912
  b.alpha = this.modelAlpha;
5753
- for (const m of models) for (const bgSpan of m.backgrounds) this.emitBackground(b, bgSpan, pane, coords);
5754
- 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));
5913
+ for (const m of models) for (const bgSpan of m.backgrounds) if (bgSpan.overlay !== true) this.emitBackground(b, bgSpan, pane, coords);
5914
+ 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));
5915
+ if (isPrice) {
5916
+ for (const m of scene.indicators.values()) {
5917
+ for (const bgSpan of m.backgrounds) if (bgSpan.overlay === true) this.emitBackground(b, bgSpan, pane, coords);
5918
+ for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, i1, scene.offsetOf(m.id));
5919
+ }
5920
+ }
5755
5921
  const drawCandles = isPrice && !scene.candlesHidden;
5756
5922
  let candleDrawn = false;
5757
5923
  for (const m of models) {
@@ -5765,7 +5931,7 @@ var WebGL2Backend = class {
5765
5931
  b.alpha = this.modelAlpha;
5766
5932
  const off = scene.offsetOf(m.id);
5767
5933
  const mp = effPane(m);
5768
- for (const s of m.series) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
5934
+ for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
5769
5935
  }
5770
5936
  if (drawCandles && !candleDrawn) {
5771
5937
  drawSlicesUpTo(scene.candleZ);
@@ -5774,6 +5940,12 @@ var WebGL2Backend = class {
5774
5940
  }
5775
5941
  drawSlicesUpTo(Infinity);
5776
5942
  b.alpha = this.modelAlpha;
5943
+ if (isPrice) {
5944
+ for (const m of scene.indicators.values()) {
5945
+ const off = scene.offsetOf(m.id);
5946
+ for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0, i1, theme, off);
5947
+ }
5948
+ }
5777
5949
  for (const m of models) {
5778
5950
  const mp = effPane(m);
5779
5951
  for (const pl of m.priceLines) this.emitHline(b, pl, mp, coords, dataW, theme);
@@ -5820,7 +5992,7 @@ var WebGL2Backend = class {
5820
5992
  for (const pane of scene.orderedPanes()) {
5821
5993
  const b = this.glowBatch;
5822
5994
  b.reset();
5823
- this.emitGlowSources(b, scene.indicatorsForPane(pane.id), pane, coords, i0, i1, (id) => scene.offsetOf(id));
5995
+ this.emitGlowSources(b, scene, pane, coords, i0, i1);
5824
5996
  if (b.vertexCount === 0) continue;
5825
5997
  const topH = Math.round(pane.bounds.top * dpr * 0.5);
5826
5998
  const botH = Math.round((pane.bounds.top + pane.bounds.height) * dpr * 0.5);
@@ -5889,15 +6061,23 @@ var WebGL2Backend = class {
5889
6061
  gl.bindVertexArray(null);
5890
6062
  return true;
5891
6063
  }
5892
- /** The "neon" elements that glow: line/area/step lines + point markers (not candles/fills/bars). */
5893
- emitGlowSources(b, models, pane, coords, i0, i1, offsetOf = () => 0) {
5894
- for (const m of models) {
5895
- const off = offsetOf(m.id);
5896
- for (const s of m.series) {
5897
- if (!isLineLikeSeries(s) || s.visible === false) continue;
5898
- if (s.kind === "histogram" || s.kind === "columns") continue;
5899
- if (s.kind === "circles" || s.kind === "cross") this.emitPointMarkers(b, s, pane, coords, i0, i1, off);
5900
- else this.emitPolyline(b, s, pane, coords, i0, i1, s.kind === "step", off);
6064
+ /** The "neon" elements that glow: line/area/step lines + point markers (not candles/fills/bars).
6065
+ * Routing mirrors the main pass: own series per pane, force_overlay series on the price pane. */
6066
+ emitGlowSources(b, scene, pane, coords, i0, i1) {
6067
+ const emitOne = (s, off) => {
6068
+ if (!isLineLikeSeries(s) || s.visible === false) return;
6069
+ if (s.kind === "histogram" || s.kind === "columns") return;
6070
+ if (s.kind === "circles" || s.kind === "cross") this.emitPointMarkers(b, s, pane, coords, i0, i1, off);
6071
+ else this.emitPolyline(b, s, pane, coords, i0, i1, s.kind === "step", off);
6072
+ };
6073
+ for (const m of scene.indicatorsForPane(pane.id)) {
6074
+ const off = scene.offsetOf(m.id);
6075
+ for (const s of m.series) if (s.overlay !== true) emitOne(s, off);
6076
+ }
6077
+ if (pane.kind === "price") {
6078
+ for (const m of scene.indicators.values()) {
6079
+ const off = scene.offsetOf(m.id);
6080
+ for (const s of m.series) if (s.overlay === true) emitOne(s, off);
5901
6081
  }
5902
6082
  }
5903
6083
  }
@@ -6180,9 +6360,9 @@ var WebGL2Backend = class {
6180
6360
  b.alpha = this.candleBodyAlpha;
6181
6361
  b.rect(g.bodyX, bodyTop, g.bodyW, bodyH, c);
6182
6362
  }
6183
- if (cs.borderVisible || bc || fading && cs.bodyVisible) {
6363
+ if (cs.borderVisible || fading && cs.bodyVisible) {
6184
6364
  b.alpha = this.candleStructureAlpha;
6185
- const bord = cs.borderVisible || bc ? parseColor((isUp ? cs.borderUpColor : cs.borderDownColor) ?? dir) : c;
6365
+ const bord = cs.borderVisible ? parseColor((isUp ? cs.borderUpColor : cs.borderDownColor) ?? bodyColorStr) : c;
6186
6366
  const bw = Math.max(0, g.bodyW - 1);
6187
6367
  const bh = Math.max(0, bodyH - 1);
6188
6368
  b.rectStroke(g.bodyX + 0.5, bodyTop + 0.5, bw, bh, 1, bord);
@@ -7734,8 +7914,14 @@ var Canvas2dBackend = class {
7734
7914
  return sc === pane.scale ? pane : { ...pane, scale: sc };
7735
7915
  };
7736
7916
  ctx.globalAlpha = this.modelAlpha;
7737
- for (const m of models) for (const bg of m.backgrounds) this.drawBackground(ctx, bg, pane, coords);
7738
- 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));
7917
+ for (const m of models) for (const bg of m.backgrounds) if (bg.overlay !== true) this.drawBackground(ctx, bg, pane, coords);
7918
+ 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));
7919
+ if (isPrice) {
7920
+ for (const m of scene.indicators.values()) {
7921
+ for (const bg of m.backgrounds) if (bg.overlay === true) this.drawBackground(ctx, bg, pane, coords);
7922
+ for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, scene.offsetOf(m.id));
7923
+ }
7924
+ }
7739
7925
  const slices = scene.drawingSlices.get(pane.id) ?? [];
7740
7926
  let si = 0;
7741
7927
  const drawSlicesUpTo = (z) => {
@@ -7757,7 +7943,7 @@ var Canvas2dBackend = class {
7757
7943
  ctx.globalAlpha = this.modelAlpha;
7758
7944
  const off = scene.offsetOf(m.id);
7759
7945
  const mp = effPane(m);
7760
- for (const s of m.series) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
7946
+ for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
7761
7947
  }
7762
7948
  if (drawCandles && !candleDrawn) {
7763
7949
  drawSlicesUpTo(scene.candleZ);
@@ -7765,6 +7951,13 @@ var Canvas2dBackend = class {
7765
7951
  this.drawPriceSeries(ctx, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
7766
7952
  }
7767
7953
  drawSlicesUpTo(Infinity);
7954
+ if (isPrice) {
7955
+ ctx.globalAlpha = this.modelAlpha;
7956
+ for (const m of scene.indicators.values()) {
7957
+ const off = scene.offsetOf(m.id);
7958
+ for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0, i1, theme, off);
7959
+ }
7960
+ }
7768
7961
  ctx.globalAlpha = this.modelAlpha;
7769
7962
  for (const m of models) {
7770
7963
  const mp = effPane(m);
@@ -8036,9 +8229,9 @@ var Canvas2dBackend = class {
8036
8229
  ctx.fillStyle = color;
8037
8230
  ctx.fillRect(g.bodyX, top, g.bodyW, bodyH);
8038
8231
  }
8039
- if (cs.borderVisible || bc || fading && cs.bodyVisible) {
8232
+ if (cs.borderVisible || fading && cs.bodyVisible) {
8040
8233
  ctx.globalAlpha = this.candleStructureAlpha;
8041
- ctx.strokeStyle = cs.borderVisible || bc ? (up ? cs.borderUpColor : cs.borderDownColor) ?? dir : color;
8234
+ ctx.strokeStyle = cs.borderVisible ? (up ? cs.borderUpColor : cs.borderDownColor) ?? color : color;
8042
8235
  ctx.lineWidth = 1;
8043
8236
  const bw = Math.max(0, g.bodyW - 1);
8044
8237
  const bh = Math.max(0, bodyH - 1);
@@ -8470,6 +8663,8 @@ var DrawingSceneRenderer = class {
8470
8663
  this.set = set;
8471
8664
  /** measureText width cache, keyed by `${font} ${text}`, persists across frames. */
8472
8665
  this.widthCache = /* @__PURE__ */ new Map();
8666
+ /** Tooltip hit-rects captured while drawing the CURRENT set (rebuilt per render). */
8667
+ this.tipRegions = [];
8473
8668
  /**
8474
8669
  * Index offset of the CURRENT set's model: its `xloc:'bar_index'` coordinates count
8475
8670
  * from the model's anchor bar, so they shift by this to land on chart logical indices.
@@ -8490,8 +8685,13 @@ var DrawingSceneRenderer = class {
8490
8685
  const s = this.set;
8491
8686
  return !s.lines.length && !s.boxes.length && !s.labels.length && !s.polylines.length && !s.linefills.length;
8492
8687
  }
8688
+ /** Tooltip hit-rects of the labels drawn by the LAST `render` call (same coords as `ctx`). */
8689
+ labelTipRegions() {
8690
+ return this.tipRegions;
8691
+ }
8493
8692
  /** Draw the whole set into `ctx` using the supplied coordinate closures. */
8494
8693
  render(ctx, W, H, xOf, yOf) {
8694
+ this.tipRegions = [];
8495
8695
  if (this.isEmpty()) return;
8496
8696
  ctx.save();
8497
8697
  this.drawLinefills(ctx, W, H, xOf, yOf);
@@ -8839,13 +9039,35 @@ var DrawingSceneRenderer = class {
8839
9039
  if (this.isPointShape(lb.style)) {
8840
9040
  if (!lb.noFill) this.drawLabelShape(ctx, lb.style, px, py, fontPx, color);
8841
9041
  if (lb.text) this.drawLabelText(ctx, lb, px, py + fontPx, fontPx);
8842
- } else if (lb.style === "none" || lb.style === "text_outline" || lb.noFill) {
8843
- if (lb.text) this.drawLabelText(ctx, lb, px, py, fontPx, lb.style === "text_outline");
9042
+ if (lb.tooltip) {
9043
+ const r = Math.max(4, fontPx * 0.6) + 3;
9044
+ this.tipRegions.push({ left: px - r, top: py - r, right: px + r, bottom: py + r, text: lb.tooltip });
9045
+ }
9046
+ } else if (lb.style === "none" || lb.style === "text_outline") {
9047
+ if (lb.text) {
9048
+ this.drawLabelText(ctx, lb, px, py, fontPx, lb.style === "text_outline");
9049
+ if (lb.tooltip) this.tipRegions.push(this.textRegion(ctx, lb, px, py, fontPx, lb.tooltip));
9050
+ }
8844
9051
  } else {
8845
- this.drawBubble(ctx, lb, px, py, fontPx, color);
9052
+ const r = this.drawBubble(ctx, lb, px, py, fontPx, color);
9053
+ 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 });
8846
9054
  }
8847
9055
  }
8848
9056
  }
9057
+ /** Canvas font for a label's text — family + Pine `text_formatting` (bold/italic). */
9058
+ labelFont(lb, fontPx) {
9059
+ const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
9060
+ return `${lb.italic ? "italic " : ""}${lb.bold ? "bold " : ""}${fontPx}px ${family}`;
9061
+ }
9062
+ /** Hover rect of a text-only label (style none/text_outline/noFill), centered like drawLabelText. */
9063
+ textRegion(ctx, lb, cx, cy, fontPx, text) {
9064
+ const font = this.labelFont(lb, fontPx);
9065
+ const lines = (lb.text ?? "").split("\n");
9066
+ const w = Math.max(1, ...lines.map((l) => this.measure(ctx, font, l)));
9067
+ const h = fontPx * 1.25 * lines.length;
9068
+ const left = lb.textAlign === "left" ? cx : lb.textAlign === "right" ? cx - w : cx - w / 2;
9069
+ return { left, top: cy - h / 2, right: left + w, bottom: cy + h / 2, text };
9070
+ }
8849
9071
  isPointShape(style) {
8850
9072
  switch (style) {
8851
9073
  case "circle":
@@ -8864,8 +9086,7 @@ var DrawingSceneRenderer = class {
8864
9086
  }
8865
9087
  }
8866
9088
  drawLabelText(ctx, lb, cx, cy, fontPx, outline = false) {
8867
- const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
8868
- ctx.font = `${fontPx}px ${family}`;
9089
+ ctx.font = this.labelFont(lb, fontPx);
8869
9090
  ctx.textAlign = lb.textAlign === "left" ? "left" : lb.textAlign === "right" ? "right" : "center";
8870
9091
  ctx.textBaseline = "middle";
8871
9092
  const lines = lb.text.split("\n");
@@ -8945,9 +9166,9 @@ var DrawingSceneRenderer = class {
8945
9166
  ctx.fill();
8946
9167
  }
8947
9168
  }
9169
+ /** Draw a bubble label; returns the bubble body rect (for tooltip hit-testing). */
8948
9170
  drawBubble(ctx, lb, px, py, fontPx, color) {
8949
- const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
8950
- ctx.font = `${fontPx}px ${family}`;
9171
+ ctx.font = this.labelFont(lb, fontPx);
8951
9172
  const lines = (lb.text ?? "").split("\n");
8952
9173
  const padX = 6;
8953
9174
  const padY = 4;
@@ -9005,38 +9226,51 @@ var DrawingSceneRenderer = class {
9005
9226
  by = py - h - ptr;
9006
9227
  pointer = "down";
9007
9228
  }
9008
- ctx.fillStyle = color;
9009
- this.roundRect(ctx, bx, by, w, h, 4);
9010
- ctx.fill();
9011
- if (pointer !== "none") {
9012
- ctx.beginPath();
9013
- if (pointer === "down") {
9014
- ctx.moveTo(px - ptr, by + h);
9015
- ctx.lineTo(px + ptr, by + h);
9016
- ctx.lineTo(px, py);
9017
- } else if (pointer === "up") {
9018
- ctx.moveTo(px - ptr, by);
9019
- ctx.lineTo(px + ptr, by);
9020
- ctx.lineTo(px, py);
9021
- } else if (pointer === "left") {
9022
- ctx.moveTo(bx, py - ptr);
9023
- ctx.lineTo(bx, py + ptr);
9024
- ctx.lineTo(px, py);
9025
- } else {
9026
- ctx.moveTo(bx + w, py - ptr);
9027
- ctx.lineTo(bx + w, py + ptr);
9028
- ctx.lineTo(px, py);
9029
- }
9030
- ctx.closePath();
9229
+ if (!lb.noFill) {
9230
+ ctx.fillStyle = color;
9231
+ this.roundRect(ctx, bx, by, w, h, 4);
9031
9232
  ctx.fill();
9233
+ if (pointer !== "none") {
9234
+ ctx.beginPath();
9235
+ if (pointer === "down") {
9236
+ ctx.moveTo(px - ptr, by + h);
9237
+ ctx.lineTo(px + ptr, by + h);
9238
+ ctx.lineTo(px, py);
9239
+ } else if (pointer === "up") {
9240
+ ctx.moveTo(px - ptr, by);
9241
+ ctx.lineTo(px + ptr, by);
9242
+ ctx.lineTo(px, py);
9243
+ } else if (pointer === "left") {
9244
+ ctx.moveTo(bx, py - ptr);
9245
+ ctx.lineTo(bx, py + ptr);
9246
+ ctx.lineTo(px, py);
9247
+ } else {
9248
+ ctx.moveTo(bx + w, py - ptr);
9249
+ ctx.lineTo(bx + w, py + ptr);
9250
+ ctx.lineTo(px, py);
9251
+ }
9252
+ ctx.closePath();
9253
+ ctx.fill();
9254
+ }
9032
9255
  }
9033
9256
  if (lb.text) {
9034
- ctx.fillStyle = lb.textColor ?? contrastColor(color);
9035
- ctx.textAlign = "center";
9257
+ ctx.fillStyle = lb.textColor ?? (lb.noFill ? this.deps.theme.textColor : contrastColor(color));
9036
9258
  ctx.textBaseline = "middle";
9259
+ let tx;
9260
+ if (lb.textAlign === "left") {
9261
+ ctx.textAlign = "left";
9262
+ tx = bx + padX;
9263
+ } else if (lb.textAlign === "right") {
9264
+ ctx.textAlign = "right";
9265
+ tx = bx + w - padX;
9266
+ } else {
9267
+ ctx.textAlign = "center";
9268
+ tx = bx + w / 2;
9269
+ }
9037
9270
  const startY = by + padY + lineH / 2;
9038
- for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], bx + w / 2, startY + i * lineH);
9271
+ for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], tx, startY + i * lineH);
9039
9272
  }
9273
+ return { x: bx, y: by, w, h };
9040
9274
  }
9041
9275
  roundRect(ctx, x, y, w, h, r) {
9042
9276
  const rr = Math.min(r, w / 2, h / 2);
@@ -9274,6 +9508,8 @@ var ChromeRenderer = class {
9274
9508
  this.axisTextColor = DARK_THEME.textColor;
9275
9509
  // Shared Pine-drawing renderer (line/box/label/polyline/linefill); widthCache persists.
9276
9510
  this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
9511
+ // Tooltip hit-rects of every label drawn this frame, in plot coords (rebuilt per render).
9512
+ this.labelTips = [];
9277
9513
  }
9278
9514
  mount(canvas) {
9279
9515
  this.canvas = canvas;
@@ -9315,6 +9551,7 @@ var ChromeRenderer = class {
9315
9551
  const dataW = coords.width;
9316
9552
  const dataH = coords.height;
9317
9553
  this.axisTextColor = surface?.textColor ?? theme.textColor;
9554
+ this.labelTips = [];
9318
9555
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
9319
9556
  ctx.clearRect(0, 0, fullW, fullH);
9320
9557
  if (surface && (fullW > dataW || fullH > dataH)) {
@@ -9424,6 +9661,17 @@ var ChromeRenderer = class {
9424
9661
  (price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
9425
9662
  );
9426
9663
  ctx.restore();
9664
+ for (const r of this.drawScene.labelTipRegions()) {
9665
+ this.labelTips.push({ ...r, top: r.top + pane.bounds.top, bottom: r.bottom + pane.bounds.top });
9666
+ }
9667
+ }
9668
+ /** Tooltip of the topmost label under a plot-space point, or null. Fed by the last render. */
9669
+ labelTooltipAt(x, y) {
9670
+ for (let i = this.labelTips.length - 1; i >= 0; i -= 1) {
9671
+ const r = this.labelTips[i];
9672
+ if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return r.text;
9673
+ }
9674
+ return null;
9427
9675
  }
9428
9676
  // ── axes ──
9429
9677
  drawPriceAxes(ctx, scene, coords, theme, dataW, panes) {
@@ -9640,6 +9888,68 @@ function formatCountdown(ms) {
9640
9888
  return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
9641
9889
  }
9642
9890
 
9891
+ // src/renderers/native/chrome/LabelTooltip.ts
9892
+ var HOVER_DELAY_MS = 350;
9893
+ var LabelTooltip = class {
9894
+ constructor(plot, deps) {
9895
+ this.plot = plot;
9896
+ this.deps = deps;
9897
+ this.tip = null;
9898
+ this.timer = null;
9899
+ /** Text of the currently open OR armed tip — dedupes moves inside one label. */
9900
+ this.current = null;
9901
+ this.onMove = (e) => {
9902
+ if (e.pointerType !== "mouse") return;
9903
+ const rect = this.plot.getBoundingClientRect();
9904
+ const x = e.clientX - rect.left;
9905
+ const y = e.clientY - rect.top;
9906
+ const text = this.deps.lookup(x, y);
9907
+ if (!text) {
9908
+ this.clear();
9909
+ return;
9910
+ }
9911
+ if (text === this.current) return;
9912
+ this.clear();
9913
+ this.current = text;
9914
+ this.timer = window.setTimeout(() => this.show(text, x, y), HOVER_DELAY_MS);
9915
+ };
9916
+ this.onLeave = () => {
9917
+ this.clear();
9918
+ };
9919
+ plot.addEventListener("pointermove", this.onMove);
9920
+ plot.addEventListener("pointerleave", this.onLeave);
9921
+ plot.addEventListener("pointerdown", this.onLeave);
9922
+ }
9923
+ destroy() {
9924
+ this.plot.removeEventListener("pointermove", this.onMove);
9925
+ this.plot.removeEventListener("pointerleave", this.onLeave);
9926
+ this.plot.removeEventListener("pointerdown", this.onLeave);
9927
+ this.clear();
9928
+ }
9929
+ show(text, x, y) {
9930
+ const doc = this.plot.ownerDocument;
9931
+ const tip = doc.createElement("div");
9932
+ tip.textContent = text;
9933
+ 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;";
9934
+ applyChromeTokens(tip, this.deps.theme());
9935
+ this.plot.appendChild(tip);
9936
+ const pw = this.plot.clientWidth;
9937
+ const ph = this.plot.clientHeight;
9938
+ tip.style.left = `${Math.max(0, Math.min(x + 12, pw - tip.offsetWidth - 4))}px`;
9939
+ tip.style.top = `${Math.max(0, Math.min(y + 16, ph - tip.offsetHeight - 4))}px`;
9940
+ this.tip = tip;
9941
+ }
9942
+ clear() {
9943
+ if (this.timer !== null) {
9944
+ clearTimeout(this.timer);
9945
+ this.timer = null;
9946
+ }
9947
+ this.tip?.remove();
9948
+ this.tip = null;
9949
+ this.current = null;
9950
+ }
9951
+ };
9952
+
9643
9953
  // src/renderers/native/chrome/CrosshairRenderer.ts
9644
9954
  var CrosshairRenderer = class {
9645
9955
  constructor() {
@@ -14717,19 +15027,8 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
14717
15027
  for (const model of models) {
14718
15028
  const off = offsetOf(model.id);
14719
15029
  for (const s of model.series) {
14720
- if (s.kind === "candle" || s.kind === "bar") {
14721
- for (let i = i0; i <= i1; i += 1) {
14722
- const b = s.bars[i - off];
14723
- if (b) {
14724
- consider(b.high);
14725
- consider(b.low);
14726
- }
14727
- }
14728
- } else if (isLineLikeSeries(s)) {
14729
- for (let i = i0; i <= i1; i += 1) consider(s.points[i - off]?.value);
14730
- if (s.kind === "histogram" || s.kind === "columns") consider(s.style?.base ?? 0);
14731
- else if (s.style?.base != null) consider(s.style.base);
14732
- }
15030
+ if (s.overlay === true) continue;
15031
+ considerSeries(s, i0, i1, off, consider);
14733
15032
  }
14734
15033
  for (const pl of model.priceLines) consider(pl.price);
14735
15034
  }
@@ -14751,6 +15050,36 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
14751
15050
  const span = max - min;
14752
15051
  return { min: min - span * MARGIN_BOTTOM, max: max + span * MARGIN_TOP };
14753
15052
  }
15053
+ function considerSeries(s, i0, i1, off, consider) {
15054
+ if (s.kind === "candle" || s.kind === "bar") {
15055
+ for (let i = i0; i <= i1; i += 1) {
15056
+ const b = s.bars[i - off];
15057
+ if (b) {
15058
+ consider(b.high);
15059
+ consider(b.low);
15060
+ }
15061
+ }
15062
+ } else if (isLineLikeSeries(s)) {
15063
+ for (let i = i0; i <= i1; i += 1) consider(s.points[i - off]?.value);
15064
+ if (s.kind === "histogram" || s.kind === "columns") consider(s.style?.base ?? 0);
15065
+ else if (s.style?.base != null) consider(s.style.base);
15066
+ }
15067
+ }
15068
+ function overlaySeriesRange(models, i0, i1, offsetOf = () => 0) {
15069
+ let min = Infinity;
15070
+ let max = -Infinity;
15071
+ const consider = (v) => {
15072
+ if (v != null && Number.isFinite(v)) {
15073
+ if (v < min) min = v;
15074
+ if (v > max) max = v;
15075
+ }
15076
+ };
15077
+ for (const model of models) {
15078
+ const off = offsetOf(model.id);
15079
+ for (const s of model.series) if (s.overlay === true) considerSeries(s, i0, i1, off, consider);
15080
+ }
15081
+ return min === Infinity ? null : { min, max };
15082
+ }
14754
15083
  function expandScaleByPixels(scale, heightPx, abovePx, belowPx) {
14755
15084
  if (abovePx <= 0 && belowPx <= 0) return scale;
14756
15085
  const content = heightPx - abovePx - belowPx;
@@ -14805,6 +15134,11 @@ function resizeSplit(split, dyTotal, minPx = MIN_PANE_PX) {
14805
15134
  }
14806
15135
 
14807
15136
  // src/renderers/native/backdrop/BackdropRenderer.ts
15137
+ function clipHighlightRect(x1, x2, left, right) {
15138
+ const x = Math.max(left, x1);
15139
+ const end = Math.min(right, x2);
15140
+ return end > x ? { x, width: end - x } : null;
15141
+ }
14808
15142
  var BackdropRenderer = class {
14809
15143
  constructor() {
14810
15144
  this.canvas = null;
@@ -14837,17 +15171,22 @@ var BackdropRenderer = class {
14837
15171
  /** Renderer-owned session highlight bands: full-height (all panes), behind the grid.
14838
15172
  * Session-zone washes (pre/post-market) paint first, host highlights on top. */
14839
15173
  drawHighlights(ctx, scene, coords) {
14840
- const bands = [...scene.sessionHighlightBands(), ...scene.highlights];
14841
- if (bands.length === 0) return;
15174
+ const sessions = scene.sessionHighlightBands();
15175
+ if (sessions.length > 0) {
15176
+ const left = Math.max(0, coords.logicalToX(-0.5));
15177
+ const right = Math.min(coords.width, coords.logicalToX(coords.barCount - 0.5));
15178
+ this.drawHighlightSet(ctx, sessions, coords, left, right);
15179
+ }
15180
+ this.drawHighlightSet(ctx, scene.highlights, coords, 0, coords.width);
15181
+ }
15182
+ drawHighlightSet(ctx, bands, coords, left, right) {
14842
15183
  for (const band of bands) {
14843
15184
  const x1 = coords.timeToX(band.from);
14844
15185
  const x2 = coords.timeToX(band.to);
14845
- if (x2 < 0 || x1 > coords.width || x2 <= x1) continue;
14846
- const cx = Math.max(0, x1);
14847
- const cw = Math.min(coords.width, x2) - cx;
14848
- if (cw <= 0) continue;
15186
+ const rect = clipHighlightRect(x1, x2, left, right);
15187
+ if (!rect) continue;
14849
15188
  ctx.fillStyle = band.color;
14850
- ctx.fillRect(cx, 0, cw, coords.height);
15189
+ ctx.fillRect(rect.x, 0, rect.width, coords.height);
14851
15190
  }
14852
15191
  }
14853
15192
  // ── grid ── vert/horz gate on `scene.showGrid` AND their own per-axis visibility
@@ -15497,6 +15836,8 @@ var NativeRenderer = class {
15497
15836
  this.glowAmount = 0;
15498
15837
  // WebGL2 neon-glow intensity (canvas2d ignores it)
15499
15838
  this.chrome = new ChromeRenderer();
15839
+ /** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
15840
+ this.labelTooltip = null;
15500
15841
  this.crosshairLayer = new CrosshairRenderer();
15501
15842
  /** 1 Hz repaint pump so the price-axis countdown-to-bar-close ticks; null when off. */
15502
15843
  this.countdownTimer = null;
@@ -15507,6 +15848,8 @@ var NativeRenderer = class {
15507
15848
  this.indicatorValuesOn = true;
15508
15849
  /** Host-contributed legend actions — held here so a rebuild of the legend re-wires them. */
15509
15850
  this.legendActionsProvider = null;
15851
+ /** Host-contributed legend callouts — held here so a rebuild of the legend re-wires them. */
15852
+ this.legendCalloutsProvider = null;
15510
15853
  /** Host override of the legend's fold toggle — held here so a remount re-applies it. */
15511
15854
  this.legendOverviewAction = null;
15512
15855
  // ── keyboard navigation / accessibility (item 11) ──
@@ -16615,6 +16958,10 @@ var NativeRenderer = class {
16615
16958
  this.plot.appendChild(this.scrollButton);
16616
16959
  this.plot.addEventListener("pointermove", this.onScrollProximityMove);
16617
16960
  this.plot.addEventListener("pointerleave", this.onScrollProximityLeave);
16961
+ this.labelTooltip = new LabelTooltip(this.plot, {
16962
+ theme: () => this.chromeTheme(),
16963
+ lookup: (x, y) => this.chrome.labelTooltipAt(x, y)
16964
+ });
16618
16965
  this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
16619
16966
  projector: () => this.drawingProjector(),
16620
16967
  dpr: () => this.coords.dpr,
@@ -16654,9 +17001,10 @@ var NativeRenderer = class {
16654
17001
  this.inputsUI.setDialogHost(this.dialogHost);
16655
17002
  this.inputsUI.setSymbolPicker(this.symbolPicker);
16656
17003
  this.inputsUI.setLegendActions(this.legendActionsProvider);
17004
+ this.inputsUI.setLegendCallouts(this.legendCalloutsProvider);
16657
17005
  this.inputsUI.setLegendOverviewAction(this.legendOverviewAction);
16658
17006
  this.inputsUI.setOnChange((c) => {
16659
- for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value });
17007
+ for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value, ...c.kind ? { kind: c.kind } : {} });
16660
17008
  });
16661
17009
  this.inputsUI.setOnRemove((id) => {
16662
17010
  for (const cb of this.removeIndicatorCbs) cb(id);
@@ -16846,6 +17194,8 @@ var NativeRenderer = class {
16846
17194
  this.settingsButton = null;
16847
17195
  this.plot?.removeEventListener("pointermove", this.onScrollProximityMove);
16848
17196
  this.plot?.removeEventListener("pointerleave", this.onScrollProximityLeave);
17197
+ this.labelTooltip?.destroy();
17198
+ this.labelTooltip = null;
16849
17199
  this.scrollButton?.remove();
16850
17200
  this.scrollButton = null;
16851
17201
  for (const l of this.extLayers) l.instance.destroy?.();
@@ -17086,7 +17436,7 @@ var NativeRenderer = class {
17086
17436
  else this.scene.assignIndicatorZ(model.id);
17087
17437
  this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
17088
17438
  native: !!model.native,
17089
- ...model.shorttitle ? { settingsTitle: model.title } : {}
17439
+ ...model.props ? { props: model.props, propValues: model.propValues ?? {} } : {}
17090
17440
  });
17091
17441
  this.syncTables(model);
17092
17442
  if (model.native?.type === "volume") {
@@ -17137,8 +17487,8 @@ var NativeRenderer = class {
17137
17487
  this.paneControls?.refresh();
17138
17488
  this.scheduler.invalidate(4 /* Full */);
17139
17489
  }
17140
- setIndicatorInputs(handle, values) {
17141
- this.inputsUI.setValues(handle.id, values);
17490
+ setIndicatorInputs(handle, values, props) {
17491
+ this.inputsUI.setValues(handle.id, values, props);
17142
17492
  }
17143
17493
  setSymbolPicker(picker) {
17144
17494
  this.symbolPicker = picker;
@@ -17148,6 +17498,10 @@ var NativeRenderer = class {
17148
17498
  this.legendActionsProvider = provider;
17149
17499
  this.inputsUI?.setLegendActions(provider);
17150
17500
  }
17501
+ setLegendCallouts(provider) {
17502
+ this.legendCalloutsProvider = provider;
17503
+ this.inputsUI?.setLegendCallouts(provider);
17504
+ }
17151
17505
  setLegendOverviewAction(action) {
17152
17506
  this.legendOverviewAction = action;
17153
17507
  this.inputsUI?.setLegendOverviewAction(action);
@@ -18128,7 +18482,11 @@ var NativeRenderer = class {
18128
18482
  }
18129
18483
  const models = this.scene.indicatorsForPane(pane.id);
18130
18484
  const masterModels = models.filter((m) => m.ownScale !== true);
18131
- const dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
18485
+ let dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
18486
+ if (pane === pricePane) {
18487
+ const or = overlaySeriesRange(this.scene.indicators.values(), i0, i1, (id) => this.scene.offsetOf(id));
18488
+ if (or) dr = dr ? { min: Math.min(dr.min, or.min), max: Math.max(dr.max, or.max) } : or;
18489
+ }
18132
18490
  const includeCandles = pane.kind === "price" && !this.scene.candlesHidden;
18133
18491
  pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
18134
18492
  pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
@@ -18402,6 +18760,7 @@ var NativeRenderer = class {
18402
18760
  for (const m of models) {
18403
18761
  const off = this.scene.offsetOf(m.id);
18404
18762
  for (const s of m.series) {
18763
+ if (s.overlay === true) continue;
18405
18764
  if (isLineLikeSeries(s)) {
18406
18765
  const v = s.points[i0 - off]?.value;
18407
18766
  if (v != null && Number.isFinite(v)) return v;
@@ -18782,6 +19141,7 @@ var IndicatorHandleImpl = class {
18782
19141
  constructor(id, title, controller, source) {
18783
19142
  this.controller = controller;
18784
19143
  this.schema = [];
19144
+ this.propsSchema = [];
18785
19145
  this.visibleState = true;
18786
19146
  this.bus = new TypedEventBus();
18787
19147
  this.id = id;
@@ -18791,6 +19151,9 @@ var IndicatorHandleImpl = class {
18791
19151
  get inputs() {
18792
19152
  return this.schema;
18793
19153
  }
19154
+ get props() {
19155
+ return this.propsSchema;
19156
+ }
18794
19157
  get visible() {
18795
19158
  return this.visibleState;
18796
19159
  }
@@ -18800,6 +19163,12 @@ var IndicatorHandleImpl = class {
18800
19163
  setInputs(values) {
18801
19164
  this.controller.applyInputs(this.id, values);
18802
19165
  }
19166
+ setProp(key, value) {
19167
+ this.controller.applyProps(this.id, { [key]: value });
19168
+ }
19169
+ setProps(values) {
19170
+ this.controller.applyProps(this.id, values);
19171
+ }
18803
19172
  setVisible(visible) {
18804
19173
  this.controller.setVisible(this.id, visible);
18805
19174
  }
@@ -18819,6 +19188,9 @@ var IndicatorHandleImpl = class {
18819
19188
  setSchema(schema) {
18820
19189
  this.schema = schema;
18821
19190
  }
19191
+ setPropsSchema(schema) {
19192
+ this.propsSchema = schema;
19193
+ }
18822
19194
  /** Sync the public `visible` getter when the orchestrator changes visibility (API or legend eye). */
18823
19195
  setVisibleState(visible) {
18824
19196
  this.visibleState = visible;
@@ -18832,6 +19204,8 @@ var IndicatorHandleImpl = class {
18832
19204
  function summarizeModel(model) {
18833
19205
  const series = {};
18834
19206
  for (const s of model.series) series[s.kind] = (series[s.kind] ?? 0) + 1;
19207
+ const countOverlay = (items) => items?.filter((i) => i.overlay === true).length ?? 0;
19208
+ 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);
18835
19209
  return {
18836
19210
  id: model.id,
18837
19211
  title: model.title,
@@ -18851,7 +19225,8 @@ function summarizeModel(model) {
18851
19225
  tables: model.tables?.length ?? 0,
18852
19226
  barColors: model.barColors?.length ?? 0,
18853
19227
  trades: model.trades?.length ?? 0,
18854
- inputs: model.inputs.length
19228
+ inputs: model.inputs.length,
19229
+ forcedOverlay
18855
19230
  };
18856
19231
  }
18857
19232
  function inspectModels(models) {
@@ -18872,7 +19247,8 @@ function inspectModels(models) {
18872
19247
  linefills: sum((s) => s.linefills),
18873
19248
  tables: sum((s) => s.tables),
18874
19249
  barColors: sum((s) => s.barColors),
18875
- trades: sum((s) => s.trades)
19250
+ trades: sum((s) => s.trades),
19251
+ forcedOverlay: sum((s) => s.forcedOverlay)
18876
19252
  }
18877
19253
  };
18878
19254
  }
@@ -19016,7 +19392,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
19016
19392
  for (const engine of engines) this.registerEngine(engine.language, engine);
19017
19393
  this.defaultLanguage = config.defaultLanguage;
19018
19394
  this.renderer.mount(container, config.theme);
19019
- this.renderer.onInputChange((e) => this.applyInputs(e.indicatorId, { [e.key]: e.value }));
19395
+ this.renderer.onInputChange(
19396
+ (e) => e.kind === "prop" ? this.applyProps(e.indicatorId, { [e.key]: e.value }) : this.applyInputs(e.indicatorId, { [e.key]: e.value })
19397
+ );
19020
19398
  this.renderer.onRemoveIndicator((id) => this.removeIndicator(id));
19021
19399
  this.renderer.onToggleIndicatorVisible?.((id, visible) => this.setVisible(id, visible));
19022
19400
  this.paneActionUnsub = this.renderer.onPaneAction?.((a) => this.handlePaneAction(a)) ?? null;
@@ -19696,7 +20074,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19696
20074
  const title = options.title ?? "Indicator";
19697
20075
  const handle = new IndicatorHandleImpl(id, title, this, source);
19698
20076
  this.handles.set(id, handle);
19699
- this.registry.add({ id, title, source, options, inputValues: { ...options.inputs ?? {} } });
20077
+ this.registry.add({ id, title, source, options, inputValues: { ...options.inputs ?? {} }, propValues: { ...options.props ?? {} } });
19700
20078
  void this.startIndicator(id, source, options, handle);
19701
20079
  return handle;
19702
20080
  }
@@ -19719,7 +20097,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19719
20097
  return handle;
19720
20098
  }
19721
20099
  const inputValues = { ...descriptor.defaultInputs(), ...options.inputs ?? {} };
19722
- this.registry.add({ id, title, source: type, inputValues, native: { type, instance: descriptor.create(), descriptor } });
20100
+ this.registry.add({ id, title, source: type, inputValues, propValues: {}, native: { type, instance: descriptor.create(), descriptor } });
19723
20101
  handle.setSchema(descriptor.inputsSchema());
19724
20102
  void this.startNativeIndicator(id, handle);
19725
20103
  return handle;
@@ -19772,6 +20150,19 @@ var EngineOrchestrator = class _EngineOrchestrator {
19772
20150
  if (record.session) record.session.update(record.inputValues);
19773
20151
  else if (record.native && !record.hidden) record.native.instance.setInputs(record.inputValues);
19774
20152
  }
20153
+ /** IndicatorController: re-run an indicator with merged declaration-prop overrides.
20154
+ * Same lifecycle as {@link applyInputs} — a prop change replays the whole script.
20155
+ * Script indicators only: natives have no declaration props. */
20156
+ applyProps(id, values) {
20157
+ const record = this.registry.get(id);
20158
+ if (!record?.session) return;
20159
+ record.propValues = { ...record.propValues, ...values };
20160
+ if (record.renderHandle) this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
20161
+ record.pendingStructural = true;
20162
+ if (!record.hidden) this.setLoading(record, true);
20163
+ record.pendingCause = "inputs";
20164
+ record.session.update(record.inputValues, record.propValues);
20165
+ }
19775
20166
  /** IndicatorController: tear down an indicator and (if now empty) its pane. */
19776
20167
  /** Live handles of every indicator on the chart (script + native), insertion order. */
19777
20168
  listIndicators() {
@@ -19920,6 +20311,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
19920
20311
  for (const input of prepared.inputs) defaults[input.key] = input.defval;
19921
20312
  record.inputValues = { ...defaults, ...record.inputValues };
19922
20313
  handle.setSchema(prepared.inputs);
20314
+ const propDefaults = {};
20315
+ for (const prop of prepared.props ?? []) propDefaults[prop.key] = prop.defval;
20316
+ record.propValues = { ...propDefaults, ...record.propValues };
20317
+ handle.setPropsSchema(prepared.props ?? []);
19923
20318
  this.mountLoadingPlaceholder(id, record);
19924
20319
  this.executeIndicator(id, handle);
19925
20320
  } catch (err) {
@@ -19943,6 +20338,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19943
20338
  getBars: () => this.bars,
19944
20339
  fetchSeries: (sym, tf, range) => this.fetchSeries(sym, tf, range),
19945
20340
  inputs: record.inputValues,
20341
+ props: record.propValues,
19946
20342
  visibleRange: this.currentVisibleRange(),
19947
20343
  mode,
19948
20344
  // Mid-backfill session starts (add / re-show / price-style re-execute) let the
@@ -20036,6 +20432,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
20036
20432
  const meta = record.prepared.meta;
20037
20433
  const model = {
20038
20434
  id,
20435
+ // Deliberately the FULL title, never a shorttitle: while the script loads the
20436
+ // legend identifies it by its full name; the compact shorttitle arrives with
20437
+ // the first computed model and takes over from there.
20039
20438
  title: record.options?.title ?? meta.title,
20040
20439
  overlay: meta.overlay,
20041
20440
  paneHint: meta.overlay ? "price" : "new",
@@ -20044,7 +20443,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
20044
20443
  backgrounds: [],
20045
20444
  priceLines: [],
20046
20445
  inputs: record.prepared.inputs,
20047
- inputValues: record.inputValues
20446
+ inputValues: record.inputValues,
20447
+ ...record.prepared.props ? { props: record.prepared.props, propValues: record.propValues } : {}
20048
20448
  };
20049
20449
  const paneId = this.routePane(id, model, record.options ?? {});
20050
20450
  this.placeModel(model, id, paneId);
@@ -20277,7 +20677,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
20277
20677
  record.model = model;
20278
20678
  if (record.pendingStructural) {
20279
20679
  record.renderHandle = this.renderer.mountIndicator(model);
20280
- this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues);
20680
+ this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
20281
20681
  record.pendingStructural = false;
20282
20682
  } else {
20283
20683
  this.renderer.updateIndicator(record.renderHandle, modelToValuePatch(model));
@@ -20304,16 +20704,17 @@ var EngineOrchestrator = class _EngineOrchestrator {
20304
20704
  placeModel(model, id, paneId) {
20305
20705
  model.id = id;
20306
20706
  model.paneId = paneId;
20307
- for (const series of model.series) series.paneId = paneId;
20308
- for (const fill of model.fills) fill.paneId = paneId;
20309
- for (const bg of model.backgrounds) bg.paneId = paneId;
20707
+ const paneOf = (item) => item.overlay === true ? "price" : paneId;
20708
+ for (const series of model.series) series.paneId = paneOf(series);
20709
+ for (const fill of model.fills) fill.paneId = paneOf(fill);
20710
+ for (const bg of model.backgrounds) bg.paneId = paneOf(bg);
20310
20711
  for (const line of model.priceLines) line.paneId = paneId;
20311
- if (model.lines) for (const ln of model.lines) ln.paneId = paneId;
20312
- if (model.boxes) for (const bx of model.boxes) bx.paneId = paneId;
20313
- if (model.labels) for (const lb of model.labels) lb.paneId = paneId;
20314
- if (model.polylines) for (const pl of model.polylines) pl.paneId = paneId;
20315
- if (model.linefills) for (const lf of model.linefills) lf.paneId = paneId;
20316
- if (model.tables) for (const tb of model.tables) tb.paneId = paneId;
20712
+ if (model.lines) for (const ln of model.lines) ln.paneId = paneOf(ln);
20713
+ if (model.boxes) for (const bx of model.boxes) bx.paneId = paneOf(bx);
20714
+ if (model.labels) for (const lb of model.labels) lb.paneId = paneOf(lb);
20715
+ if (model.polylines) for (const pl of model.polylines) pl.paneId = paneOf(pl);
20716
+ if (model.linefills) for (const lf of model.linefills) lf.paneId = paneOf(lf);
20717
+ if (model.tables) for (const tb of model.tables) tb.paneId = paneOf(tb);
20317
20718
  }
20318
20719
  emitContextChanged(id) {
20319
20720
  if (!this.registry.get(id)?.session?.getContext) return;
@@ -20973,4 +21374,4 @@ function resolveElement(container) {
20973
21374
  return element;
20974
21375
  }
20975
21376
 
20976
- export { BUILTIN_PRICE_STYLES, BarStore, CachingDataFeed, DARK_THEME, DataControl, DrawingStore, DrawingToolbar, DrawingsControl, LIGHT_THEME, MultiProviderFeed, NativeRenderer, RendererControl, TIMEZONES, TypedEventBus, Vela, applyAttributionMarkTheme, buildToolbar, createAttributionMark, createCustomMark, defaultToolbar, normalizeTimezone, parseSymbol, priceStyleIds, registerBuiltinChartTypes, resolveTheme, sharedBarStore, timeframeToMs, tzButtonLabel, tzMenuLabel, tzOffset };
21377
+ export { BUILTIN_PRICE_STYLES, BarStore, CachingDataFeed, DARK_THEME, DataControl, DrawingStore, DrawingToolbar, DrawingsControl, LEGEND_AT_TOP_ATTR, LIGHT_THEME, MultiProviderFeed, NativeRenderer, RendererControl, TIMEZONES, TypedEventBus, Vela, applyAttributionMarkTheme, buildToolbar, createAttributionMark, createCustomMark, defaultToolbar, normalizeTimezone, parseSymbol, priceStyleIds, registerBuiltinChartTypes, resolveTheme, sharedBarStore, timeframeToMs, tzButtonLabel, tzMenuLabel, tzOffset };