@luxalgo/vela 0.6.8 → 0.6.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/{DataProvider-BsQM2WNH.d.cts → DataProvider-8Z95Q-RJ.d.cts} +1 -1
  2. package/dist/{DataProvider-BSLlBpB9.d.ts → DataProvider-DExJrfut.d.ts} +1 -1
  3. package/dist/{chunk-2R7BANEM.js → chunk-62SVGONC.js} +2 -2
  4. package/dist/{chunk-SQETIBFU.js → chunk-6WDDVMBJ.js} +502 -139
  5. package/dist/{chunk-L3I2CCYO.js → chunk-EQCHJZOT.js} +49 -2
  6. package/dist/{chunk-OICPLNU7.js → chunk-FCVIG7JG.js} +188 -2
  7. package/dist/{chunk-N7LGMKCE.js → chunk-STHSKXOR.js} +107 -84
  8. package/dist/{chunk-PN5KWFZ4.js → chunk-WFSZBX3R.js} +2 -2
  9. package/dist/{contributions-Vw-Hm58R.d.cts → contributions-C1U2Krwg.d.cts} +99 -7
  10. package/dist/{contributions-DLLdV9jD.d.ts → contributions-D7PVZO2i.d.ts} +99 -7
  11. package/dist/index.cjs +709 -144
  12. package/dist/index.d.cts +12 -7
  13. package/dist/index.d.ts +12 -7
  14. package/dist/index.js +4 -4
  15. package/dist/{options-BaVTMXaO.d.ts → options-FM0peknS.d.cts} +84 -7
  16. package/dist/{options-BaVTMXaO.d.cts → options-FM0peknS.d.ts} +84 -7
  17. package/dist/{plugin-CC7rBrOY.d.ts → plugin-7bkF32Rk.d.ts} +3 -3
  18. package/dist/{plugin-BnJgjLAy.d.cts → plugin-DfVqBz9p.d.cts} +3 -3
  19. package/dist/plugin.cjs +18 -2
  20. package/dist/plugin.d.cts +4 -4
  21. package/dist/plugin.d.ts +4 -4
  22. package/dist/plugin.js +2 -2
  23. package/dist/providers/binance.d.cts +2 -2
  24. package/dist/providers/binance.d.ts +2 -2
  25. package/dist/providers/coinbase.d.cts +2 -2
  26. package/dist/providers/coinbase.d.ts +2 -2
  27. package/dist/providers/hyperliquid.d.cts +2 -2
  28. package/dist/providers/hyperliquid.d.ts +2 -2
  29. package/dist/{statusline-DqzBqy42.d.cts → statusline-DOPiT6I6.d.cts} +5 -3
  30. package/dist/{statusline-CGkB2EOo.d.ts → statusline-zdF4eZLr.d.ts} +5 -3
  31. package/dist/ui.cjs +201 -4
  32. package/dist/ui.d.cts +89 -2
  33. package/dist/ui.d.ts +89 -2
  34. package/dist/ui.js +3 -3
  35. package/dist/vela.global.js +709 -144
  36. package/dist/vela.global.min.js +93 -44
  37. package/dist/widget.cjs +798 -188
  38. package/dist/widget.d.cts +6 -6
  39. package/dist/widget.d.ts +6 -6
  40. package/dist/widget.js +7 -7
  41. package/dist/workspace.cjs +798 -188
  42. package/dist/workspace.d.cts +16 -6
  43. package/dist/workspace.d.ts +16 -6
  44. package/dist/workspace.js +6 -6
  45. package/package.json +1 -1
@@ -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,
@@ -2973,6 +2988,16 @@ var IndicatorInputsDialog = class {
2973
2988
  text: () => "Close"
2974
2989
  }));
2975
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
+ }
2976
3001
  const tabs = document.createElement("div");
2977
3002
  tabs.style.cssText = `display:flex;gap:12px;padding:0 20px;border-bottom:1px solid ${border};flex:0 0 auto;`;
2978
3003
  const tabEls = [];
@@ -3075,9 +3100,10 @@ var IndicatorInputsDialog = class {
3075
3100
  this.openId = null;
3076
3101
  this.row = null;
3077
3102
  this.snapshot = null;
3103
+ this.propSnapshot = null;
3078
3104
  ui?.destroy();
3079
3105
  }
3080
- /** 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. */
3081
3107
  revertAndClose() {
3082
3108
  const row = this.row;
3083
3109
  const snap = this.snapshot;
@@ -3091,6 +3117,17 @@ var IndicatorInputsDialog = class {
3091
3117
  }
3092
3118
  }
3093
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
+ }
3094
3131
  this.close();
3095
3132
  }
3096
3133
  /** The footer's reset button — same chip as Cancel, pinned to the LEFT edge
@@ -3108,19 +3145,27 @@ var IndicatorInputsDialog = class {
3108
3145
  const row = this.row;
3109
3146
  if (!row) return;
3110
3147
  const snap = this.snapshot;
3148
+ const propSnap = this.propSnapshot;
3111
3149
  for (const inp of row.inputs) {
3112
3150
  if (row.values[inp.key] !== inp.defval) {
3113
3151
  row.values[inp.key] = inp.defval;
3114
3152
  this.host.onChange?.({ indicatorId: row.id, key: inp.key, value: inp.defval });
3115
3153
  }
3116
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
+ }
3117
3161
  this.open(row);
3118
3162
  if (snap) this.snapshot = snap;
3163
+ if (propSnap) this.propSnapshot = propSnap;
3119
3164
  }
3120
3165
  /** Write one edit through: store it, notify the host, and re-apply the `when` gates. */
3121
- commit(row, key, value) {
3122
- row.values[key] = value;
3123
- 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" } : {} });
3124
3169
  this.refreshVisibility?.();
3125
3170
  }
3126
3171
  /** One settings row (or several `inline=` inputs) placed into a section's grid. */
