@luxalgo/vela 0.8.1 → 0.8.2

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 (48) hide show
  1. package/README.md +1 -1
  2. package/dist/{DataProvider-BrlxntPN.d.cts → DataProvider-COGDs9Xg.d.cts} +1 -1
  3. package/dist/{DataProvider-DwFIoV4r.d.ts → DataProvider-DpOWZD8F.d.ts} +1 -1
  4. package/dist/{chunk-LMBYEGUR.js → chunk-27UTYE7T.js} +25 -2
  5. package/dist/{chunk-EZ5FWVLA.js → chunk-DPAA3SRT.js} +94 -13
  6. package/dist/{chunk-BZQM2XO7.js → chunk-HKNO24GD.js} +13 -1
  7. package/dist/{chunk-NELQJCGK.js → chunk-NLDTVFOI.js} +41 -3
  8. package/dist/{chunk-DBDXKZUF.js → chunk-R2JMNIHY.js} +53 -13
  9. package/dist/{chunk-ATGTCWHJ.js → chunk-T3DZIR5Y.js} +171 -32
  10. package/dist/{contributions-tjNExo1o.d.ts → contributions-Cgm9RJYP.d.cts} +38 -11
  11. package/dist/{contributions-CtlptxR8.d.cts → contributions-bWgwyiVi.d.ts} +38 -11
  12. package/dist/index.cjs +239 -37
  13. package/dist/index.d.cts +8 -8
  14. package/dist/index.d.ts +8 -8
  15. package/dist/index.js +4 -4
  16. package/dist/{options-BnEPUuVV.d.ts → options-NAUHiQEC.d.cts} +3 -3
  17. package/dist/{options-BnEPUuVV.d.cts → options-NAUHiQEC.d.ts} +3 -3
  18. package/dist/{plugin-BWxJRKzS.d.cts → plugin-CjjzZX2F.d.cts} +4 -4
  19. package/dist/{plugin-e2WUEtiI.d.ts → plugin-CqxWoDKS.d.ts} +4 -4
  20. package/dist/plugin.cjs +20 -6
  21. package/dist/plugin.d.cts +5 -5
  22. package/dist/plugin.d.ts +5 -5
  23. package/dist/plugin.js +2 -2
  24. package/dist/providers/binance.d.cts +2 -2
  25. package/dist/providers/binance.d.ts +2 -2
  26. package/dist/providers/coinbase.d.cts +2 -2
  27. package/dist/providers/coinbase.d.ts +2 -2
  28. package/dist/providers/hyperliquid.d.cts +2 -2
  29. package/dist/providers/hyperliquid.d.ts +2 -2
  30. package/dist/{side-panel-HF0IAzwf.d.cts → side-panel-wMNfgyZb.d.cts} +13 -0
  31. package/dist/{side-panel-HF0IAzwf.d.ts → side-panel-wMNfgyZb.d.ts} +13 -0
  32. package/dist/{statusline-model-zKXtGIjj.d.ts → statusline-model-Dih6FGWP.d.ts} +3 -3
  33. package/dist/{statusline-model-DFY7tuHj.d.cts → statusline-model-Dpfpq54A.d.cts} +3 -3
  34. package/dist/ui.cjs +77 -2
  35. package/dist/ui.d.cts +28 -2
  36. package/dist/ui.d.ts +28 -2
  37. package/dist/ui.js +3 -3
  38. package/dist/vela.global.js +239 -37
  39. package/dist/vela.global.min.js +52 -52
  40. package/dist/widget.cjs +383 -50
  41. package/dist/widget.d.cts +20 -10
  42. package/dist/widget.d.ts +20 -10
  43. package/dist/widget.js +7 -7
  44. package/dist/workspace.cjs +383 -50
  45. package/dist/workspace.d.cts +5 -5
  46. package/dist/workspace.d.ts +5 -5
  47. package/dist/workspace.js +6 -6
  48. package/package.json +2 -2
package/README.md CHANGED
@@ -12,7 +12,7 @@
12
12
 
13
13
  # Vela™
14
14
 
15
- <p><strong>Fast, extensible financial charts for the web.</strong><br>
15
+ <p><strong>Fast, extensible financial charts for the web: the open-source core of <a href="https://vela.luxalgo.com/chart">Vela</a> by LuxAlgo.</strong><br>
16
16
  Headless core · native WebGL2 renderer · batteries-included workspace · plugin SDK</p>
17
17
 
18
18
  [![npm version][npm-version-img]][npm-link]
@@ -1,4 +1,4 @@
1
- import { M as MarketConfig, O as OHLCV, U as Unsubscribe } from './options-BnEPUuVV.cjs';
1
+ import { M as MarketConfig, O as OHLCV, U as Unsubscribe } from './options-NAUHiQEC.cjs';
2
2
 
3
3
  /**
4
4
  * Symbol metadata an engine may need (e.g. Pine `syminfo.*`). Free-form beyond
@@ -1,4 +1,4 @@
1
- import { M as MarketConfig, O as OHLCV, U as Unsubscribe } from './options-BnEPUuVV.js';
1
+ import { M as MarketConfig, O as OHLCV, U as Unsubscribe } from './options-NAUHiQEC.js';
2
2
 
3
3
  /**
4
4
  * Symbol metadata an engine may need (e.g. Pine `syminfo.*`). Free-form beyond
@@ -1,5 +1,5 @@
1
- import { normalizeProps, nextUid, runMachine, spreadProps } from './chunk-NELQJCGK.js';
2
- import { injectStyles } from './chunk-BZQM2XO7.js';
1
+ import { normalizeProps, nextUid, runMachine, spreadProps } from './chunk-NLDTVFOI.js';
2
+ import { injectStyles, announceSurface } from './chunk-HKNO24GD.js';
3
3
  import * as tooltip from '@zag-js/tooltip';
4
4
  import * as dialog from '@zag-js/dialog';
5
5
 
@@ -343,6 +343,8 @@ function swipeDirection(dx, dy) {
343
343
  }
344
344
  var Drawer = class {
345
345
  constructor(opts = {}) {
346
+ this.opener = null;
347
+ this.wasOpen = false;
346
348
  const doc = (opts.host ?? document.body).ownerDocument;
347
349
  injectStyles(DRAWER_STYLE_ID, DRAWER_CSS, doc);
348
350
  const host = opts.host ?? doc.body;
@@ -370,12 +372,16 @@ var Drawer = class {
370
372
  const mid = String(this.ctrl.props.id);
371
373
  this.handle = runMachine(this.ctrl.machine, this.ctrl.props, (service) => {
372
374
  const api = this.ctrl.connect(service);
375
+ if (this.wasOpen && !api.open) this.announce(false);
373
376
  spreadProps(this.backdrop, api.getBackdropProps(), mid);
374
377
  spreadProps(this.positioner, api.getPositionerProps(), mid);
375
378
  spreadProps(this.panel, api.getContentProps(), mid);
376
379
  spreadProps(this.titleEl, api.getTitleProps(), mid);
377
380
  this.backdrop.style.display = api.open ? "" : "none";
378
381
  this.positioner.style.display = api.open ? "" : "none";
382
+ if (api.open && !this.wasOpen) this.announce(true);
383
+ if (this.wasOpen && !api.open) this.restoreOpener();
384
+ this.wasOpen = api.open;
379
385
  });
380
386
  }
381
387
  /** Any element between `from` and the panel that has already been scrolled down —
@@ -484,10 +490,22 @@ var Drawer = class {
484
490
  setTitle(title) {
485
491
  this.titleEl.textContent = title;
486
492
  }
493
+ /** Closing hands focus back to whatever opened it (Escape, the ✕, hide(), or a
494
+ * teardown that outruns the machine's close notification). */
495
+ restoreOpener() {
496
+ const opener = this.opener;
497
+ this.opener = null;
498
+ if (opener?.isConnected) opener.focus({ preventScroll: true });
499
+ }
500
+ announce(open) {
501
+ const opener = this.opener === this.panel.ownerDocument.body ? null : this.opener;
502
+ announceSurface(this.panel, open, "drawer", opener);
503
+ }
487
504
  get open() {
488
505
  return this.ctrl.connect(this.handle.service).open;
489
506
  }
490
507
  show() {
508
+ this.opener = this.panel.ownerDocument.activeElement;
491
509
  this.ctrl.connect(this.handle.service).setOpen(true);
492
510
  }