@@ -3147,7 +3192,7 @@ var IndicatorInputsDialog = class {
3147
3192
  const id = idOf(lead);
3148
3193
  const info = lead.tooltip ? this.infoButton(lead.tooltip) : void 0;
3149
3194
  if (lead.type === "bool") {
3150
- const current = Boolean(row.values[lead.key] ?? lead.defval);
3195
+ const current = Boolean(bagOf(row, lead)[lead.key] ?? lead.defval);
3151
3196
  return append(fieldRow({
3152
3197
  label: nameOf(lead),
3153
3198
  id,
@@ -3156,7 +3201,7 @@ var IndicatorInputsDialog = class {
3156
3201
  toggle: {
3157
3202
  id,
3158
3203
  checked: current,
3159
- onChange: (v) => this.commit(row, lead.key, v)
3204
+ onChange: (v) => this.commit(row, lead, v)
3160
3205
  }
3161
3206
  }));
3162
3207
  }
@@ -3181,8 +3226,8 @@ var IndicatorInputsDialog = class {
3181
3226
  }
3182
3227
  /** Build the typed control for one input, committing edits live via `onChange`. */
3183
3228
  buildControl(row, inp, id) {
3184
- const current = row.values[inp.key] ?? inp.defval;
3185
- 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);
3186
3231
  if (inp.type === "bool") {
3187
3232
  return buildFieldControl({ kind: "switch", id, checked: Boolean(current), onChange: (v) => emit(v) }).el;
3188
3233
  }
@@ -3193,7 +3238,7 @@ var IndicatorInputsDialog = class {
3193
3238
  kind: "color",
3194
3239
  id,
3195
3240
  theme: this.host.theme(),
3196
- get: () => String(row.values[inp.key] ?? inp.defval),
3241
+ get: () => String(bagOf(row, inp)[inp.key] ?? inp.defval),
3197
3242
  onChange: (v) => emit(v)
3198
3243
  }).el;
3199
3244
  }
@@ -3756,6 +3801,7 @@ function timeParts(ts) {
3756
3801
  }
3757
3802
 
3758
3803
  // src/renderers/shared/InputsUI.ts
3804
+ var LEGEND_AT_TOP_ATTR = "data-vela-pane-at-top";
3759
3805
  var STATUS_KEYFRAMES = "@keyframes vela-ind-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.3;transform:scale(.6)}}";
3760
3806
  var LEGEND_ICON_PX2 = 16;
3761
3807
  var LEGEND_CTL_PX = 18;
@@ -3794,10 +3840,14 @@ var InputsUI = class {
3794
3840
  this.moveApi = null;
3795
3841
  /** Host-contributed legend actions, resolved PER ROW at render time (see setLegendActions). */
3796
3842
  this.legendActions = null;
3843
+ /** Host-contributed callout bubbles, resolved PER ROW at render time (see setLegendCallouts). */
3844
+ this.legendCallouts = null;
3797
3845
  /** Chrome-tooltip disposers, per row id — a tip open at removal must not outlive its row. */
3798
3846
  this.rowTips = /* @__PURE__ */ new Map();
3799
3847
  /** Same, for the contributed extras only (rebuilt independently by setLegendActions). */
3800
3848
  this.extrasTips = /* @__PURE__ */ new Map();
3849
+ /** Same, for the contributed callouts only (rebuilt independently by setLegendCallouts). */
3850
+ this.calloutTips = /* @__PURE__ */ new Map();
3801
3851
  /** Open "Move to" menu (kept so it can be torn down). */
3802
3852
  this.moveMenu = null;
3803
3853
  this.moveTargets = [];
@@ -3955,6 +4005,42 @@ var InputsUI = class {
3955
4005
  extrasEl.appendChild(btn2);
3956
4006
  }
3957
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
+ }
3958
4044
  /** Reposition the per-pane legend containers after a layout change. */