493
511
  hide() {
@@ -495,6 +513,11 @@ var Drawer = class {
495
513
  }
496
514
  destroy() {
497
515
  this.handle.stop();
516
+ if (this.wasOpen) {
517
+ this.wasOpen = false;
518
+ this.announce(false);
519
+ }
520
+ this.restoreOpener();
498
521
  this.backdrop.remove();
499
522
  this.positioner.remove();
500
523
  }
@@ -1,4 +1,4 @@
1
- import { svg24, NEUTRAL, BEARISH, WARNING, BULLISH, INFO, ACCENT_BRIGHT, VALID, INVALID, MARKER, ACCENT, injectStyles, iconEl } from './chunk-BZQM2XO7.js';
1
+ import { svg24, NEUTRAL, BEARISH, WARNING, BULLISH, INFO, ACCENT_BRIGHT, VALID, INVALID, MARKER, ACCENT, injectStyles, iconEl, announceSurface } from './chunk-HKNO24GD.js';
2
2
 
3
3
  // src/core/model/series.ts
4
4
  function isLineLikeSeries(spec) {
@@ -2778,6 +2778,10 @@ var FibRatios = class extends Drawing {
2778
2778
 
2779
2779
  // src/core/drawings/types/FibLevels.ts
2780
2780
  var FibLevels = class extends FibRatios {
2781
+ /** Price of a level for the two anchor prices; subclasses override to change which anchor ratio 0 sits on. */
2782
+ levelPrice(ratio, p1, p2) {
2783
+ return p1 + ratio * (p2 - p1);
2784
+ }
2781
2785
  /** Per-level pixel line + price for the ENABLED levels, spanning the anchors' time range. */
2782
2786
  levelLines(proj) {
2783
2787
  const a = this.anchors[0];
@@ -2787,11 +2791,10 @@ var FibLevels = class extends FibRatios {
2787
2791
  const xb = proj.xOf(b.time);
2788
2792
  const x1 = Math.min(xa, xb);
2789
2793
  const x2 = Math.max(xa, xb);
2790
- const delta = b.price - a.price;
2791
2794
  const out = [];
2792
2795
  for (const lv of this.levels) {
2793
2796
  if (!lv.enabled) continue;
2794
- const price = a.price + lv.ratio * delta;
2797
+ const price = this.levelPrice(lv.ratio, a.price, b.price);
2795
2798
  const y = proj.yOf(price, this.paneId);
2796
2799
  if (y == null) continue;
2797
2800
  out.push({ ratio: lv.ratio, color: lv.color, label: lv.label, price, x1, x2, y });
@@ -2831,8 +2834,7 @@ var FibLevels = class extends FibRatios {
2831
2834
  const a = this.anchors[0];
2832
2835
  const b = this.anchors[1];
2833
2836
  if (!a || !b) return null;
2834
- const delta = b.price - a.price;
2835
- const prices = this.levels.filter((l) => l.enabled).map((l) => a.price + l.ratio * delta);
2837
+ const prices = this.levels.filter((l) => l.enabled).map((l) => this.levelPrice(l.ratio, a.price, b.price));
2836
2838
  if (prices.length === 0) return null;
2837
2839
  return { min: Math.min(...prices), max: Math.max(...prices) };
2838
2840
  }
@@ -2918,13 +2920,24 @@ function cycleLevels(count, enabledCount = count) {
2918
2920
  // src/core/drawings/types/FibRetracement.ts
2919
2921
  var LEVELS = fibLevels([0, 0.236, 0.382, 0.5, 0.618, 0.786, 1]);
2920
2922
  var FibRetracement = class extends FibLevels {
2921
- constructor() {
2922
- super(...arguments);
2923
+ constructor(init) {
2924
+ super(init);
2923
2925
  this.type = "fibretracement";
2926
+ if (this.reverse === void 0) this.reverse = false;
2924
2927
  }
2925
2928
  defaultLevels() {
2926
2929
  return LEVELS;
2927
2930
  }
2931
+ levelPrice(ratio, p1, p2) {
2932
+ return this.reverse ? p1 + ratio * (p2 - p1) : p2 + ratio * (p1 - p2);
2933
+ }
2934
+ writeProps() {
2935
+ return { ...super.writeProps(), reverse: this.reverse };
2936
+ }
2937
+ readProps(props) {
2938
+ super.readProps(props);
2939
+ this.reverse = typeof props.reverse === "boolean" ? props.reverse : true;
2940
+ }
2928
2941
  };
2929
2942
 
2930
2943
  // src/core/drawings/types/FibExtension.ts
@@ -6023,10 +6036,12 @@ function legendActions() {
6023
6036
  }
6024
6037
  function legendActionsProviderFor(chart, context) {
6025
6038
  return (indicatorId) => {
6026
- const handle = chart.indicators().find((h) => h.id === indicatorId);
6039
+ const find = () => chart.indicators().find((h) => h.id === indicatorId);
6040
+ const infoOf = (h) => ({ id: h.id, title: h.title, ...h.source !== void 0 ? { source: h.source } : {} });
6041
+ const handle = find();
6027
6042
  if (!handle) return [];
6028
- const info = { id: handle.id, title: handle.title, ...handle.source !== void 0 ? { source: handle.source } : {} };
6029
- return legendActions().filter((d) => !d.when || d.when(info)).map((d) => ({ id: d.id, icon: d.icon, tooltip: d.tooltip, run: () => d.run(context(), info) }));
6043
+ const info = infoOf(handle);
6044
+ return legendActions().filter((d) => !d.when || d.when(info)).map((d) => ({ id: d.id, icon: d.icon, tooltip: d.tooltip, run: () => d.run(context(), infoOf(find() ?? handle)) }));
6030
6045
  };
6031
6046
  }
6032
6047
  var calloutRegistry = /* @__PURE__ */ new Map();
@@ -6177,6 +6192,32 @@ var CSS = `
6177
6192
  .vela-panel-pin .vela-icon { width: 15px; height: 15px; }
6178
6193
  .vela-panel-pin:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
6179
6194
  .vela-panel-pin[data-on='1'] { color: var(--vela-fg-bright); }
6195
+ .vela-panel-max {
6196
+ all: unset;
6197
+ cursor: pointer;
6198
+ width: 26px;
6199
+ height: 26px;
6200
+ flex: none;
6201
+ display: inline-flex;
6202
+ align-items: center;
6203
+ justify-content: center;
6204
+ border-radius: 4px;
6205
+ color: var(--vela-fg-muted);
6206
+ }
6207
+ .vela-panel-max .vela-icon { width: 15px; height: 15px; }
6208
+ .vela-panel-max:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
6209
+ /* MAXIMIZED: the panel covers the whole dock host \u2014 every chart of the shell \u2014 whatever
6210
+ its placement; the width handle and the pin have nothing to do meanwhile. */
6211
+ .vela-panel[data-maximized] {
6212
+ position: absolute;
6213
+ inset: 0;
6214
+ width: auto;
6215
+ max-width: none;
6216
+ z-index: 26;
6217
+ border-left: none;
6218
+ }
6219
+ .vela-panel[data-maximized] .vela-panel-resizer,
6220
+ .vela-panel[data-maximized] .vela-panel-pin { display: none; }
6180
6221
  .vela-panel-body { flex: 1; overflow: auto; padding: 8px; }
6181
6222
  .vela-panel-body::-webkit-scrollbar { width: 8px; }
6182
6223
  .vela-panel-body::-webkit-scrollbar-thumb { background: var(--vela-scroll); border-radius: 4px; border: 2px solid transparent; background-clip: padding-box; }
@@ -6229,6 +6270,7 @@ var CSS = `
6229
6270
  border-left: none;
6230
6271
  }
6231
6272
  [data-layout='mobile'] .vela-panel-resizer { display: none; }
6273
+ [data-layout='mobile'] .vela-panel-max { display: none; }
6232
6274
  `;
6233
6275
  function clampPanelWidth(px, min = DEFAULT_PANEL_MIN_WIDTH, max = DEFAULT_PANEL_MAX_WIDTH) {
6234
6276
  const lo = Number.isFinite(min) && min > 0 ? min : DEFAULT_PANEL_MIN_WIDTH;
@@ -6252,6 +6294,10 @@ var SidePanel = class {
6252
6294
  * never on a programmatic {@link setOverlay}, for the same reason as widths. */
6253
6295
  this.onPlacementChange = null;
6254
6296
  this.pin = null;
6297
+ this.maxButton = null;
6298
+ this.maximizedOn = false;
6299
+ /** Set while the close is announced, so a listener's own close does not re-enter. */
6300
+ this.closing = false;
6255
6301
  const doc = host.ownerDocument;
6256
6302
  injectStyles(STYLE_ID, CSS, doc);
6257
6303
  this.minWidth = opts.resizable ? opts.minWidth ?? DEFAULT_PANEL_MIN_WIDTH : 1;
@@ -6286,7 +6332,13 @@ var SidePanel = class {
6286
6332
  });
6287
6333
  this.refreshPin();
6288
6334
  }
6289
- header.append(this.heading, this.slot, ...this.pin ? [this.pin] : [], close);
6335
+ if (opts.maximizable) {
6336
+ this.maxButton = doc.createElement("button");
6337
+ this.maxButton.className = "vela-panel-max";
6338
+ this.maxButton.addEventListener("click", () => this.setMaximized(!this.maximizedOn));
6339
+ this.refreshMaxButton();
6340
+ }
6341
+ header.append(this.heading, this.slot, ...this.pin ? [this.pin] : [], ...this.maxButton ? [this.maxButton] : [], close);
6290
6342
  this.body = doc.createElement("div");
6291
6343
  this.body.className = "vela-panel-body";
6292
6344
  this.el.append(header, this.body);
@@ -6298,9 +6350,37 @@ var SidePanel = class {
6298
6350
  }
6299
6351
  /** Open/close the panel — a bare call flips it. */
6300
6352
  toggle(open = this.el.hidden) {
6301
- if (open === !this.el.hidden) return;
6353
+ if (open === !this.el.hidden || !open && this.closing) return;
6354
+ if (!open) {
6355
+ this.closing = true;
6356
+ announceSurface(this.el, false, "panel");
6357
+ this.closing = false;
6358
+ }
6302
6359
  this.el.hidden = !open;
6360
+ if (!open) this.setMaximized(false);
6303
6361
  this.onOpenChange?.(open);
6362
+ if (open && !this.el.hidden) announceSurface(this.el, true, "panel");
6363
+ }
6364
+ /** Whether the panel covers every chart right now. */
6365
+ get maximized() {
6366
+ return this.maximizedOn;
6367
+ }
6368
+ /** Maximize the panel over the shell's charts, or restore it. A no-op on a panel not
6369
+ * declared `maximizable`. */
6370
+ setMaximized(maximized) {
6371
+ if (!this.maxButton || maximized === this.maximizedOn) return;
6372
+ this.maximizedOn = maximized;
6373
+ if (maximized) this.el.dataset.maximized = "1";
6374
+ else delete this.el.dataset.maximized;
6375
+ this.refreshMaxButton();
6376
+ }
6377
+ refreshMaxButton() {
6378
+ if (!this.maxButton) return;
6379
+ const doc = this.maxButton.ownerDocument;
6380
+ this.maxButton.replaceChildren(iconEl(this.maximizedOn ? "restore" : "maximize", doc));
6381
+ this.maxButton.title = this.maximizedOn ? "Restore" : "Maximize";
6382
+ this.maxButton.setAttribute("aria-label", this.maxButton.title);
6383
+ this.maxButton.setAttribute("aria-pressed", this.maximizedOn ? "true" : "false");
6304
6384
  }
6305
6385
  /** The scrolling body, for a panel filled from OUTSIDE the class — a contributed panel's
6306
6386
  * `mount` receives exactly this element. Subclasses use the protected `body`. */
@@ -6352,6 +6432,7 @@ var SidePanel = class {
6352
6432
  this.pin.setAttribute("aria-pressed", pinned ? "true" : "false");
6353
6433
  }
6354
6434
  destroy() {
6435
+ if (this.open) announceSurface(this.el, false, "panel");
6355
6436
  this.el.remove();
6356
6437
  }
6357
6438
  /** The drag handle on the panel's inner (left) edge — the panel is docked right, so dragging
@@ -6396,4 +6477,4 @@ var SidePanel = class {
6396
6477
  }
6397
6478
  };
6398
6479
 
6399
- export { AnchoredVwap, ArrowMark, Callout, CalloutBase, Comment, DEDEKIND_CURVATURE_OPTIONS, DEFAULT_DRAWING_COLOR, DEFAULT_PANEL_MAX_WIDTH, DEFAULT_PANEL_MIN_WIDTH, DEFAULT_PANEL_ORDER, DEFAULT_PANEL_WIDTH, DIRECTION_OPTIONS, DedekindTessellation, Drawing, FibRatios, FibSpiral, FixedRangeVolumeProfile, GANN_SQUARE_ARCS, GLYPH_OPTIONS, GannSquare, GlyphStamp, LINE_STYLE_OPTIONS, MACH_NUMBER_OPTIONS, MACH_WAVE_COUNT_OPTIONS, MAGNIFIER_TIMEFRAME_OPTIONS, MachFigure, Magnifier, MeasureBox, Note, OVERRIDABLE_TOPBAR_IDS, PatternDrawing, PositionTool, PriceLabel, PriceNote, RadialFib, RegressionChannel, STAMP_SIZE_OPTIONS, SegmentDrawing, SidePanel, Signpost, TEXT_SIZE_OPTIONS, TOPBAR_BUILTIN_IDS, TOPBAR_DEFAULT_LEFT, TOPBAR_DEFAULT_RIGHT, TextLabel, chartType, chartTypes, clampPanelWidth, createDrawing, deserializeDrawing, drawingTypes, foldBaseModulation, formatDuration, getDrawingType, getNativeIndicator, inputDeltas, inputVisible, isLineLikeSeries, legendActions, legendActionsProviderFor, legendCallouts, legendCalloutsProviderFor, lineSegmentIntersection, magnifierTimeframeLabel, mobilePlacement, nativeIndicatorDescriptors, nativeIndicatorTypes, nativeInstanceChannel, normalizeSettingsRow, pinnedTopbarActionIds, registerChartType, registerDefaultEngine, registerDrawingType, registerLegendAction, registerLegendCallout, registerNativeIndicator, registerRendererDefaults, registerRendererLayer, registerSidePanel, registerStatePersistence, registerSymbolRanking, registerWidgetAction, registerWidgetAttachment, rendererDefaults, rendererLayers, resetDrawingSettings, resolveEngines, resolveTopbarComposition, seriesInScale, seriesShownOn, settingsRowValueKeys, settingsRowVisible, sidePanels, stableSeriesId, statePersistenceHandlers, symbolRanking, tickerModifierIds, topbarActionOverride, topbarHas, unregisterChartType, unregisterDefaultEngine, unregisterLegendAction, unregisterLegendCallout, unregisterNativeIndicator, unregisterRendererDefaults, unregisterRendererLayer, unregisterSidePanel, unregisterStatePersistence, unregisterWidgetAction, unregisterWidgetAttachment, widgetActions, widgetAttachments };
6480
+ export { AnchoredVwap, ArrowMark, Callout, CalloutBase, Comment, DEDEKIND_CURVATURE_OPTIONS, DEFAULT_DRAWING_COLOR, DEFAULT_PANEL_MAX_WIDTH, DEFAULT_PANEL_MIN_WIDTH, DEFAULT_PANEL_ORDER, DEFAULT_PANEL_WIDTH, DIRECTION_OPTIONS, DedekindTessellation, Drawing, FibRatios, FibRetracement, FibSpiral, FixedRangeVolumeProfile, GANN_SQUARE_ARCS, GLYPH_OPTIONS, GannSquare, GlyphStamp, LINE_STYLE_OPTIONS, MACH_NUMBER_OPTIONS, MACH_WAVE_COUNT_OPTIONS, MAGNIFIER_TIMEFRAME_OPTIONS, MachFigure, Magnifier, MeasureBox, Note, OVERRIDABLE_TOPBAR_IDS, PatternDrawing, PositionTool, PriceLabel, PriceNote, RadialFib, RegressionChannel, STAMP_SIZE_OPTIONS, SegmentDrawing, SidePanel, Signpost, TEXT_SIZE_OPTIONS, TOPBAR_BUILTIN_IDS, TOPBAR_DEFAULT_LEFT, TOPBAR_DEFAULT_RIGHT, TextLabel, chartType, chartTypes, clampPanelWidth, createDrawing, deserializeDrawing, drawingTypes, foldBaseModulation, formatDuration, getDrawingType, getNativeIndicator, inputDeltas, inputVisible, isLineLikeSeries, legendActions, legendActionsProviderFor, legendCallouts, legendCalloutsProviderFor, lineSegmentIntersection, magnifierTimeframeLabel, mobilePlacement, nativeIndicatorDescriptors, nativeIndicatorTypes, nativeInstanceChannel, normalizeSettingsRow, pinnedTopbarActionIds, registerChartType, registerDefaultEngine, registerDrawingType, registerLegendAction, registerLegendCallout, registerNativeIndicator, registerRendererDefaults, registerRendererLayer, registerSidePanel, registerStatePersistence, registerSymbolRanking, registerWidgetAction, registerWidgetAttachment, rendererDefaults, rendererLayers, resetDrawingSettings, resolveEngines, resolveTopbarComposition, seriesInScale, seriesShownOn, settingsRowValueKeys, settingsRowVisible, sidePanels, stableSeriesId, statePersistenceHandlers, symbolRanking, tickerModifierIds, topbarActionOverride, topbarHas, unregisterChartType, unregisterDefaultEngine, unregisterLegendAction, unregisterLegendCallout, unregisterNativeIndicator, unregisterRendererDefaults, unregisterRendererLayer, unregisterSidePanel, unregisterStatePersistence, unregisterWidgetAction, unregisterWidgetAttachment, widgetActions, widgetAttachments };
@@ -146,6 +146,7 @@ registerIcon("expand", S('<rect x="2.2" y="2.2" width="11.6" height="11.6" rx="1
146
146
  registerIcon("plus", S('<path d="M8 2.8v10.4M2.8 8h10.4"/>'));
147
147
  registerIcon("minus", S('<path d="M2.8 8h10.4"/>'));
148
148
  registerIcon("maximize", S('<path d="M2.5 6V3a.5.5 0 0 1 .5-.5h3M10 2.5h3a.5.5 0 0 1 .5.5v3M13.5 10v3a.5.5 0 0 1-.5.5h-3M6 13.5H3a.5.5 0 0 1-.5-.5v-3"/>'));
149
+ registerIcon("restore", S('<path d="M6 2.5v3a.5.5 0 0 1-.5.5h-3M13.5 6h-3a.5.5 0 0 1-.5-.5v-3M10 13.5v-3a.5.5 0 0 1 .5-.5h3M2.5 10h3a.5.5 0 0 1 .5.5v3"/>'));
149
150
  registerIcon("restore", S('<path d="M6.2 2.5v3.7H2.5M9.8 13.5V9.8h3.7M13.5 6.2H9.8V2.5M2.5 9.8h3.7v3.7"/>'));
150
151
  registerIcon("star", S('<path d="M8 2.2l1.75 3.55 3.9.55-2.8 2.75.65 3.9L8 11.1l-3.5 1.85.65-3.9-2.8-2.75 3.9-.55z"/>'));
151
152
  registerIcon("star-filled", S('<path d="M8 2.2l1.75 3.55 3.9.55-2.8 2.75.65 3.9L8 11.1l-3.5 1.85.65-3.9-2.8-2.75 3.9-.55z"/>', 'fill="currentColor"'));
@@ -270,4 +271,15 @@ function iconEl(id, doc = document) {
270
271
  return span;
271
272
  }
272
273
 
273
- export { ACCENT, ACCENT_BRIGHT, BEARISH, BULLISH, CATEGORICAL, CHIP_PLATE, CROSSHAIR, FIELD_FOCUS_CSS, FIELD_FOCUS_RING, HIGHLIGHT, INFO, INVALID, MARKER, NEUTRAL, SERIES_LINE, SESSION_OFF, SESSION_POST, SESSION_PRE, SLATE, SLATE_DEEP, TRADE_EXIT, TRADE_LONG, TRADE_SHORT, VALID, WARNING, categoricalColor, icon, iconAt, iconEl, iconMarkup, injectStyles, isDarkColor, mix, overlayScrollbarCss, registerIcon, svg16, svg24, svg24Solid, withAlpha };
274
+ // src/ui/surface-events.ts
275
+ var SURFACE_OPEN_EVENT = "vela:surface-open";
276
+ var SURFACE_CLOSE_EVENT = "vela:surface-close";
277
+ function announceSurface(el, open, kind, trigger = null) {
278
+ const View = el.ownerDocument.defaultView;
279
+ if (!View) return;
280
+ el.dispatchEvent(
281
+ new View.CustomEvent(open ? SURFACE_OPEN_EVENT : SURFACE_CLOSE_EVENT, { bubbles: true, composed: true, detail: { kind, trigger } })
282
+ );
283
+ }
284
+
285
+ export { ACCENT, ACCENT_BRIGHT, BEARISH, BULLISH, CATEGORICAL, CHIP_PLATE, CROSSHAIR, FIELD_FOCUS_CSS, FIELD_FOCUS_RING, HIGHLIGHT, INFO, INVALID, MARKER, NEUTRAL, SERIES_LINE, SESSION_OFF, SESSION_POST, SESSION_PRE, SLATE, SLATE_DEEP, SURFACE_CLOSE_EVENT, SURFACE_OPEN_EVENT, TRADE_EXIT, TRADE_LONG, TRADE_SHORT, VALID, WARNING, announceSurface, categoricalColor, icon, iconAt, iconEl, iconMarkup, injectStyles, isDarkColor, mix, overlayScrollbarCss, registerIcon, svg16, svg24, svg24Solid, withAlpha };
@@ -1,4 +1,4 @@
1
- import { overlayScrollbarCss, isDarkColor, mix, HIGHLIGHT, ACCENT_BRIGHT, ACCENT, withAlpha, injectStyles, iconAt, iconEl, BULLISH, BEARISH, WARNING, NEUTRAL } from './chunk-BZQM2XO7.js';
1
+ import { overlayScrollbarCss, isDarkColor, mix, HIGHLIGHT, ACCENT_BRIGHT, ACCENT, withAlpha, injectStyles, announceSurface, iconAt, iconEl, BULLISH, BEARISH, WARNING, NEUTRAL } from './chunk-HKNO24GD.js';
2
2
  import { VanillaMachine, normalizeProps, spreadProps } from '@zag-js/vanilla';
3
3
  export { normalizeProps, spreadProps } from '@zag-js/vanilla';
4
4
  import * as menu from '@zag-js/menu';
@@ -290,6 +290,8 @@ var Surface = class _Surface {
290
290
  /** Trigger rect captured when this level opened — the list stays put if the
291
291
  * trigger then moves (favorite chips shifting the caret). */
292
292
  this.pinnedAnchor = null;
293
+ /** The open state last announced — see {@link announce}. */
294
+ this.shown = false;
293
295
  this.doc = doc;
294
296
  this.host = opts.host;
295
297
  this.onSelect = opts.onSelect;
@@ -303,6 +305,7 @@ var Surface = class _Surface {
303
305
  this.positioner.appendChild(this.list);
304
306
  this.host.appendChild(this.positioner);
305
307
  const trigger = opts.trigger;
308
+ this.opener = trigger ?? opts.opener ?? null;
306
309
  this.ctrl = menuController({
307
310
  items: [],
308
311
  id: opts.id,
@@ -324,15 +327,21 @@ var Surface = class _Surface {
324
327
  if (opts.triggerId && trigger) trigger.id = opts.triggerId;
325
328
  this.handle = runMachine(this.ctrl.machine, this.ctrl.props, (service) => {
326
329
  const api = this.ctrl.connect(service);
330
+ if (this.shown && !api.open) this.announce(false);
327
331
  if (trigger) spreadProps(trigger, api.getTriggerProps(), this.mid);
328
332
  spreadProps(this.positioner, api.getPositionerProps(), this.mid);
329
333
  spreadProps(this.list, api.getContentProps(), this.mid);
330
334
  this.project(api);
335
+ if (api.open && !this.shown) this.announce(true);
331
336
  });
332
337
  }
333
338
  get api() {
334
339
  return this.ctrl.connect(this.handle.service);
335
340
  }
341
+ announce(open2) {
342
+ this.shown = open2;
343
+ announceSurface(this.list, open2, "menu", this.opener);
344
+ }
336
345
  setItems(items) {
337
346
  this.items = items;
338
347
  for (const sub of this.subs.values()) sub.destroy();
@@ -350,6 +359,7 @@ var Surface = class _Surface {
350
359
  for (const sub of this.subs.values()) sub.destroy();
351
360
  this.subs.clear();
352
361
  this.handle.stop();
362
+ if (this.shown) this.announce(false);
353
363
  this.positioner.remove();
354
364
  }
355
365
  byId(id) {
@@ -443,6 +453,7 @@ var Surface = class _Surface {
443
453
  const sub = new _Surface(doc, {
444
454
  host: this.host,
445
455
  placement: "right-start",
456
+ opener: li,
446
457
  onSelect: this.onSelect,
447
458
  onFavorite: this.onFavorite,
448
459
  checkmarks: this.checkmarks,
@@ -662,16 +673,21 @@ var Popover = class {
662
673
  this.hide();
663
674
  };
664
675
  const onReflow = () => this.place();
665
- setTimeout(() => document.addEventListener("pointerdown", onOutside, true), 0);
676
+ setTimeout(() => {
677
+ if (this.onOutside === onOutside) document.addEventListener("pointerdown", onOutside, true);
678
+ }, 0);
666
679
  document.addEventListener("keydown", onKey, true);
667
680
  window.addEventListener("resize", onReflow, true);
668
681
  document.addEventListener("scroll", onReflow, true);
669
682
  this.onOutside = onOutside;
670
683
  this.onKey = onKey;
671
684
  this.onReflow = onReflow;
685
+ announceSurface(this.el, true, "popover", this.trigger);
672
686
  }
673
687
  hide() {
674
688
  if (!this.shown) return;
689
+ this.shown = false;
690
+ announceSurface(this.el, false, "popover", this.trigger);
675
691
  if (this.onOutside) document.removeEventListener("pointerdown", this.onOutside, true);
676
692
  if (this.onKey) document.removeEventListener("keydown", this.onKey, true);
677
693
  if (this.onReflow) {
@@ -691,7 +707,6 @@ var Popover = class {
691
707
  } else {
692
708
  this.el.remove();
693
709
  }
694
- this.shown = false;
695
710
  if (open === this) open = null;
696
711
  this.ctrl.onClose?.();
697
712
  }
@@ -1377,6 +1392,8 @@ ${overlayScrollbarCss(".vela-dialog *")}
1377
1392
  `;
1378
1393
  var Dialog = class {
1379
1394
  constructor(opts = {}) {
1395
+ this.opener = null;
1396
+ this.wasOpen = false;
1380
1397
  const doc = (opts.host ?? document.body).ownerDocument;
1381
1398
  injectStyles(DIALOG_STYLE_ID, DIALOG_CSS, doc);
1382
1399
  const host = opts.host ?? doc.body;
@@ -1473,6 +1490,7 @@ var Dialog = class {
1473
1490
  const mid = String(this.ctrl.props.id);
1474
1491
  this.handle = runMachine(this.ctrl.machine, this.ctrl.props, (service) => {
1475
1492
  const api = this.ctrl.connect(service);
1493
+ if (this.wasOpen && !api.open) this.announce(false);
1476
1494
  spreadProps(this.backdrop, api.getBackdropProps(), mid);
1477
1495
  spreadProps(this.positioner, api.getPositionerProps(), mid);
1478
1496
  spreadProps(this.panel, api.getContentProps(), mid);
@@ -1480,12 +1498,27 @@ var Dialog = class {
1480
1498
  spreadProps(close, api.getCloseTriggerProps(), mid);
1481
1499
  this.backdrop.style.display = api.open ? "" : "none";
1482
1500
  this.positioner.style.display = api.open ? "" : "none";
1501
+ if (api.open && !this.wasOpen) this.announce(true);
1502
+ if (this.wasOpen && !api.open) this.restoreOpener();
1503
+ this.wasOpen = api.open;
1483
1504
  });
1484
1505
  }
1506
+ /** Closing hands focus back to whatever opened it (Escape, the ✕, hide(), or a
1507
+ * teardown that outruns the machine's close notification). */
1508
+ restoreOpener() {
1509
+ const opener = this.opener;
1510
+ this.opener = null;
1511
+ if (opener?.isConnected) opener.focus({ preventScroll: true });
1512
+ }
1513
+ announce(open2) {
1514
+ const opener = this.opener === this.panel.ownerDocument.body ? null : this.opener;
1515
+ announceSurface(this.panel, open2, "dialog", opener);
1516
+ }
1485
1517
  get open() {
1486
1518
  return this.ctrl.connect(this.handle.service).open;
1487
1519
  }
1488
1520
  show() {
1521
+ this.opener = this.panel.ownerDocument.activeElement;
1489
1522
  this.ctrl.connect(this.handle.service).setOpen(true);
1490
1523
  }
1491
1524
  hide() {
@@ -1500,6 +1533,11 @@ var Dialog = class {
1500
1533
  destroy() {
1501
1534
  if (this.open) this.hide();
1502
1535
  this.handle.stop();
1536
+ if (this.wasOpen) {
1537
+ this.wasOpen = false;
1538
+ this.announce(false);
1539
+ }
1540
+ this.restoreOpener();
1503
1541
  this.backdrop.remove();
1504
1542
  this.positioner.remove();
1505
1543
  }
@@ -1,8 +1,8 @@
1
- import { parseSymbol, priceStyleIds, BUILTIN_PRICE_STYLES, tzButtonLabel, SecondClock, resolveTimezone, timezoneMenuRows, isGroupRow, defaultMemberOf, groupKeyOf, groupMembers, LEGEND_AT_TOP_ATTR, normalizeSession, Vela, normalizeTimezone, isExchangeTimezone, TypedEventBus, timeframeToMs, MultiProviderFeed, registerBuiltinChartTypes, resolveTheme, buildToolbar, createCustomMark, createAttributionMark, DrawingToolbar, applyAttributionMarkTheme, sharedBarStore } from './chunk-ATGTCWHJ.js';
2
- import { chartType, resolveTopbarComposition, topbarActionOverride, TOPBAR_BUILTIN_IDS, widgetActions, SidePanel, sidePanels, DEFAULT_PANEL_ORDER, symbolRanking, resolveEngines, legendActionsProviderFor, legendCalloutsProviderFor, statePersistenceHandlers, topbarHas, rendererDefaults, mobilePlacement, widgetAttachments, getDrawingType, inputDeltas } from './chunk-EZ5FWVLA.js';
3
- import { Tooltip, KeymapManager, isEditableTarget, Drawer } from './chunk-LMBYEGUR.js';
4
- import { Menu, Dialog, CalloutBubble, applyPlotOverlayTokens, ensureUIHost } from './chunk-NELQJCGK.js';
5
- import { registerIcon, svg16, injectStyles, iconEl, iconMarkup, iconAt, SESSION_OFF, SESSION_POST, SESSION_PRE, icon, categoricalColor } from './chunk-BZQM2XO7.js';
1
+ import { parseSymbol, priceStyleIds, BUILTIN_PRICE_STYLES, tzButtonLabel, SecondClock, resolveTimezone, timezoneMenuRows, isGroupRow, defaultMemberOf, groupKeyOf, groupMembers, LEGEND_AT_TOP_ATTR, normalizeSession, Vela, normalizeTimezone, isExchangeTimezone, TypedEventBus, timeframeToMs, MultiProviderFeed, registerBuiltinChartTypes, resolveTheme, buildToolbar, createCustomMark, createAttributionMark, DrawingToolbar, applyAttributionMarkTheme, sharedBarStore } from './chunk-T3DZIR5Y.js';
2
+ import { chartType, resolveTopbarComposition, topbarActionOverride, TOPBAR_BUILTIN_IDS, widgetActions, SidePanel, sidePanels, DEFAULT_PANEL_ORDER, symbolRanking, resolveEngines, legendActionsProviderFor, legendCalloutsProviderFor, statePersistenceHandlers, topbarHas, rendererDefaults, mobilePlacement, widgetAttachments, getDrawingType, inputDeltas } from './chunk-DPAA3SRT.js';
3
+ import { Tooltip, KeymapManager, isEditableTarget, Drawer } from './chunk-27UTYE7T.js';
4
+ import { Menu, Dialog, CalloutBubble, applyPlotOverlayTokens, ensureUIHost } from './chunk-NLDTVFOI.js';
5
+ import { registerIcon, svg16, injectStyles, iconEl, iconMarkup, iconAt, SESSION_OFF, SESSION_POST, SESSION_PRE, announceSurface, icon, categoricalColor } from './chunk-HKNO24GD.js';
6
6
  import { baseOf } from './chunk-W4EJWLEO.js';
7
7
 
8
8
  // src/widget/timeframe.ts
@@ -261,6 +261,7 @@ var LayoutPicker = class {
261
261
  const panel = doc.createElement("div");
262
262
  panel.className = "vela-lp";
263
263
  this.layer.appendChild(panel);
264
+ this.panel = panel;
264
265
  const cols = doc.createElement("div");
265
266
  cols.className = "vela-lp-cols";
266
267
  panel.appendChild(cols);
@@ -342,10 +343,12 @@ var LayoutPicker = class {
342
343
  this.doc.addEventListener("pointerdown", this.onDocPointerDown, true);
343
344
  this.doc.addEventListener("keydown", this.onDocKeydown, true);
344
345
  this.opts.onOpenChange?.(true);
346
+ if (this.isOpen) announceSurface(this.panel, true, "popover", this.opts.trigger);
345
347
  }
346
348
  close() {
347
349
  if (!this.isOpen) return;
348
350
  this.isOpen = false;
351
+ announceSurface(this.panel, false, "popover", this.opts.trigger);
349
352
  this.layer.style.display = "none";
350
353
  this.opts.trigger.setAttribute("aria-expanded", "false");
351
354
  this.doc.removeEventListener("pointerdown", this.onDocPointerDown, true);
@@ -2813,19 +2816,22 @@ var PanelDock = class {
2813
2816
  /**
2814
2817
  * (Re)build the CONTRIBUTED panels from the registry — call once after the built-ins, and
2815
2818
  * again on `refreshActions()` so a late registration appears. Contributed panels that are
2816
- * gone from the registry are dropped; the ones still there are rebuilt, so a replaced
2817
- * descriptor takes effect.
2819
+ * gone from the registry are dropped and a REPLACED descriptor is rebuilt; a panel whose
2820
+ * descriptor is unchanged stays mounted as it is, so its content and state survive.
2818
2821
  */
2819
2822
  refresh() {
2820
2823
  const openBefore = this.openId;
2821
- for (const entry of [...this.entries]) if (entry.contributed) this.drop(entry);
2822
- for (const desc of sidePanels()) {
2824
+ const registered = sidePanels();
2825
+ for (const entry of [...this.entries]) if (entry.contributed && !registered.includes(entry.desc)) this.drop(entry);
2826
+ for (const desc of registered) {
2827
+ if (this.entries.some((e) => e.desc === desc)) continue;
2823
2828
  const panel = new SidePanel(this.host, desc.title, `vela-panel-${desc.id}`, {
2824
2829
  width: desc.width,
2825
2830
  resizable: desc.resizable,
2826
2831
  minWidth: desc.minWidth,
2827
2832
  maxWidth: desc.maxWidth,
2828
- overlay: desc.overlay
2833
+ overlay: desc.overlay,
2834
+ maximizable: desc.maximizable
2829
2835
  });
2830
2836
  const entry = {
2831
2837
  id: desc.id,
@@ -2833,10 +2839,19 @@ var PanelDock = class {
2833
2839
  icon: desc.icon,
2834
2840
  order: desc.order ?? DEFAULT_PANEL_ORDER,
2835
2841
  panel,
2836
- contributed: true
2842
+ contributed: true,
2843
+ desc
2837
2844
  };
2838
2845
  try {
2839
- entry.handle = desc.mount(this.deps.context(), panel.content, { slot: panel.headerSlot, setTitle: (t) => panel.setTitle(t) }) ?? void 0;
2846
+ const header = {
2847
+ slot: panel.headerSlot,
2848
+ setTitle: (t) => panel.setTitle(t),
2849
+ get maximized() {
2850
+ return panel.maximized;
2851
+ },
2852
+ setMaximized: (on) => panel.setMaximized(on)
2853
+ };
2854
+ entry.handle = desc.mount(this.deps.context(), panel.content, header) ?? void 0;
2840
2855
  if (this.chart) entry.handle?.onChart?.(this.chart);
2841
2856
  } catch (err) {
2842
2857
  console.warn(`[vela] side panel "${desc.id}" failed to mount`, err);
@@ -4223,6 +4238,9 @@ var CSS10 = `
4223
4238
  .vela-statusline .vela-sl-market { align-self: center; display: inline-flex; }
4224
4239
  .vela-statusline .vela-sl-market > [hidden] { display: none !important; }
4225
4240
  .vela-statusline .vela-sl-replay-badge, .vela-statusline .vela-sl-replay-badge svg { display: block; width: 16px; height: 16px; }
4241
+ /* Tabular figures: every digit has one advance, so a readout whose text changes only in
4242
+ * its digits keeps its width \u2014 render() skips the ladder for those (see widthKey). */
4243
+ .vela-statusline .vela-sl-values { font-variant-numeric: tabular-nums; }
4226
4244
  .vela-statusline .vela-sl-ohlc { display: flex; gap: var(--vela-space-1); color: var(--vela-fg-muted); }
4227
4245
  .vela-statusline .vela-sl-ohlc b { color: var(--vela-fg); font-weight: 500; }
4228
4246
  /* The change value wears the SAME ink as the OHLC values (set inline per render) \u2014
@@ -4340,6 +4358,10 @@ var Statusline = class {
4340
4358
  this.fitRO = null;
4341
4359
  /** The ladder rung currently applied — see {@link fit}. */
4342
4360
  this.layout = { stacked: false, level: "full" };
4361
+ /** The readout {@link widthKey} the applied rung was fitted to. */
4362
+ this.fitKey = null;
4363
+ /** Web fonts landing after the last fit change every width without a resize. */
4364
+ this.onFontsLoaded = () => this.fit();
4343
4365
  this.onContextMenu = (e) => {
4344
4366
  if (!this.menu || !this.menuHooks) return;
4345
4367
  e.preventDefault();
@@ -4403,6 +4425,7 @@ var Statusline = class {
4403
4425
  this.fitRO = new ResizeObserver(() => this.fit());
4404
4426
  this.fitRO.observe(host);
4405
4427
  }
4428
+ doc.fonts?.addEventListener("loadingdone", this.onFontsLoaded);
4406
4429
  this.marketTip = new Tooltip(this.marketEl, { content: MARKET_LABELS.open, placement: "bottom" });
4407
4430
  this.eyeTip = new Tooltip(this.eyeEl, { content: "Show chart", placement: "bottom" });
4408
4431
  this.setMarketStatus("open");
@@ -4455,6 +4478,7 @@ var Statusline = class {
4455
4478
  * (detached host, node tests) nothing overflows, so the widest rung stays.
4456
4479
  */
4457
4480
  fit() {
4481
+ this.fitKey = this.widthKey();
4458
4482
  this.syncParts();
4459
4483
  const seg = segmentVisibility(this.parts, this.chartHidden);
4460
4484
  const overflows = () => this.el.scrollWidth > this.el.clientWidth;
@@ -4611,6 +4635,7 @@ var Statusline = class {
4611
4635
  this.detach();
4612
4636
  this.fitRO?.disconnect();
4613
4637
  this.fitRO = null;
4638
+ this.el.ownerDocument.fonts?.removeEventListener("loadingdone", this.onFontsLoaded);
4614
4639
  this.el.removeEventListener("contextmenu", this.onContextMenu);
4615
4640
  this.menu?.destroy();
4616
4641
  this.menu = null;
@@ -4626,7 +4651,22 @@ var Statusline = class {
4626
4651
  }
4627
4652
  render() {
4628
4653
  if (this.menuHooks) this.setChartHidden(!this.menuHooks.chartVisible());
4629
- this.fit();
4654
+ if (this.widthKey() === this.fitKey) this.renderValues();
4655
+ else this.fit();
4656
+ }
4657
+ /** Everything the readout's width depends on, at every rung: its shape, and each
4658
+ * value's sign, digit count and decimals (the digits themselves are tabular — see
4659
+ * the CSS; the grouping commas follow from the digit count). `toFixed` carries all
4660
+ * three at a fraction of the locale formatter's cost — this runs per pointer move.
4661
+ * The parts, the host's size and the fonts re-fit through their own paths. */
4662
+ widthKey() {
4663
+ const bar = this.hoverBar ?? this.lastBar;
4664
+ if (!bar) return this.readout;
4665
+ const dp = decimalsFor(bar.close);
4666
+ const diff = bar.close - bar.open;
4667
+ const text = [bar.open, bar.high, bar.low, bar.close, diff].map((v) => v.toFixed(dp));
4668
+ text.push((diff / bar.open * 100).toFixed(2));
4669
+ return `${this.readout} ${text.join(" ")}`.replace(/\d/g, "0");
4630
4670
  }
4631
4671
  /** Write the value readout for the current bar at the current ladder level. */
4632
4672
  renderValues() {