3959
4045
  reposition() {
3960
4046
  for (const [paneId, lg] of this.legends) this.positionLegend(lg, paneId);
@@ -4122,6 +4208,7 @@ var InputsUI = class {
4122
4208
  }
4123
4209
  positionLegend(lg, paneId) {
4124
4210
  const bounds = this.paneBoundsOf ? this.paneBoundsOf(paneId) : { top: 0, height: Infinity };
4211
+ lg.toggleAttribute(LEGEND_AT_TOP_ATTR, bounds.top === 0);
4125
4212
  lg.style.display = bounds.height < 4 || !this.titlesVisible ? "none" : "flex";
4126
4213
  const collapsed = this.paneCollapse.has(paneId);
4127
4214
  const masterId = this.paneCollapse.get(paneId) ?? null;
@@ -4197,13 +4284,13 @@ var InputsUI = class {
4197
4284
  }
4198
4285
  /** Create or update an indicator's legend row (in the legend for its pane). */
4199
4286
  upsert(id, title, inputs, values, paneId = "price", opts = {}) {
4200
- const settingsTitle = opts.settingsTitle ?? title;
4201
4287
  const existing = this.rows.get(id);
4202
4288
  if (existing) {
4203
4289
  existing.title = title;
4204
- existing.settingsTitle = settingsTitle;
4205
4290
  existing.inputs = inputs;
4206
4291
  existing.values = { ...values };
4292
+ existing.props = opts.props ?? [];
4293
+ existing.propValues = { ...opts.propValues ?? {} };
4207
4294
  existing.titleEl.textContent = title;
4208
4295
  if (existing.paneId !== paneId) {
4209
4296
  existing.paneId = paneId;
@@ -4255,6 +4342,9 @@ var InputsUI = class {
4255
4342
  titleWrap.appendChild(beta);
4256
4343
  }
4257
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);
4258
4348
  el.appendChild(statusEl);
4259
4349
  const valuesEl = document.createElement("span");
4260
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;`;
@@ -4271,13 +4361,13 @@ var InputsUI = class {
4271
4361
  eye.className = "vela-ind-ctl";
4272
4362
  eye.style.cssText = LEGEND_CTL_CSS;
4273
4363
  eye.addEventListener("click", () => {
4274
- const row = this.rows.get(id);
4275
- this.onToggleVisible?.(id, Boolean(row?.hidden));
4364
+ const row2 = this.rows.get(id);
4365
+ this.onToggleVisible?.(id, Boolean(row2?.hidden));
4276
4366
  });
4277
4367
  controlsEl.appendChild(eye);
4278
4368
  eyeEl = eye;
4279
4369
  }
4280
- if (inputs.length > 0) {
4370
+ if (inputs.length > 0 || (opts.props ?? []).length > 0) {
4281
4371
  const gear = document.createElement("button");
4282
4372
  gear.type = "button";
4283
4373
  gear.setAttribute("aria-label", "Settings");
@@ -4317,7 +4407,9 @@ var InputsUI = class {
4317
4407
  controlsEl.appendChild(close);
4318
4408
  el.appendChild(controlsEl);
4319
4409
  this.attach(this.legendFor(paneId), el, !!opts.native);
4320
- 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);
4321
4413
  this.syncFoldToggle();
4322
4414
  }
4323
4415
  /** Place a row in its pane's legend — native rows PREPEND (pinned to the top), Pine rows append. */
@@ -4326,9 +4418,11 @@ var InputsUI = class {
4326
4418
  else container.appendChild(el);
4327
4419
  }
4328
4420
  /** Reflect programmatic input changes (so a re-opened dialog shows current values). */
4329
- setValues(id, values) {
4421
+ setValues(id, values, props) {
4330
4422
  const row = this.rows.get(id);
4331
- 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 };
4332
4426
  }
4333
4427
  /**
4334
4428
  * Reflect an indicator's live status in its legend row: `'loading'` shows three pulsing
@@ -4410,8 +4504,13 @@ var InputsUI = class {
4410
4504
  syncRowActions(row) {
4411
4505
  const open = row.highlighted;
4412
4506
  row.controlsEl.style.display = open || row.hidden ? "inline-flex" : "none";
4413
- if (open) row.el.appendChild(row.statusEl);
4414
- 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
+ }
4415
4514
  for (const child of Array.from(row.controlsEl.children)) {
4416
4515
  if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
4417
4516
  if (child === row.extrasEl) {
@@ -4429,10 +4528,12 @@ var InputsUI = class {
4429
4528
  }
4430
4529
  remove(id) {
4431
4530
  const row = this.rows.get(id);
4531
+ for (const bubble of row?.callouts ?? []) bubble.destroy();
4432
4532
  row?.el.remove();
4433
4533
  this.rows.delete(id);
4434
4534
  this.disposeTips(this.rowTips, id);
4435
4535
  this.disposeTips(this.extrasTips, id);
4536
+ this.disposeTips(this.calloutTips, id);
4436
4537
  if (this.selectedId === id) this.selectedId = null;
4437
4538
  if (this.inputsDialog.openId === id) this.inputsDialog.close();
4438
4539
  this.syncFoldToggle();
@@ -4452,6 +4553,8 @@ var InputsUI = class {
4452
4553
  this.rowMenu = null;
4453
4554
  for (const id of [...this.rowTips.keys()]) this.disposeTips(this.rowTips, id);
4454
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();
4455
4558
  for (const lg of this.legends.values()) lg.remove();
4456
4559
  this.legends.clear();
4457
4560
  this.rows.clear();
@@ -4806,6 +4909,32 @@ var SIZE_PX = {
4806
4909
  large: 16,
4807
4910
  huge: 20
4808
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
+ }
4809
4938
  var TableOverlay = class {
4810
4939
  constructor(container, theme, paneBounds) {
4811
4940
  this.container = container;
@@ -4826,7 +4955,9 @@ var TableOverlay = class {
4826
4955
  update(tables) {
4827
4956
  this.lastTables = tables;
4828
4957
  this.root.replaceChildren();
4829
- 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
+ }
4830
4961
  }
4831
4962
  /** Re-render at the current pane geometry — after layout settles or on resize. */
4832
4963
  reposition() {
@@ -4841,39 +4972,36 @@ var TableOverlay = class {
4841
4972
  this.root.remove();
4842
4973
  }
4843
4974
  renderTable(t) {
4975
+ const b = this.paneBounds(t.paneId);
4844
4976
  const wrap = document.createElement("div");
4845
4977
  wrap.style.position = "absolute";
4846
- 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);
4847
4980
  const table = document.createElement("table");
4848
4981
  Object.assign(table.style, {
4849
4982
  borderCollapse: "collapse",
4850
4983
  background: t.bgColor ?? "transparent",
4851
4984
  fontFamily: this.theme.fontFamily || "sans-serif",
4852
- // Frame as an inset shadow (not a `border`) so it stays independent of the
4853
- // collapsed cell borders even when frame_width != border_width.
4854
- boxShadow: t.frameColor && t.frameWidth > 0 ? `inset 0 0 0 ${t.frameWidth}px ${t.frameColor}` : "none",
4855
4985
  border: "none",
4856
4986
  tableLayout: "auto",
4857
4987
  // Re-enable pointer events on the table only (root is none) so cell tooltips work.
4858
4988
  pointerEvents: "auto"
4859
4989
  });
4860
- const span = /* @__PURE__ */ new Map();
4861
- const skip = /* @__PURE__ */ new Set();
4862
- for (const m of t.merges) {
4863
- span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
4864
- for (let r = m.startRow; r <= m.endRow; r += 1) {
4865
- for (let c = m.startCol; c <= m.endCol; c += 1) {
4866
- if (r !== m.startRow || c !== m.startCol) skip.add(`${r}:${c}`);
4867
- }
4868
- }
4869
- }
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;
4870
4993
  const cellBorder = t.borderColor && t.borderWidth > 0 ? `${t.borderWidth}px solid ${t.borderColor}` : "none";
4871
4994
  for (let r = 0; r < t.rows; r += 1) {
4872
4995
  const tr = document.createElement("tr");
4873
4996
  for (let c = 0; c < t.columns; c += 1) {
4997
+ if (omit.has(`${r}:${c}`)) continue;
4874
4998
  const cell = t.cells[r]?.[c] ?? null;
4875
- if (skip.has(`${r}:${c}`) || cell?.merged) continue;
4876
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
+ }
4877
5005
  const sp = span.get(`${r}:${c}`);
4878
5006
  if (sp) {
4879
5007
  if (sp.cs > 1) td.colSpan = sp.cs;
@@ -4882,18 +5010,22 @@ var TableOverlay = class {
4882
5010
  Object.assign(td.style, {
4883
5011
  border: cellBorder,
4884
5012
  padding: "2px 6px",
4885
- background: cell?.bgColor ?? "transparent",
4886
- color: cell?.textColor ?? this.theme.textColor,
4887
- textAlign: cell?.hAlign ?? "center",
4888
- verticalAlign: cell?.vAlign === "top" ? "top" : cell?.vAlign === "bottom" ? "bottom" : "middle",
4889
- fontSize: `${SIZE_PX[cell?.textSize ?? "normal"]}px`,
4890
- fontFamily: cell?.fontFamily === "monospace" ? "monospace" : "inherit",
4891
- fontWeight: cell?.bold ? "bold" : "normal",
4892
- fontStyle: cell?.italic ? "italic" : "normal",
4893
- 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"
4894
5024
  });
4895
- if (cell?.tooltip) td.title = cell.tooltip;
4896
- 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 ?? "";
4897
5029
  tr.appendChild(td);
4898
5030
  }
4899
5031
  table.appendChild(tr);
@@ -5778,8 +5910,14 @@ var WebGL2Backend = class {
5778
5910
  return sc === pane.scale ? pane : { ...pane, scale: sc };
5779
5911
  };
5780
5912
  b.alpha = this.modelAlpha;
5781
- for (const m of models) for (const bgSpan of m.backgrounds) this.emitBackground(b, bgSpan, pane, coords);
5782
- 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
+ }
5783
5921
  const drawCandles = isPrice && !scene.candlesHidden;
5784
5922
  let candleDrawn = false;
5785
5923
  for (const m of models) {
@@ -5793,7 +5931,7 @@ var WebGL2Backend = class {
5793
5931
  b.alpha = this.modelAlpha;
5794
5932
  const off = scene.offsetOf(m.id);
5795
5933
  const mp = effPane(m);
5796
- 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);
5797
5935
  }
5798
5936
  if (drawCandles && !candleDrawn) {
5799
5937
  drawSlicesUpTo(scene.candleZ);
@@ -5802,6 +5940,12 @@ var WebGL2Backend = class {
5802
5940
  }
5803
5941
  drawSlicesUpTo(Infinity);
5804
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
+ }
5805
5949
  for (const m of models) {
5806
5950
  const mp = effPane(m);
5807
5951
  for (const pl of m.priceLines) this.emitHline(b, pl, mp, coords, dataW, theme);
@@ -5848,7 +5992,7 @@ var WebGL2Backend = class {
5848
5992
  for (const pane of scene.orderedPanes()) {
5849
5993
  const b = this.glowBatch;
5850
5994
  b.reset();
5851
- this.emitGlowSources(b, scene.indicatorsForPane(pane.id), pane, coords, i0, i1, (id) => scene.offsetOf(id));
5995
+ this.emitGlowSources(b, scene, pane, coords, i0, i1);
5852
5996
  if (b.vertexCount === 0) continue;
5853
5997
  const topH = Math.round(pane.bounds.top * dpr * 0.5);
5854
5998
  const botH = Math.round((pane.bounds.top + pane.bounds.height) * dpr * 0.5);
@@ -5917,15 +6061,23 @@ var WebGL2Backend = class {
5917
6061
  gl.bindVertexArray(null);
5918
6062
  return true;
5919
6063
  }
5920
- /** The "neon" elements that glow: line/area/step lines + point markers (not candles/fills/bars). */
5921
- emitGlowSources(b, models, pane, coords, i0, i1, offsetOf = () => 0) {
5922
- for (const m of models) {
5923
- const off = offsetOf(m.id);
5924
- for (const s of m.series) {
5925
- if (!isLineLikeSeries(s) || s.visible === false) continue;
5926
- if (s.kind === "histogram" || s.kind === "columns") continue;
5927
- if (s.kind === "circles" || s.kind === "cross") this.emitPointMarkers(b, s, pane, coords, i0, i1, off);
5928
- 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);
5929
6081
  }
5930
6082
  }
5931
6083
  }
@@ -6208,9 +6360,9 @@ var WebGL2Backend = class {
6208
6360
  b.alpha = this.candleBodyAlpha;
6209
6361
  b.rect(g.bodyX, bodyTop, g.bodyW, bodyH, c);
6210
6362
  }
6211
- if (cs.borderVisible || bc || fading && cs.bodyVisible) {
6363
+ if (cs.borderVisible || fading && cs.bodyVisible) {
6212
6364
  b.alpha = this.candleStructureAlpha;
6213
- 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;
6214
6366
  const bw = Math.max(0, g.bodyW - 1);
6215
6367
  const bh = Math.max(0, bodyH - 1);
6216
6368
  b.rectStroke(g.bodyX + 0.5, bodyTop + 0.5, bw, bh, 1, bord);
@@ -7762,8 +7914,14 @@ var Canvas2dBackend = class {
7762
7914
  return sc === pane.scale ? pane : { ...pane, scale: sc };
7763
7915
  };
7764
7916
  ctx.globalAlpha = this.modelAlpha;
7765
- for (const m of models) for (const bg of m.backgrounds) this.drawBackground(ctx, bg, pane, coords);
7766
- 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
+ }
7767
7925
  const slices = scene.drawingSlices.get(pane.id) ?? [];
7768
7926
  let si = 0;
7769
7927
  const drawSlicesUpTo = (z) => {
@@ -7785,7 +7943,7 @@ var Canvas2dBackend = class {
7785
7943
  ctx.globalAlpha = this.modelAlpha;
7786
7944
  const off = scene.offsetOf(m.id);
7787
7945
  const mp = effPane(m);
7788
- 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);
7789
7947
  }
7790
7948
  if (drawCandles && !candleDrawn) {
7791
7949
  drawSlicesUpTo(scene.candleZ);
@@ -7793,6 +7951,13 @@ var Canvas2dBackend = class {
7793
7951
  this.drawPriceSeries(ctx, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
7794
7952
  }
7795
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
+ }
7796
7961
  ctx.globalAlpha = this.modelAlpha;
7797
7962
  for (const m of models) {
7798
7963
  const mp = effPane(m);
@@ -8064,9 +8229,9 @@ var Canvas2dBackend = class {
8064
8229
  ctx.fillStyle = color;
8065
8230
  ctx.fillRect(g.bodyX, top, g.bodyW, bodyH);
8066
8231
  }
8067
- if (cs.borderVisible || bc || fading && cs.bodyVisible) {
8232
+ if (cs.borderVisible || fading && cs.bodyVisible) {
8068
8233
  ctx.globalAlpha = this.candleStructureAlpha;
8069
- ctx.strokeStyle = cs.borderVisible || bc ? (up ? cs.borderUpColor : cs.borderDownColor) ?? dir : color;
8234
+ ctx.strokeStyle = cs.borderVisible ? (up ? cs.borderUpColor : cs.borderDownColor) ?? color : color;
8070
8235
  ctx.lineWidth = 1;
8071
8236
  const bw = Math.max(0, g.bodyW - 1);
8072
8237
  const bh = Math.max(0, bodyH - 1);
@@ -8498,6 +8663,8 @@ var DrawingSceneRenderer = class {
8498
8663
  this.set = set;
8499
8664
  /** measureText width cache, keyed by `${font} ${text}`, persists across frames. */
8500
8665
  this.widthCache = /* @__PURE__ */ new Map();
8666
+ /** Tooltip hit-rects captured while drawing the CURRENT set (rebuilt per render). */
8667
+ this.tipRegions = [];
8501
8668
  /**
8502
8669
  * Index offset of the CURRENT set's model: its `xloc:'bar_index'` coordinates count
8503
8670
  * from the model's anchor bar, so they shift by this to land on chart logical indices.
@@ -8518,8 +8685,13 @@ var DrawingSceneRenderer = class {
8518
8685
  const s = this.set;
8519
8686
  return !s.lines.length && !s.boxes.length && !s.labels.length && !s.polylines.length && !s.linefills.length;
8520
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
+ }
8521
8692
  /** Draw the whole set into `ctx` using the supplied coordinate closures. */
8522
8693
  render(ctx, W, H, xOf, yOf) {
8694
+ this.tipRegions = [];
8523
8695
  if (this.isEmpty()) return;
8524
8696
  ctx.save();
8525
8697
  this.drawLinefills(ctx, W, H, xOf, yOf);
@@ -8867,13 +9039,35 @@ var DrawingSceneRenderer = class {
8867
9039
  if (this.isPointShape(lb.style)) {
8868
9040
  if (!lb.noFill) this.drawLabelShape(ctx, lb.style, px, py, fontPx, color);
8869
9041
  if (lb.text) this.drawLabelText(ctx, lb, px, py + fontPx, fontPx);
8870
- } else if (lb.style === "none" || lb.style === "text_outline" || lb.noFill) {
8871
- 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
+ }
8872
9051
  } else {
8873
- 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 });
8874
9054
  }
8875
9055
  }
8876
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
+ }
8877
9071
  isPointShape(style) {
8878
9072
  switch (style) {
8879
9073
  case "circle":
@@ -8892,8 +9086,7 @@ var DrawingSceneRenderer = class {
8892
9086
  }
8893
9087
  }
8894
9088
  drawLabelText(ctx, lb, cx, cy, fontPx, outline = false) {
8895
- const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
8896
- ctx.font = `${fontPx}px ${family}`;
9089
+ ctx.font = this.labelFont(lb, fontPx);
8897
9090
  ctx.textAlign = lb.textAlign === "left" ? "left" : lb.textAlign === "right" ? "right" : "center";
8898
9091
  ctx.textBaseline = "middle";
8899
9092
  const lines = lb.text.split("\n");
@@ -8973,9 +9166,9 @@ var DrawingSceneRenderer = class {
8973
9166
  ctx.fill();
8974
9167
  }
8975
9168
  }
9169
+ /** Draw a bubble label; returns the bubble body rect (for tooltip hit-testing). */
8976
9170
  drawBubble(ctx, lb, px, py, fontPx, color) {
8977
- const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
8978
- ctx.font = `${fontPx}px ${family}`;
9171
+ ctx.font = this.labelFont(lb, fontPx);
8979
9172
  const lines = (lb.text ?? "").split("\n");
8980
9173
  const padX = 6;
8981
9174
  const padY = 4;
@@ -9033,38 +9226,51 @@ var DrawingSceneRenderer = class {
9033
9226
  by = py - h - ptr;
9034
9227
  pointer = "down";
9035
9228
  }
9036
- ctx.fillStyle = color;
9037
- this.roundRect(ctx, bx, by, w, h, 4);
9038
- ctx.fill();
9039
- if (pointer !== "none") {
9040
- ctx.beginPath();
9041
- if (pointer === "down") {
9042
- ctx.moveTo(px - ptr, by + h);
9043
- ctx.lineTo(px + ptr, by + h);
9044
- ctx.lineTo(px, py);
9045
- } else if (pointer === "up") {
9046
- ctx.moveTo(px - ptr, by);
9047
- ctx.lineTo(px + ptr, by);
9048
- ctx.lineTo(px, py);
9049
- } else if (pointer === "left") {
9050
- ctx.moveTo(bx, py - ptr);
9051
- ctx.lineTo(bx, py + ptr);
9052
- ctx.lineTo(px, py);
9053
- } else {
9054
- ctx.moveTo(bx + w, py - ptr);
9055
- ctx.lineTo(bx + w, py + ptr);
9056
- ctx.lineTo(px, py);
9057
- }
9058
- ctx.closePath();
9229
+ if (!lb.noFill) {
9230
+ ctx.fillStyle = color;
9231
+ this.roundRect(ctx, bx, by, w, h, 4);
9059
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
+ }
9060
9255
  }
9061
9256
  if (lb.text) {
9062
- ctx.fillStyle = lb.textColor ?? contrastColor(color);
9063
- ctx.textAlign = "center";
9257
+ ctx.fillStyle = lb.textColor ?? (lb.noFill ? this.deps.theme.textColor : contrastColor(color));
9064
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
+ }
9065
9270
  const startY = by + padY + lineH / 2;
9066
- 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);
9067
9272
  }
9273
+ return { x: bx, y: by, w, h };
9068
9274
  }
9069
9275
  roundRect(ctx, x, y, w, h, r) {
9070
9276
  const rr = Math.min(r, w / 2, h / 2);
@@ -9302,6 +9508,8 @@ var ChromeRenderer = class {
9302
9508
  this.axisTextColor = DARK_THEME.textColor;
9303
9509
  // Shared Pine-drawing renderer (line/box/label/polyline/linefill); widthCache persists.
9304
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 = [];
9305
9513
  }
9306
9514
  mount(canvas) {
9307
9515
  this.canvas = canvas;
@@ -9343,6 +9551,7 @@ var ChromeRenderer = class {
9343
9551
  const dataW = coords.width;
9344
9552
  const dataH = coords.height;
9345
9553
  this.axisTextColor = surface?.textColor ?? theme.textColor;
9554
+ this.labelTips = [];
9346
9555
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
9347
9556
  ctx.clearRect(0, 0, fullW, fullH);
9348
9557
  if (surface && (fullW > dataW || fullH > dataH)) {
@@ -9452,6 +9661,17 @@ var ChromeRenderer = class {
9452
9661
  (price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
9453
9662
  );
9454
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;
9455
9675
  }
9456
9676
  // ── axes ──
9457
9677
  drawPriceAxes(ctx, scene, coords, theme, dataW, panes) {
@@ -9668,6 +9888,68 @@ function formatCountdown(ms) {
9668
9888
  return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
9669
9889
  }
9670
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
+
9671
9953
  // src/renderers/native/chrome/CrosshairRenderer.ts
9672
9954
  var CrosshairRenderer = class {
9673
9955
  constructor() {
@@ -14745,19 +15027,8 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
14745
15027
  for (const model of models) {
14746
15028
  const off = offsetOf(model.id);
14747
15029
  for (const s of model.series) {
14748
- if (s.kind === "candle" || s.kind === "bar") {
14749
- for (let i = i0; i <= i1; i += 1) {
14750
- const b = s.bars[i - off];
14751
- if (b) {
14752
- consider(b.high);
14753
- consider(b.low);
14754
- }
14755
- }
14756
- } else if (isLineLikeSeries(s)) {
14757
- for (let i = i0; i <= i1; i += 1) consider(s.points[i - off]?.value);
14758
- if (s.kind === "histogram" || s.kind === "columns") consider(s.style?.base ?? 0);
14759
- else if (s.style?.base != null) consider(s.style.base);
14760
- }
15030
+ if (s.overlay === true) continue;
15031
+ considerSeries(s, i0, i1, off, consider);
14761
15032
  }
14762
15033
  for (const pl of model.priceLines) consider(pl.price);
14763
15034
  }
@@ -14779,6 +15050,36 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
14779
15050
  const span = max - min;
14780
15051
  return { min: min - span * MARGIN_BOTTOM, max: max + span * MARGIN_TOP };
14781
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
+ }
14782
15083
  function expandScaleByPixels(scale, heightPx, abovePx, belowPx) {
14783
15084
  if (abovePx <= 0 && belowPx <= 0) return scale;
14784
15085
  const content = heightPx - abovePx - belowPx;
@@ -15535,6 +15836,8 @@ var NativeRenderer = class {
15535
15836
  this.glowAmount = 0;
15536
15837
  // WebGL2 neon-glow intensity (canvas2d ignores it)
15537
15838
  this.chrome = new ChromeRenderer();
15839
+ /** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
15840
+ this.labelTooltip = null;
15538
15841
  this.crosshairLayer = new CrosshairRenderer();
15539
15842
  /** 1 Hz repaint pump so the price-axis countdown-to-bar-close ticks; null when off. */
15540
15843
  this.countdownTimer = null;
@@ -15545,6 +15848,8 @@ var NativeRenderer = class {
15545
15848
  this.indicatorValuesOn = true;
15546
15849
  /** Host-contributed legend actions — held here so a rebuild of the legend re-wires them. */
15547
15850
  this.legendActionsProvider = null;
15851
+ /** Host-contributed legend callouts — held here so a rebuild of the legend re-wires them. */
15852
+ this.legendCalloutsProvider = null;
15548
15853
  /** Host override of the legend's fold toggle — held here so a remount re-applies it. */
15549
15854
  this.legendOverviewAction = null;
15550
15855
  // ── keyboard navigation / accessibility (item 11) ──
@@ -16653,6 +16958,10 @@ var NativeRenderer = class {
16653
16958
  this.plot.appendChild(this.scrollButton);
16654
16959
  this.plot.addEventListener("pointermove", this.onScrollProximityMove);
16655
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
+ });
16656
16965
  this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
16657
16966
  projector: () => this.drawingProjector(),
16658
16967
  dpr: () => this.coords.dpr,
@@ -16692,9 +17001,10 @@ var NativeRenderer = class {
16692
17001
  this.inputsUI.setDialogHost(this.dialogHost);
16693
17002
  this.inputsUI.setSymbolPicker(this.symbolPicker);
16694
17003
  this.inputsUI.setLegendActions(this.legendActionsProvider);
17004
+ this.inputsUI.setLegendCallouts(this.legendCalloutsProvider);
16695
17005
  this.inputsUI.setLegendOverviewAction(this.legendOverviewAction);
16696
17006
  this.inputsUI.setOnChange((c) => {
16697
- 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 } : {} });
16698
17008
  });
16699
17009
  this.inputsUI.setOnRemove((id) => {
16700
17010
  for (const cb of this.removeIndicatorCbs) cb(id);
@@ -16884,6 +17194,8 @@ var NativeRenderer = class {
16884
17194
  this.settingsButton = null;
16885
17195
  this.plot?.removeEventListener("pointermove", this.onScrollProximityMove);
16886
17196
  this.plot?.removeEventListener("pointerleave", this.onScrollProximityLeave);
17197
+ this.labelTooltip?.destroy();
17198
+ this.labelTooltip = null;
16887
17199
  this.scrollButton?.remove();
16888
17200
  this.scrollButton = null;
16889
17201
  for (const l of this.extLayers) l.instance.destroy?.();
@@ -17124,7 +17436,7 @@ var NativeRenderer = class {
17124
17436
  else this.scene.assignIndicatorZ(model.id);
17125
17437
  this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
17126
17438
  native: !!model.native,
17127
- ...model.shorttitle ? { settingsTitle: model.title } : {}
17439
+ ...model.props ? { props: model.props, propValues: model.propValues ?? {} } : {}
17128
17440
  });
17129
17441
  this.syncTables(model);
17130
17442
  if (model.native?.type === "volume") {
@@ -17175,8 +17487,8 @@ var NativeRenderer = class {
17175
17487
  this.paneControls?.refresh();
17176
17488
  this.scheduler.invalidate(4 /* Full */);
17177
17489
  }
17178
- setIndicatorInputs(handle, values) {
17179
- this.inputsUI.setValues(handle.id, values);
17490
+ setIndicatorInputs(handle, values, props) {
17491
+ this.inputsUI.setValues(handle.id, values, props);
17180
17492
  }
17181
17493
  setSymbolPicker(picker) {
17182
17494
  this.symbolPicker = picker;
@@ -17186,6 +17498,10 @@ var NativeRenderer = class {
17186
17498
  this.legendActionsProvider = provider;
17187
17499
  this.inputsUI?.setLegendActions(provider);
17188
17500
  }
17501
+ setLegendCallouts(provider) {
17502
+ this.legendCalloutsProvider = provider;
17503
+ this.inputsUI?.setLegendCallouts(provider);
17504
+ }
17189
17505
  setLegendOverviewAction(action) {
17190
17506
  this.legendOverviewAction = action;
17191
17507
  this.inputsUI?.setLegendOverviewAction(action);
@@ -18166,7 +18482,11 @@ var NativeRenderer = class {
18166
18482
  }
18167
18483
  const models = this.scene.indicatorsForPane(pane.id);
18168
18484
  const masterModels = models.filter((m) => m.ownScale !== true);
18169
- 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
+ }
18170
18490
  const includeCandles = pane.kind === "price" && !this.scene.candlesHidden;
18171
18491
  pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
18172
18492
  pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
@@ -18440,6 +18760,7 @@ var NativeRenderer = class {
18440
18760
  for (const m of models) {
18441
18761
  const off = this.scene.offsetOf(m.id);
18442
18762
  for (const s of m.series) {
18763
+ if (s.overlay === true) continue;
18443
18764
  if (isLineLikeSeries(s)) {
18444
18765
  const v = s.points[i0 - off]?.value;
18445
18766
  if (v != null && Number.isFinite(v)) return v;
@@ -18820,6 +19141,7 @@ var IndicatorHandleImpl = class {
18820
19141
  constructor(id, title, controller, source) {
18821
19142
  this.controller = controller;
18822
19143
  this.schema = [];
19144
+ this.propsSchema = [];
18823
19145
  this.visibleState = true;
18824
19146
  this.bus = new TypedEventBus();
18825
19147
  this.id = id;
@@ -18829,6 +19151,9 @@ var IndicatorHandleImpl = class {
18829
19151
  get inputs() {
18830
19152
  return this.schema;
18831
19153
  }
19154
+ get props() {
19155
+ return this.propsSchema;
19156
+ }
18832
19157
  get visible() {
18833
19158
  return this.visibleState;
18834
19159
  }
@@ -18838,6 +19163,12 @@ var IndicatorHandleImpl = class {
18838
19163
  setInputs(values) {
18839
19164
  this.controller.applyInputs(this.id, values);
18840
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
+ }
18841
19172
  setVisible(visible) {
18842
19173
  this.controller.setVisible(this.id, visible);
18843
19174
  }
@@ -18857,6 +19188,9 @@ var IndicatorHandleImpl = class {
18857
19188
  setSchema(schema) {
18858
19189
  this.schema = schema;
18859
19190
  }
19191
+ setPropsSchema(schema) {
19192
+ this.propsSchema = schema;
19193
+ }
18860
19194
  /** Sync the public `visible` getter when the orchestrator changes visibility (API or legend eye). */
18861
19195
  setVisibleState(visible) {
18862
19196
  this.visibleState = visible;
@@ -18870,6 +19204,8 @@ var IndicatorHandleImpl = class {
18870
19204
  function summarizeModel(model) {
18871
19205
  const series = {};
18872
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);
18873
19209
  return {
18874
19210
  id: model.id,
18875
19211
  title: model.title,
@@ -18889,7 +19225,8 @@ function summarizeModel(model) {
18889
19225
  tables: model.tables?.length ?? 0,
18890
19226
  barColors: model.barColors?.length ?? 0,
18891
19227
  trades: model.trades?.length ?? 0,
18892
- inputs: model.inputs.length
19228
+ inputs: model.inputs.length,
19229
+ forcedOverlay
18893
19230
  };
18894
19231
  }
18895
19232
  function inspectModels(models) {
@@ -18910,7 +19247,8 @@ function inspectModels(models) {
18910
19247
  linefills: sum((s) => s.linefills),
18911
19248
  tables: sum((s) => s.tables),
18912
19249
  barColors: sum((s) => s.barColors),
18913
- trades: sum((s) => s.trades)
19250
+ trades: sum((s) => s.trades),
19251
+ forcedOverlay: sum((s) => s.forcedOverlay)
18914
19252
  }
18915
19253
  };
18916
19254
  }
@@ -19054,7 +19392,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
19054
19392
  for (const engine of engines) this.registerEngine(engine.language, engine);
19055
19393
  this.defaultLanguage = config.defaultLanguage;
19056
19394
  this.renderer.mount(container, config.theme);
19057
- 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
+ );
19058
19398
  this.renderer.onRemoveIndicator((id) => this.removeIndicator(id));
19059
19399
  this.renderer.onToggleIndicatorVisible?.((id, visible) => this.setVisible(id, visible));
19060
19400
  this.paneActionUnsub = this.renderer.onPaneAction?.((a) => this.handlePaneAction(a)) ?? null;
@@ -19734,7 +20074,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19734
20074
  const title = options.title ?? "Indicator";
19735
20075
  const handle = new IndicatorHandleImpl(id, title, this, source);
19736
20076
  this.handles.set(id, handle);
19737
- this.registry.add({ id, title, source, options, inputValues: { ...options.inputs ?? {} } });
20077
+ this.registry.add({ id, title, source, options, inputValues: { ...options.inputs ?? {} }, propValues: { ...options.props ?? {} } });
19738
20078
  void this.startIndicator(id, source, options, handle);
19739
20079
  return handle;
19740
20080
  }
@@ -19757,7 +20097,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19757
20097
  return handle;
19758
20098
  }
19759
20099
  const inputValues = { ...descriptor.defaultInputs(), ...options.inputs ?? {} };
19760
- 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 } });
19761
20101
  handle.setSchema(descriptor.inputsSchema());
19762
20102
  void this.startNativeIndicator(id, handle);
19763
20103
  return handle;
@@ -19810,6 +20150,19 @@ var EngineOrchestrator = class _EngineOrchestrator {
19810
20150
  if (record.session) record.session.update(record.inputValues);
19811
20151
  else if (record.native && !record.hidden) record.native.instance.setInputs(record.inputValues);
19812
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
+ }
19813
20166
  /** IndicatorController: tear down an indicator and (if now empty) its pane. */
19814
20167
  /** Live handles of every indicator on the chart (script + native), insertion order. */
19815
20168
  listIndicators() {
@@ -19958,6 +20311,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
19958
20311
  for (const input of prepared.inputs) defaults[input.key] = input.defval;
19959
20312
  record.inputValues = { ...defaults, ...record.inputValues };
19960
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 ?? []);
19961
20318
  this.mountLoadingPlaceholder(id, record);
19962
20319
  this.executeIndicator(id, handle);
19963
20320
  } catch (err) {
@@ -19981,6 +20338,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19981
20338
  getBars: () => this.bars,
19982
20339
  fetchSeries: (sym, tf, range) => this.fetchSeries(sym, tf, range),
19983
20340
  inputs: record.inputValues,
20341
+ props: record.propValues,
19984
20342
  visibleRange: this.currentVisibleRange(),
19985
20343
  mode,
19986
20344
  // Mid-backfill session starts (add / re-show / price-style re-execute) let the
@@ -20074,6 +20432,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
20074
20432
  const meta = record.prepared.meta;
20075
20433
  const model = {
20076
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.
20077
20438
  title: record.options?.title ?? meta.title,
20078
20439
  overlay: meta.overlay,
20079
20440
  paneHint: meta.overlay ? "price" : "new",
@@ -20082,7 +20443,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
20082
20443
  backgrounds: [],
20083
20444
  priceLines: [],
20084
20445
  inputs: record.prepared.inputs,
20085
- inputValues: record.inputValues
20446
+ inputValues: record.inputValues,
20447
+ ...record.prepared.props ? { props: record.prepared.props, propValues: record.propValues } : {}
20086
20448
  };
20087
20449
  const paneId = this.routePane(id, model, record.options ?? {});
20088
20450
  this.placeModel(model, id, paneId);
@@ -20315,7 +20677,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
20315
20677
  record.model = model;
20316
20678
  if (record.pendingStructural) {
20317
20679
  record.renderHandle = this.renderer.mountIndicator(model);
20318
- this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues);
20680
+ this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
20319
20681
  record.pendingStructural = false;
20320
20682
  } else {
20321
20683
  this.renderer.updateIndicator(record.renderHandle, modelToValuePatch(model));
@@ -20342,16 +20704,17 @@ var EngineOrchestrator = class _EngineOrchestrator {
20342
20704
  placeModel(model, id, paneId) {
20343
20705
  model.id = id;
20344
20706
  model.paneId = paneId;
20345
- for (const series of model.series) series.paneId = paneId;
20346
- for (const fill of model.fills) fill.paneId = paneId;
20347
- 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);
20348
20711
  for (const line of model.priceLines) line.paneId = paneId;
20349
- if (model.lines) for (const ln of model.lines) ln.paneId = paneId;
20350
- if (model.boxes) for (const bx of model.boxes) bx.paneId = paneId;
20351
- if (model.labels) for (const lb of model.labels) lb.paneId = paneId;
20352
- if (model.polylines) for (const pl of model.polylines) pl.paneId = paneId;
20353
- if (model.linefills) for (const lf of model.linefills) lf.paneId = paneId;
20354
- 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);
20355
20718
  }
20356
20719
  emitContextChanged(id) {
20357
20720
  if (!this.registry.get(id)?.session?.getContext) return;
@@ -21011,4 +21374,4 @@ function resolveElement(container) {
21011
21374
  return element;
21012
21375
  }
21013
21376
 
21014
- 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 };