@luxalgo/vela 0.7.8 → 0.8.1

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 (47) hide show
  1. package/NOTICE +28 -28
  2. package/README.md +15 -7
  3. package/dist/{DataProvider-CGa4KHRa.d.cts → DataProvider-BrlxntPN.d.cts} +1 -1
  4. package/dist/{DataProvider-CmWUtDVL.d.ts → DataProvider-DwFIoV4r.d.ts} +1 -1
  5. package/dist/{chunk-N4ZZYLMP.js → chunk-ATGTCWHJ.js} +726 -273
  6. package/dist/{chunk-CAFCLMPF.js → chunk-BZQM2XO7.js} +1 -0
  7. package/dist/{chunk-M6GP7DAX.js → chunk-DBDXKZUF.js} +474 -47
  8. package/dist/{chunk-BFA32GOU.js → chunk-EZ5FWVLA.js} +5 -2
  9. package/dist/{chunk-PVGWNF7V.js → chunk-LMBYEGUR.js} +2 -2
  10. package/dist/{chunk-WTWGEB7K.js → chunk-NELQJCGK.js} +298 -5
  11. package/dist/{contributions-DF_Ea07m.d.cts → contributions-CtlptxR8.d.cts} +187 -5
  12. package/dist/{contributions-nKOSeLy8.d.ts → contributions-tjNExo1o.d.ts} +187 -5
  13. package/dist/index.cjs +1044 -272
  14. package/dist/index.d.cts +24 -8
  15. package/dist/index.d.ts +24 -8
  16. package/dist/index.js +28 -4
  17. package/dist/{options-D1AJKsn_.d.ts → options-BnEPUuVV.d.cts} +1 -1
  18. package/dist/{options-D1AJKsn_.d.cts → options-BnEPUuVV.d.ts} +1 -1
  19. package/dist/{plugin-6F04gjQe.d.cts → plugin-BWxJRKzS.d.cts} +10 -4
  20. package/dist/{plugin-BLsNipkc.d.ts → plugin-e2WUEtiI.d.ts} +10 -4
  21. package/dist/plugin.cjs +1 -0
  22. package/dist/plugin.d.cts +4 -4
  23. package/dist/plugin.d.ts +4 -4
  24. package/dist/plugin.js +2 -2
  25. package/dist/providers/binance.d.cts +2 -2
  26. package/dist/providers/binance.d.ts +2 -2
  27. package/dist/providers/coinbase.d.cts +2 -2
  28. package/dist/providers/coinbase.d.ts +2 -2
  29. package/dist/providers/hyperliquid.d.cts +2 -2
  30. package/dist/providers/hyperliquid.d.ts +2 -2
  31. package/dist/{statusline-model-DAFyqVmR.d.ts → statusline-model-DFY7tuHj.d.cts} +5 -3
  32. package/dist/{statusline-model-BoWJ3cWQ.d.cts → statusline-model-zKXtGIjj.d.ts} +5 -3
  33. package/dist/ui.cjs +301 -3
  34. package/dist/ui.d.cts +75 -2
  35. package/dist/ui.d.ts +75 -2
  36. package/dist/ui.js +3 -3
  37. package/dist/vela.global.js +1046 -274
  38. package/dist/vela.global.min.js +82 -87
  39. package/dist/widget.cjs +1504 -327
  40. package/dist/widget.d.cts +26 -7
  41. package/dist/widget.d.ts +26 -7
  42. package/dist/widget.js +7 -7
  43. package/dist/workspace.cjs +1507 -327
  44. package/dist/workspace.d.cts +125 -7
  45. package/dist/workspace.d.ts +125 -7
  46. package/dist/workspace.js +6 -6
  47. package/package.json +2 -2
@@ -1,6 +1,6 @@
1
- import { deserializeDrawing, chartTypes, rendererLayers, isLineLikeSeries, seriesShownOn, foldBaseModulation, registerChartType, rendererDefaults, getDrawingType, settingsRowVisible, normalizeSettingsRow, Magnifier, TextLabel, Callout, resetDrawingSettings, registerNativeIndicator, chartType, getNativeIndicator, nativeIndicatorDescriptors, nativeInstanceChannel, drawingTypes, settingsRowValueKeys, formatDuration, RegressionChannel, FixedRangeVolumeProfile, DEFAULT_DRAWING_COLOR, SegmentDrawing, ArrowMark, GlyphStamp, RadialFib, FibSpiral, DedekindTessellation, MachFigure, GannSquare, FibRatios, MeasureBox, PositionTool, AnchoredVwap, PatternDrawing, Comment, PriceNote, Signpost, Note, PriceLabel, GANN_SQUARE_ARCS, magnifierTimeframeLabel, lineSegmentIntersection, MAGNIFIER_TIMEFRAME_OPTIONS, GLYPH_OPTIONS, STAMP_SIZE_OPTIONS, LINE_STYLE_OPTIONS, DEDEKIND_CURVATURE_OPTIONS, MACH_NUMBER_OPTIONS, MACH_WAVE_COUNT_OPTIONS, TEXT_SIZE_OPTIONS, createDrawing, inputVisible, seriesInScale, tickerModifierIds, CalloutBase, DIRECTION_OPTIONS, stableSeriesId } from './chunk-BFA32GOU.js';
2
- import { themeTokens, Dialog, closeOpenPopovers, closeWidthPopover, fieldSection, buildFieldControl, fieldSeparator, fieldGrid, fieldRow, CALLOUT_STYLE_ID, CALLOUT_CSS, Popover, blendOver, splitColor, CalloutBubble, Menu, TextArea, NumberInput, buildColorPicker, isPopoverOpen, eventDismissedPopover, toggleSelectList, openPopoverTrigger, fieldGridColumns, FIELD_GAP_PX, STATIC_TOKENS } from './chunk-WTWGEB7K.js';
3
- import { icon, iconAt, withAlpha, WARNING, ACCENT, NEUTRAL, INFO, BULLISH, BEARISH, isDarkColor, injectStyles, SERIES_LINE, CHIP_PLATE, CROSSHAIR, TRADE_EXIT, TRADE_SHORT, TRADE_LONG, iconMarkup, overlayScrollbarCss, SLATE_DEEP, VALID, INVALID, SLATE, FIELD_FOCUS_CSS, FIELD_FOCUS_RING, svg24, svg24Solid } from './chunk-CAFCLMPF.js';
1
+ import { deserializeDrawing, chartTypes, rendererLayers, isLineLikeSeries, seriesShownOn, foldBaseModulation, registerChartType, rendererDefaults, getDrawingType, settingsRowVisible, normalizeSettingsRow, Magnifier, TextLabel, Callout, resetDrawingSettings, registerNativeIndicator, chartType, getNativeIndicator, nativeIndicatorDescriptors, nativeInstanceChannel, drawingTypes, settingsRowValueKeys, formatDuration, RegressionChannel, FixedRangeVolumeProfile, DEFAULT_DRAWING_COLOR, SegmentDrawing, ArrowMark, GlyphStamp, RadialFib, FibSpiral, DedekindTessellation, MachFigure, GannSquare, FibRatios, MeasureBox, PositionTool, AnchoredVwap, PatternDrawing, Comment, PriceNote, Signpost, Note, PriceLabel, GANN_SQUARE_ARCS, magnifierTimeframeLabel, lineSegmentIntersection, MAGNIFIER_TIMEFRAME_OPTIONS, GLYPH_OPTIONS, STAMP_SIZE_OPTIONS, LINE_STYLE_OPTIONS, DEDEKIND_CURVATURE_OPTIONS, MACH_NUMBER_OPTIONS, MACH_WAVE_COUNT_OPTIONS, TEXT_SIZE_OPTIONS, createDrawing, inputVisible, seriesInScale, tickerModifierIds, CalloutBase, DIRECTION_OPTIONS, stableSeriesId } from './chunk-EZ5FWVLA.js';
2
+ import { themeTokens, Dialog, closeOpenPopovers, closeWidthPopover, fieldSection, buildFieldControl, fieldSeparator, fieldGrid, fieldRow, CALLOUT_STYLE_ID, CALLOUT_CSS, Popover, blendOver, splitColor, CalloutBubble, Menu, TextArea, NumberInput, buildColorPicker, isPopoverOpen, DatePicker, eventDismissedPopover, toggleSelectList, normalizeDateInput, openPopoverTrigger, fieldGridColumns, FIELD_GAP_PX, STATIC_TOKENS } from './chunk-NELQJCGK.js';
3
+ import { icon, iconAt, withAlpha, WARNING, ACCENT, NEUTRAL, INFO, BULLISH, BEARISH, isDarkColor, injectStyles, SERIES_LINE, CHIP_PLATE, CROSSHAIR, TRADE_EXIT, TRADE_SHORT, TRADE_LONG, iconMarkup, overlayScrollbarCss, SLATE_DEEP, VALID, INVALID, SLATE, FIELD_FOCUS_CSS, FIELD_FOCUS_RING, svg24, svg24Solid } from './chunk-BZQM2XO7.js';
4
4
 
5
5
  // src/core/events/EventBus.ts
6
6
  var TypedEventBus = class {
@@ -514,6 +514,12 @@ var RendererControl = class {
514
514
  onCrosshairMove(cb) {
515
515
  return this.renderer.onCrosshairMove(cb);
516
516
  }
517
+ /** A click — or a touch tap — on the plot, never the end of a pan: the open time of
518
+ * the bar under it (`null` off the bars). On touch, where a tap moves no crosshair,
519
+ * this is how an interaction learns which bar was chosen. */
520
+ onClick(cb) {
521
+ return this.renderer.onClick(cb);
522
+ }
517
523
  /** Touch long-press on a price or time axis strip — silent no-op without the seam. */
518
524
  onAxisLongPress(cb) {
519
525
  return this.renderer.onAxisLongPress?.(cb) ?? (() => void 0);
@@ -1780,11 +1786,76 @@ var MarksControl = class {
1780
1786
  }
1781
1787
  };
1782
1788
 
1789
+ // src/core/ReplayControl.ts
1790
+ var ReplayControl = class {
1791
+ constructor(ctrl) {
1792
+ this.ctrl = ctrl;
1793
+ }
1794
+ /**
1795
+ * Enter replay (or seek, when already replaying) at `opts.from`. Resolves once the
1796
+ * chart shows the rewound history — after any in-flight history load has finished.
1797
+ * Starts paused; call {@link play} or {@link step}.
1798
+ */
1799
+ start(opts) {
1800
+ return this.ctrl.replayStart(opts);
1801
+ }
1802
+ /** Reveal the next bar — or, mid-way through a bar played tick by tick, complete it.
1803
+ * Returns false when replay is off or nothing is left. */
1804
+ step() {
1805
+ return this.ctrl.replayStep();
1806
+ }
1807
+ /** Reveal the next UPDATE: with {@link setTicks}, the next tick of the forming bar — or
1808
+ * the next bar opened at its first tick (once its ticks are in, if still loading);
1809
+ * otherwise the next whole bar, like {@link step}. Returns false when nothing is left. */
1810
+ stepUpdate() {
1811
+ return this.ctrl.replayStepUpdate();
1812
+ }
1813
+ /** Reveal one bar (one tick, with {@link setTicks}) every `intervalMs` (default: the last
1814
+ * interval used, else 1000). Calling it again while playing changes the pace. */
1815
+ play(intervalMs2) {
1816
+ this.ctrl.replayPlay(intervalMs2);
1817
+ return this;
1818
+ }
1819
+ pause() {
1820
+ this.ctrl.replayPause();
1821
+ return this;
1822
+ }
1823
+ /** Leave replay: the full history comes back and live updates resume. Also cancels a
1824
+ * `start()` still loading older history (the chart returns to its previous depth). */
1825
+ stop() {
1826
+ this.ctrl.replayStop();
1827
+ return this;
1828
+ }
1829
+ get state() {
1830
+ return this.ctrl.replayState();
1831
+ }
1832
+ /** The loaded history a replay can walk — hidden bars included while replaying. Null
1833
+ * before any bar loaded. A start older than `first` deepens the history first. */
1834
+ get bounds() {
1835
+ return this.ctrl.replayBounds();
1836
+ }
1837
+ /**
1838
+ * Play each revealed bar as a series of intrabar updates instead of whole: while
1839
+ * playing, the forming candle opens at the first tick, then every tick moves its close
1840
+ * and stretches its high and low, exactly like a live tick — indicators, chart types
1841
+ * and the `bar` event follow — and the last one settles it on the stored bar. The play
1842
+ * interval is then the time between two TICKS: a bar lasts as many intervals as it
1843
+ * has ticks (below ~16 ms, ticks are batched per frame). The next bar's ticks are
1844
+ * requested while the current one plays; playback waits for them. `null` goes back to
1845
+ * whole bars. Takes effect from the next bar; a bar forming when the source changes
1846
+ * completes at once.
1847
+ */
1848
+ setTicks(source) {
1849
+ this.ctrl.replaySetTicks(source);
1850
+ return this;
1851
+ }
1852
+ };
1853
+
1783
1854
  // src/core/util/wall-clock.ts
1784
1855
  var BOUNDARY_SLACK_MS = 5;
1785
1856
  var SecondClock = class _SecondClock {
1786
- constructor(now = () => Date.now()) {
1787
- this.now = now;
1857
+ constructor(now2 = () => Date.now()) {
1858
+ this.now = now2;
1788
1859
  this.subs = /* @__PURE__ */ new Set();
1789
1860
  this.timer = null;
1790
1861
  }
@@ -1800,15 +1871,15 @@ var SecondClock = class _SecondClock {
1800
1871
  };
1801
1872
  }
1802
1873
  /** Milliseconds from `now` to just past the next second boundary. */
1803
- static delayToNextSecond(now) {
1804
- const intoSecond = (now % 1e3 + 1e3) % 1e3;
1874
+ static delayToNextSecond(now2) {
1875
+ const intoSecond = (now2 % 1e3 + 1e3) % 1e3;
1805
1876
  return 1e3 - intoSecond + BOUNDARY_SLACK_MS;
1806
1877
  }
1807
1878
  arm() {
1808
1879
  this.timer = setTimeout(() => {
1809
1880
  this.timer = null;
1810
- const now = this.now();
1811
- for (const cb of this.subs) cb(now);
1881
+ const now2 = this.now();
1882
+ for (const cb of this.subs) cb(now2);
1812
1883
  if (this.subs.size > 0) this.arm();
1813
1884
  }, _SecondClock.delayToNextSecond(this.now()));
1814
1885
  }
@@ -3801,10 +3872,16 @@ var IndicatorInputsDialog = class {
3801
3872
  this.calendarPop = null;
3802
3873
  this.calendarAnchor = null;
3803
3874
  },
3804
- content: (el) => fillCalendar(el, current, (iso) => {
3805
- pop.hide();
3806
- onPick(iso);
3807
- })
3875
+ content: (el) => {
3876
+ const picker = new DatePicker({
3877
+ value: current,
3878
+ onPick: (iso) => {
3879
+ pop.hide();
3880
+ onPick(iso);
3881
+ }
3882
+ });
3883
+ el.append(picker.el);
3884
+ }
3808
3885
  });
3809
3886
  this.calendarPop = pop;
3810
3887
  this.calendarAnchor = anchor;
@@ -3929,28 +4006,6 @@ function ensureDialogStyles() {
3929
4006
  .vela-ind-combo-chevron{position:absolute;right:0;top:0;bottom:0;width:26px;border:none;background:transparent;color:inherit;opacity:0.55;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;}
3930
4007
  .vela-ind-combo-chevron:hover{opacity:0.9;}
3931
4008
  .vela-ind-cal{background:var(--vela-bg);color:var(--vela-fg);border:none;border-radius:6px;box-shadow:var(--vela-shadow);font:14px var(--vela-font);padding:10px 12px;user-select:none;}
3932
- .vela-ind-cal [hidden]{display:none !important;}
3933
- .vela-ind-cal-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;}
3934
- .vela-ind-cal-title{flex:1;display:flex;align-items:center;justify-content:center;gap:2px;min-width:0;}
3935
- .vela-ind-cal-switch{border:none;background:transparent;color:var(--vela-fg-bright);font:inherit;font-weight:600;font-size:14px;padding:2px 6px;border-radius:4px;cursor:pointer;}
3936
- .vela-ind-cal-switch:not(:disabled):hover{background:var(--vela-hover);}
3937
- .vela-ind-cal-switch:disabled{cursor:default;}
3938
- .vela-ind-cal-nav{width:24px;height:24px;border:none;background:transparent;color:var(--vela-fg-muted);border-radius:4px;padding:0;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;}
3939
- .vela-ind-cal-nav:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
3940
- .vela-ind-cal-week,.vela-ind-cal-grid{display:grid;grid-template-columns:repeat(7,28px);gap:2px;}
3941
- .vela-ind-cal-week{margin-bottom:4px;color:var(--vela-fg-muted);font-size:11px;text-align:center;}
3942
- .vela-ind-cal-week span{line-height:20px;}
3943
- .vela-ind-cal-blank{width:28px;height:28px;}
3944
- .vela-ind-cal-day{width:28px;height:28px;border:none;background:transparent;color:inherit;border-radius:4px;padding:0;cursor:pointer;font:inherit;font-size:14px;}
3945
- .vela-ind-cal-day:hover{background:var(--vela-hover);}
3946
- .vela-ind-cal-day[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
3947
- .vela-ind-cal-day[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
3948
- .vela-ind-cal-cells{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;width:calc(7 * 28px + 6 * 2px);}
3949
- .vela-ind-cal-cell{height:36px;border:none;background:transparent;color:inherit;border-radius:4px;padding:0 4px;cursor:pointer;font:inherit;font-size:14px;}
3950
- .vela-ind-cal-cell:hover{background:var(--vela-hover);}
3951
- .vela-ind-cal-cell[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
3952
- .vela-ind-cal-cell[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
3953
- .vela-ind-cal-cell[data-outside]{opacity:0.45;}
3954
4009
  ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
3955
4010
  .vela-ind-tab{font-weight:600;font-size:13px;line-height:20px;transition:color var(--vela-dur-fast) ease,border-color var(--vela-dur-fast) ease;}
3956
4011
  .vela-ind-tab:not(.vela-ind-tab-active):hover{color:var(--vela-fg-bright);}
@@ -3984,174 +4039,6 @@ var TIME_OPTIONS = Array.from({ length: 48 }, (_, i) => {
3984
4039
  const v = `${hh}:${mm}`;
3985
4040
  return { value: v, label: v };
3986
4041
  });
3987
- var MONTH_LABELS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
3988
- var MONTH_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
3989
- var WEEKDAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
3990
- function normalizeDateInput(raw) {
3991
- const t = raw.trim();
3992
- const m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(t);
3993
- if (!m) return null;
3994
- const y = Number(m[1]);
3995
- const mo = Number(m[2]);
3996
- const d = Number(m[3]);
3997
- const dt = new Date(y, mo - 1, d);
3998
- if (dt.getFullYear() !== y || dt.getMonth() !== mo - 1 || dt.getDate() !== d) return null;
3999
- return isoDate(dt);
4000
- }
4001
- function parseIsoDate(raw) {
4002
- const iso = normalizeDateInput(raw);
4003
- if (!iso) return null;
4004
- const [y, mo, d] = iso.split("-").map(Number);
4005
- return new Date(y, mo - 1, d);
4006
- }
4007
- function isoDate(d) {
4008
- return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
4009
- }
4010
- function fillCalendar(el, current, onPick) {
4011
- const today = /* @__PURE__ */ new Date();
4012
- const selected = parseIsoDate(current);
4013
- let year = selected?.getFullYear() ?? today.getFullYear();
4014
- let month = selected?.getMonth() ?? today.getMonth();
4015
- let mode = "date";
4016
- const prev2 = document.createElement("button");
4017
- prev2.type = "button";
4018
- prev2.className = "vela-ind-cal-nav";
4019
- prev2.innerHTML = iconAt("chevron-left", 14);
4020
- const next = document.createElement("button");
4021
- next.type = "button";
4022
- next.className = "vela-ind-cal-nav";
4023
- next.innerHTML = iconAt("chevron-right", 14);
4024
- const monthBtn = document.createElement("button");
4025
- monthBtn.type = "button";
4026
- monthBtn.className = "vela-ind-cal-switch";
4027
- monthBtn.setAttribute("aria-label", "Choose month");
4028
- const yearBtn = document.createElement("button");
4029
- yearBtn.type = "button";
4030
- yearBtn.className = "vela-ind-cal-switch";
4031
- yearBtn.setAttribute("aria-label", "Choose year");
4032
- const title = document.createElement("div");
4033
- title.className = "vela-ind-cal-title";
4034
- title.append(monthBtn, yearBtn);
4035
- const head = document.createElement("div");
4036
- head.className = "vela-ind-cal-head";
4037
- head.append(prev2, title, next);
4038
- const week = document.createElement("div");
4039
- week.className = "vela-ind-cal-week";
4040
- for (const d of WEEKDAY_LABELS) {
4041
- const cell = document.createElement("span");
4042
- cell.textContent = d;
4043
- week.appendChild(cell);
4044
- }
4045
- const grid = document.createElement("div");
4046
- const paint = () => {
4047
- const decade = Math.floor(year / 10) * 10;
4048
- monthBtn.hidden = mode !== "date";
4049
- monthBtn.textContent = MONTH_LABELS[month] ?? "";
4050
- yearBtn.textContent = mode === "year" ? `${decade}-${decade + 9}` : String(year);
4051
- yearBtn.disabled = mode === "year";
4052
- week.hidden = mode !== "date";
4053
- prev2.setAttribute("aria-label", mode === "date" ? "Previous month" : mode === "month" ? "Previous year" : "Previous decade");
4054
- next.setAttribute("aria-label", mode === "date" ? "Next month" : mode === "month" ? "Next year" : "Next decade");
4055
- grid.className = mode === "date" ? "vela-ind-cal-grid" : "vela-ind-cal-cells";
4056
- grid.replaceChildren();
4057
- if (mode === "date") {
4058
- const startPad = new Date(year, month, 1).getDay();
4059
- const days = new Date(year, month + 1, 0).getDate();
4060
- const todayIso = isoDate(today);
4061
- const selectedIso = selected ? isoDate(selected) : "";
4062
- for (let i = 0; i < startPad; i++) {
4063
- const blank = document.createElement("span");
4064
- blank.className = "vela-ind-cal-blank";
4065
- grid.appendChild(blank);
4066
- }
4067
- for (let day = 1; day <= days; day++) {
4068
- const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
4069
- const b = document.createElement("button");
4070
- b.type = "button";
4071
- b.className = "vela-ind-cal-day";
4072
- b.textContent = String(day);
4073
- if (iso === selectedIso) b.dataset.checked = "1";
4074
- if (iso === todayIso) b.dataset.today = "1";
4075
- b.addEventListener("click", (e) => {
4076
- e.stopPropagation();
4077
- onPick(iso);
4078
- });
4079
- grid.appendChild(b);
4080
- }
4081
- return;
4082
- }
4083
- if (mode === "month") {
4084
- for (let m = 0; m < 12; m++) {
4085
- const b = document.createElement("button");
4086
- b.type = "button";
4087
- b.className = "vela-ind-cal-cell";
4088
- b.textContent = MONTH_SHORT[m] ?? "";
4089
- if (selected?.getFullYear() === year && selected.getMonth() === m) b.dataset.checked = "1";
4090
- if (today.getFullYear() === year && today.getMonth() === m) b.dataset.today = "1";
4091
- b.addEventListener("click", (e) => {
4092
- e.stopPropagation();
4093
- month = m;
4094
- mode = "date";
4095
- paint();
4096
- });
4097
- grid.appendChild(b);
4098
- }
4099
- return;
4100
- }
4101
- for (let y = decade - 1; y <= decade + 10; y++) {
4102
- const b = document.createElement("button");
4103
- b.type = "button";
4104
- b.className = "vela-ind-cal-cell";
4105
- b.textContent = String(y);
4106
- if (y < decade || y > decade + 9) b.dataset.outside = "1";
4107
- if (selected?.getFullYear() === y) b.dataset.checked = "1";
4108
- if (today.getFullYear() === y) b.dataset.today = "1";
4109
- b.addEventListener("click", (e) => {
4110
- e.stopPropagation();
4111
- year = y;
4112
- mode = "month";
4113
- paint();
4114
- });
4115
- grid.appendChild(b);
4116
- }
4117
- };
4118
- const step = (dir) => {
4119
- if (mode === "date") {
4120
- month += dir;
4121
- if (month < 0) {
4122
- month = 11;
4123
- year -= 1;
4124
- }
4125
- if (month > 11) {
4126
- month = 0;
4127
- year += 1;
4128
- }
4129
- } else {
4130
- year += mode === "month" ? dir : dir * 10;
4131
- }
4132
- paint();
4133
- };
4134
- prev2.addEventListener("click", (e) => {
4135
- e.stopPropagation();
4136
- step(-1);
4137
- });
4138
- next.addEventListener("click", (e) => {
4139
- e.stopPropagation();
4140
- step(1);
4141
- });
4142
- monthBtn.addEventListener("click", (e) => {
4143
- e.stopPropagation();
4144
- mode = "month";
4145
- paint();
4146
- });
4147
- yearBtn.addEventListener("click", (e) => {
4148
- e.stopPropagation();
4149
- mode = "year";
4150
- paint();
4151
- });
4152
- paint();
4153
- el.append(head, week, grid);
4154
- }
4155
4042
  function normalizeTimeInput(raw) {
4156
4043
  const t = raw.trim();
4157
4044
  const m = /^(\d{1,2}):(\d{2})$/.exec(t) ?? /^(\d{2})(\d{2})$/.exec(t);
@@ -5529,6 +5416,22 @@ function priceStyleIds() {
5529
5416
  for (const t of chartTypes()) if (!out.includes(t.id)) out.push(t.id);
5530
5417
  return out;
5531
5418
  }
5419
+ function sanitizeCrosshairOverride(value) {
5420
+ if (!value || typeof value !== "object") return null;
5421
+ const v = value;
5422
+ const out = {};
5423
+ if (isBool(v.vertical)) out.vertical = v.vertical;
5424
+ if (isBool(v.horizontal)) out.horizontal = v.horizontal;
5425
+ if (isColor(v.color)) out.color = v.color;
5426
+ if (isNum(v.width)) out.width = Math.max(0.5, Math.min(8, v.width));
5427
+ if (isLineStyle(v.style)) out.style = v.style;
5428
+ if (isNum(v.opacity)) out.opacity = clampOpacity(v.opacity);
5429
+ const shade = v.shadeRight;
5430
+ if (shade && typeof shade === "object" && isColor(shade.color)) {
5431
+ out.shadeRight = isNum(shade.opacity) ? { color: shade.color, opacity: clampOpacity(shade.opacity) } : { color: shade.color };
5432
+ }
5433
+ return Object.keys(out).length > 0 ? out : null;
5434
+ }
5532
5435
  function isColor(v) {
5533
5436
  return typeof v === "string" && v.trim().length > 0;
5534
5437
  }
@@ -7190,7 +7093,7 @@ var Scheduler = class {
7190
7093
 
7191
7094
  // src/renderers/native/core/Animator.ts
7192
7095
  var Animator = class {
7193
- constructor(tick, raf, cancel, now) {
7096
+ constructor(tick, raf, cancel, now2) {
7194
7097
  this.tick = tick;
7195
7098
  this.handle = null;
7196
7099
  this.last = 0;
@@ -7204,7 +7107,7 @@ var Animator = class {
7204
7107
  };
7205
7108
  this.raf = raf ?? ((cb) => requestAnimationFrame(cb));
7206
7109
  this.cancel = cancel ?? ((h) => cancelAnimationFrame(h));
7207
- this.now = now ?? (() => performance.now());
7110
+ this.now = now2 ?? (() => performance.now());
7208
7111
  }
7209
7112
  get active() {
7210
7113
  return this.handle !== null;
@@ -7336,6 +7239,12 @@ var InputController = class {
7336
7239
  this.lastTapT = 0;
7337
7240
  this.lastTapX = 0;
7338
7241
  this.lastTapY = 0;
7242
+ // Whether the last two primary presses were claimed by the drawings layer. A double-click
7243
+ // is the last press plus the one before it; a tool-driven click pair must never fall through
7244
+ // to the pane maximize toggle, even when the tool disarmed itself between the two clicks
7245
+ // (a finished placement leaves plain cursor mode for the second click).
7246
+ this.lastPressDrawing = false;
7247
+ this.prevPressDrawing = false;
7339
7248
  this.onTouchContextMenu = (e) => {
7340
7249
  if (this.touches.size === 0) return;
7341
7250
  if (!(e.target instanceof Node) || !this.el?.contains(e.target)) return;
@@ -7390,19 +7299,24 @@ var InputController = class {
7390
7299
  this.moved = false;
7391
7300
  this.startX = x;
7392
7301
  this.startY = y;
7302
+ this.prevPressDrawing = this.lastPressDrawing;
7303
+ this.lastPressDrawing = false;
7393
7304
  if (this.deps.drawingsClaim?.(x, y)) {
7394
7305
  this.region = "drawing";
7306
+ this.lastPressDrawing = true;
7395
7307
  this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
7396
7308
  this.capture(e.pointerId);
7397
7309
  return;
7398
7310
  }
7399
7311
  if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y, this.snapMode(e))) {
7400
7312
  this.region = "drawing";
7313
+ this.lastPressDrawing = true;
7401
7314
  this.capture(e.pointerId);
7402
7315
  return;
7403
7316
  }
7404
7317
  if ((e.ctrlKey || e.metaKey) && this.regionAt(x, y) === "data" && this.deps.drawingsMarqueeStart?.(x, y)) {
7405
7318
  this.region = "drawing";
7319
+ this.lastPressDrawing = true;
7406
7320
  this.capture(e.pointerId);
7407
7321
  return;
7408
7322
  }
@@ -7724,6 +7638,7 @@ var InputController = class {
7724
7638
  if (region === "price") this.deps.resetPriceScale(x, y);
7725
7639
  else if (region === "separator") this.deps.resetPaneSize(y);
7726
7640
  else if (region === "time") this.deps.resetView();
7641
+ else if (this.lastPressDrawing || this.prevPressDrawing) return;
7727
7642
  else this.deps.dataDblClick(x, y);
7728
7643
  }
7729
7644
  };
@@ -8039,6 +7954,8 @@ var SceneGraph = class {
8039
7954
  /** VPVR-layer config pushed by the VPVR native indicator (null ⇒ layer off). Ephemeral. */
8040
7955
  this.vpvrLayer = null;
8041
7956
  this.crosshair = null;
7957
+ /** Runtime override of the crosshair's lines and style (the `crosshairOverride` feature). Ephemeral. */
7958
+ this.crosshairOverride = null;
8042
7959
  /** How the base price series is drawn on the price pane (candles by default). */
8043
7960
  this.priceStyle = "candles";
8044
7961
  /** Price-series base painting for the ACTIVE style (see ChartTypeDefinition.basePainting). */
@@ -9923,6 +9840,7 @@ function timeTicks(fromMs, toMs, target = 8, offsetMs = 0) {
9923
9840
  const step = pickStep(span / Math.max(1, target));
9924
9841
  const zFrom = fromMs + offsetMs;
9925
9842
  const zTo = toMs + offsetMs;
9843
+ if (step >= MONTH) return calendarTicks(zFrom, zTo, step >= YEAR ? 12 : step >= 3 * MONTH ? 3 : 1, offsetMs);
9926
9844
  const first = Math.ceil(zFrom / step) * step;
9927
9845
  const out = [];
9928
9846
  for (let zt = first; zt <= zTo; zt += step) {
@@ -9953,11 +9871,23 @@ function timeTicks(fromMs, toMs, target = 8, offsetMs = 0) {
9953
9871
  }
9954
9872
  return out;
9955
9873
  }
9874
+ function calendarTicks(zFrom, zTo, months, offsetMs) {
9875
+ const d0 = new Date(zFrom);
9876
+ let idx = d0.getUTCFullYear() * 12 + d0.getUTCMonth();
9877
+ if (Date.UTC(Math.floor(idx / 12), idx % 12, 1) < zFrom) idx += 1;
9878
+ idx = Math.ceil(idx / months) * months;
9879
+ const out = [];
9880
+ for (let zt = Date.UTC(Math.floor(idx / 12), idx % 12, 1); zt <= zTo; idx += months, zt = Date.UTC(Math.floor(idx / 12), idx % 12, 1)) {
9881
+ const label = months === 12 ? String(Math.floor(idx / 12)) : MONTHS[idx % 12];
9882
+ out.push({ time: zt - offsetMs, label, major: true });
9883
+ }
9884
+ return out;
9885
+ }
9956
9886
 
9957
9887
  // src/renderers/native/chrome/countdown.ts
9958
- function countdownText(barOpen, barMs, now) {
9888
+ function countdownText(barOpen, barMs, now2) {
9959
9889
  if (!(barMs > 0)) return null;
9960
- const remaining = barOpen + barMs - now;
9890
+ const remaining = barOpen + barMs - now2;
9961
9891
  if (remaining <= 0) return null;
9962
9892
  return formatCountdown(remaining);
9963
9893
  }
@@ -10769,19 +10699,35 @@ var CrosshairRenderer = class {
10769
10699
  const dataH = coords.height;
10770
10700
  if (!ch || ch.x < 0 || ch.x > dataW || ch.y < 0 || ch.y > dataH) return;
10771
10701
  const cs = scene.style.crosshair;
10702
+ const ov = scene.crosshairOverride;
10703
+ const vertical = ov?.vertical !== false;
10704
+ const horizontal = ov?.horizontal !== false;
10772
10705
  ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
10773
10706
  ctx.textBaseline = "middle";
10774
10707
  const logical = Math.round(coords.xToLogical(ch.x));
10775
- const x = Math.round(coords.logicalToX(logical)) + 0.5;
10776
- ctx.strokeStyle = cs.color ?? theme.textColor;
10777
- ctx.lineWidth = cs.width;
10778
- ctx.globalAlpha = cs.opacity;
10779
- setDash3(ctx, cs.style);
10708
+ const x = crisp(coords.logicalToX(logical), ov?.width ?? cs.width);
10709
+ if (vertical && ov?.shadeRight) {
10710
+ const from = Math.max(0, Math.round(coords.logicalToX(logical + 0.5)));
10711
+ if (from < dataW) {
10712
+ ctx.fillStyle = ov.shadeRight.color;
10713
+ ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
10714
+ ctx.fillRect(from, 0, dataW - from, dataH);
10715
+ ctx.globalAlpha = 1;
10716
+ }
10717
+ }
10718
+ ctx.strokeStyle = ov?.color ?? cs.color ?? theme.textColor;
10719
+ ctx.lineWidth = ov?.width ?? cs.width;
10720
+ ctx.globalAlpha = ov?.opacity ?? cs.opacity;
10721
+ setDash3(ctx, ov?.style ?? cs.style);
10780
10722
  ctx.beginPath();
10781
- ctx.moveTo(x, 0);
10782
- ctx.lineTo(x, dataH);
10783
- ctx.moveTo(0, Math.round(ch.y) + 0.5);
10784
- ctx.lineTo(dataW, Math.round(ch.y) + 0.5);
10723
+ if (vertical) {
10724
+ ctx.moveTo(x, 0);
10725
+ ctx.lineTo(x, dataH);
10726
+ }
10727
+ if (horizontal) {
10728
+ ctx.moveTo(0, Math.round(ch.y) + 0.5);
10729
+ ctx.lineTo(dataW, Math.round(ch.y) + 0.5);
10730
+ }
10785
10731
  ctx.stroke();
10786
10732
  ctx.setLineDash([]);
10787
10733
  ctx.lineWidth = 1;
@@ -10794,11 +10740,11 @@ var CrosshairRenderer = class {
10794
10740
  }
10795
10741
  }
10796
10742
  const chipBg = cs.labelBackground ?? theme.borderColor;
10797
- if (pane && pane.axisFormat !== "none") {
10743
+ if (horizontal && pane && pane.axisFormat !== "none") {
10798
10744
  const price = coords.yToPrice(ch.y, pane.scale, pane.bounds);
10799
10745
  this.chip(ctx, dataW + 1, ch.y, formatAxisValue(pane.scale, pane.bounds.height, price, percentScaleFor(scene, pane), scene.priceMintick, pane.axisFormat), chipBg, "left", false, theme.background);
10800
10746
  }
10801
- this.chip(ctx, x, dataH + 1, formatTimeStamp(coords.logicalToTime(logical), scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
10747
+ if (vertical) this.chip(ctx, x, dataH + 1, formatTimeStamp(coords.logicalToTime(logical), scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
10802
10748
  }
10803
10749
  destroy() {
10804
10750
  this.canvas = null;
@@ -10809,23 +10755,37 @@ var CrosshairRenderer = class {
10809
10755
  * along), with that bar's time chip in this chart's own timezone and — when the
10810
10756
  * level resolved — the price chip on the right axis. The snap happened upstream
10811
10757
  * (`externalCrossPx`, floor-to-containing-bar) — this method only draws. Chips
10812
- * render slightly dimmed so the ghost still reads as foreign. */
10758
+ * render slightly dimmed so the ghost still reads as foreign. A `crosshairOverride`
10759
+ * applies to the ghost as well — its line style, a dropped horizontal line, and the
10760
+ * `shadeRight` veil from the ghost bar's right edge — so a pick that spans several
10761
+ * charts reads the same on every one of them. */
10813
10762
  drawExternal(ctx, ext, scene, coords, theme) {
10814
10763
  const cs = scene.style.crosshair;
10764
+ const ov = scene.crosshairOverride;
10815
10765
  const dataW = coords.width;
10816
10766
  const dataH = coords.height;
10817
- const x = Math.round(ext.x) + 0.5;
10818
- if (x < 0 || x > dataW) return;
10767
+ const x = crisp(ext.x, ov?.width ?? cs.width);
10768
+ if (ov?.vertical === false) return;
10769
+ if (ov?.shadeRight) {
10770
+ const from = Math.max(0, Math.round(coords.logicalToX(Math.round(coords.xToLogical(ext.x)) + 0.5)));
10771
+ if (from < dataW) {
10772
+ ctx.fillStyle = ov.shadeRight.color;
10773
+ ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
10774
+ ctx.fillRect(from, 0, dataW - from, dataH);
10775
+ ctx.globalAlpha = 1;
10776
+ }
10777
+ }
10778
+ if (ext.line === false || x < 0 || x > dataW) return;
10819
10779
  ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
10820
10780
  ctx.textBaseline = "middle";
10821
- ctx.strokeStyle = cs.color ?? theme.textColor;
10822
- ctx.lineWidth = cs.width;
10823
- ctx.globalAlpha = cs.opacity * 0.55;
10824
- setDash3(ctx, cs.style);
10781
+ ctx.strokeStyle = ov?.color ?? cs.color ?? theme.textColor;
10782
+ ctx.lineWidth = ov?.width ?? cs.width;
10783
+ ctx.globalAlpha = (ov?.opacity ?? cs.opacity) * 0.55;
10784
+ setDash3(ctx, ov?.style ?? cs.style);
10825
10785
  ctx.beginPath();
10826
10786
  ctx.moveTo(x, 0);
10827
10787
  ctx.lineTo(x, dataH);
10828
- if (ext.y != null) {
10788
+ if (ext.y != null && ov?.horizontal !== false) {
10829
10789
  ctx.moveTo(0, Math.round(ext.y) + 0.5);
10830
10790
  ctx.lineTo(dataW, Math.round(ext.y) + 0.5);
10831
10791
  }
@@ -10834,7 +10794,7 @@ var CrosshairRenderer = class {
10834
10794
  ctx.lineWidth = 1;
10835
10795
  ctx.globalAlpha = 0.8;
10836
10796
  const chipBg = cs.labelBackground ?? theme.borderColor;
10837
- if (ext.y != null && ext.price != null) {
10797
+ if (ext.y != null && ext.price != null && ov?.horizontal !== false) {
10838
10798
  let pane;
10839
10799
  for (const p of scene.panes.values()) {
10840
10800
  if (ext.y >= p.bounds.top && ext.y <= p.bounds.top + p.bounds.height) {
@@ -10874,6 +10834,9 @@ var CrosshairRenderer = class {
10874
10834
  ctx.textAlign = "start";
10875
10835
  }
10876
10836
  };
10837
+ function crisp(px, width) {
10838
+ return Math.round(px) + (Math.round(width) % 2 === 1 ? 0.5 : 0);
10839
+ }
10877
10840
  function setDash3(ctx, style) {
10878
10841
  if (style === "dashed") ctx.setLineDash([6, 4]);
10879
10842
  else if (style === "dotted") ctx.setLineDash([2, 3]);
@@ -17729,23 +17692,21 @@ function stackLayers(entries, candleZ) {
17729
17692
  return { below, above };
17730
17693
  }
17731
17694
 
17732
- // src/renderers/native/chrome/luxalgo-logos.ts
17733
- var LUXALGO_SYMBOL_SVG = '<svg viewBox="0 0 45 40" aria-hidden="true"><g fill="currentColor"><path d="m40.25 38 4.58-7.998L28.802 2l-16.03 28 9.16-.001 6.87-12z"/><path d="M34.525 32.002 9.33 31.997 27.655 0h-9.158L.18 31.993 4.759 40h34.347z"/></g></svg>';
17734
- var LUXALGO_WORDMARK_SVG = '<svg viewBox="0 0 964 252" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M 614 94.500 L 614 189 633.500 189 L 653 189 653 94.500 L 653 0 633.500 0 L 614 0 614 94.500 M 0 97.500 L 0 189 63.023 189 L 126.045 189 125.773 170.750 L 125.500 152.500 84.250 152.239 L 43 151.978 43 78.989 L 43 6 21.500 6 L 0 6 0 97.500 M 450.387 96.451 C 421.871 146.253, 398.306 186.994, 398.020 186.987 C 397.423 186.972, 362 124.724, 362 123.691 C 362 123.316, 370.100 108.818, 380 91.472 C 389.900 74.127, 398 59.500, 398 58.968 C 398 58.380, 391.007 58, 380.210 58 L 362.420 58 348.060 83.138 C 334.400 107.052, 333.637 108.160, 332.399 105.888 C 331.682 104.575, 325.287 93.375, 318.187 81 L 305.277 58.500 287.139 58.227 C 277.162 58.078, 269 58.186, 269 58.469 C 269 58.751, 277.154 73.274, 287.120 90.741 C 297.086 108.209, 305.261 122.983, 305.287 123.574 C 305.312 124.164, 297.318 138.789, 287.522 156.074 C 277.725 173.358, 269.524 187.838, 269.296 188.250 C 269.068 188.662, 276.977 189, 286.870 189 L 304.859 189 319.163 163.959 C 331.321 142.674, 333.647 139.165, 334.670 140.563 C 335.331 141.468, 341.864 152.737, 349.186 165.606 L 362.500 189.003 402.442 188.752 L 442.384 188.500 472.080 136.695 C 488.413 108.203, 501.939 84.729, 502.138 84.531 C 502.337 84.333, 516 107.757, 532.500 136.583 L 562.500 188.994 584.750 188.997 C 596.987 188.999, 607 188.854, 607 188.675 C 607 188.319, 503.014 6.686, 502.532 6.201 C 502.369 6.037, 478.904 46.649, 450.387 96.451 M 717.500 55.017 C 697.175 57.389, 680.641 68.752, 672.036 86.264 C 665.866 98.818, 663.840 108.024, 663.840 123.500 C 663.840 133.409, 664.328 138.355, 665.892 144.300 C 672.076 167.810, 689.191 184.757, 711.529 189.489 C 730.549 193.518, 747.264 189.540, 761.282 177.648 C 764.944 174.542, 768.178 172, 768.470 172 C 769.345 172, 769.030 185.018, 767.979 192.285 C 765.618 208.613, 755.148 218.672, 738.473 220.632 C 732.586 221.324, 723.136 219.599, 717.709 216.843 C 712.693 214.295, 706.636 207.946, 703.972 202.443 C 702.789 199.999, 701.749 198, 701.661 198.001 C 700.201 198.007, 665.596 205.071, 665.230 205.437 C 664.326 206.341, 669.156 218.313, 672.607 223.721 C 682.146 238.672, 700.008 248.257, 723.500 251.033 C 758.132 255.124, 788.930 241.875, 800.287 218 C 806.923 204.047, 807 203.001, 807 126.542 L 807 58 788.500 58 L 770 58 770 66.500 C 770 71.175, 769.641 75, 769.203 75 C 768.765 75, 766.627 73.142, 764.453 70.872 C 752.805 58.708, 735.614 52.903, 717.500 55.017 M 878.500 55.031 C 850.107 59.412, 830.755 75.064, 822.285 100.500 C 819.905 107.644, 819.614 110.051, 819.565 123 C 819.501 139.581, 820.786 145.825, 826.763 158 C 832.104 168.880, 842.842 179.424, 854.077 184.822 C 867.651 191.343, 873.132 192.462, 891.500 192.462 C 909.868 192.462, 915.349 191.343, 928.923 184.822 C 944.653 177.265, 956.667 162.306, 961.710 144 C 963.074 139.048, 963.481 134.047, 963.418 123 C 963.346 110.140, 963.038 107.595, 960.694 100.500 C 953.325 78.193, 936.695 62.854, 913.500 56.969 C 904.262 54.625, 887.240 53.682, 878.500 55.031 M 134.015 102.750 C 134.027 139.250, 134.310 148.689, 135.549 153.947 C 141.139 177.663, 159.815 193, 183.105 193 C 196.453 193, 210.163 186.171, 217.936 175.650 C 219.826 173.093, 221.738 171, 222.186 171 C 222.634 171, 223 175.050, 223 180 L 223 189 242.507 189 L 262.014 189 261.757 123.750 L 261.500 58.500 242.500 58.500 L 223.500 58.500 223 99.500 C 222.513 139.406, 222.441 140.610, 220.289 144.634 C 209.729 164.377, 183.014 164.880, 174.299 145.500 C 172.677 141.895, 172.469 137.382, 172.190 99.750 L 171.880 58 152.940 58 L 134 58 134.015 102.750 M 729.932 87.998 C 719.311 89.821, 710.155 97.164, 705.141 107.880 C 702.851 112.772, 702.510 114.798, 702.505 123.521 C 702.500 132.834, 702.732 134.014, 705.787 140.218 C 709.761 148.290, 715.989 154.097, 723.853 157.063 C 731.409 159.912, 744.359 159.466, 750.795 156.136 C 767.289 147.601, 774.103 127.713, 767.019 108.780 C 761.433 93.851, 746.051 85.232, 729.932 87.998 M 883.972 88.348 C 865.749 92.310, 854.873 109.966, 858.119 130.316 C 860.933 147.963, 872.346 158.830, 889.084 159.799 C 900.714 160.472, 906.867 158.234, 914.650 150.497 C 919.856 145.323, 921.123 143.340, 922.900 137.587 C 925.412 129.456, 925.655 118.570, 923.489 111.275 C 918.479 94.405, 901.478 84.541, 883.972 88.348"/></svg>';
17695
+ // src/renderers/native/chrome/vela-logos.ts
17696
+ var VELA_MARK_SVG = '<svg viewBox="0 -142 161.66 148" aria-hidden="true"><g fill="currentColor"><path d="M0 -140L32.91 -140L77.52 -62.73L61.06 -34.24Z"/><path d="M161.66 -140L128.75 -140L84.14 -62.73L100.59 -34.24Z"/><path d="M80.83 -52.04L95.85 -26.02L80.83 0L65.81 -26.02Z"/></g></svg>';
17697
+ var VELA_LETTERS_SVG = '<svg viewBox="161.66 -142 310.57 148" aria-hidden="true"><g fill="currentColor"><path d="M226.3 1.17Q200.81 1.17 186.12 -12.99Q171.42 -27.15 171.42 -51.17Q171.42 -75.2 185.87 -89.6Q200.33 -104 224.25 -104Q247.4 -104 261.7 -90.58Q276.01 -77.15 276.3 -54.59Q276.3 -49.9 275.42 -45.02H200.81V-43.55Q201.2 -32.62 207.75 -26.42Q214.29 -20.21 225.03 -20.21Q233.72 -20.21 239.34 -23.93Q244.95 -27.64 246.81 -34.77H274.64Q272.1 -19.14 259.16 -8.98Q246.22 1.17 226.3 1.17ZM201.5 -63.38H247.88Q246.42 -72.75 240.27 -77.88Q234.12 -83.01 224.45 -83.01Q215.17 -83.01 208.92 -77.73Q202.67 -72.46 201.5 -63.38ZM287.54 0V-140.04H315.76V0ZM429.34 -23.83H432.56V0H418.11Q407.76 0 403.36 -4.44Q398.97 -8.89 399.16 -16.8Q387.83 1.17 365.57 1.17Q348.48 1.17 337.88 -6.74Q327.29 -14.65 327.29 -29Q327.29 -45.41 339.45 -54.3Q351.6 -63.18 374.55 -63.18H394.96V-68.16Q394.96 -75.2 390.08 -79.3Q385.2 -83.4 376.51 -83.4Q368.3 -83.4 363.03 -79.98Q357.76 -76.56 356.88 -70.8H329.73Q331.19 -85.84 344.08 -94.92Q356.97 -104 377.29 -104Q399.06 -104 410.88 -94.43Q422.7 -84.86 422.7 -66.6V-30.18Q422.7 -26.37 424.26 -25.1Q425.82 -23.83 429.34 -23.83ZM394.96 -42.38V-44.43H374.36Q365.47 -44.43 360.59 -40.92Q355.7 -37.4 355.7 -30.96Q355.7 -25.59 359.81 -22.41Q363.91 -19.24 370.94 -19.24Q381.97 -19.24 388.37 -25.54Q394.77 -31.84 394.96 -42.38Z"/><path fill-opacity="0.55" d="M437.44 -124.28H441.96V-113.41H445.34V-124.28H449.87V-127.4H437.44ZM451.43 -113.41H454.7V-123.29L458.97 -113.41H461.62L465.99 -123.29V-113.41H469.21V-127.4H464.53L460.42 -117.68L456.16 -127.4H451.43Z"/></g></svg>';
17735
17698
 
17736
17699
  // src/renderers/native/chrome/AttributionMark.ts
17737
- var ATTRIBUTION_URL = "https://luxalgo.com/vela";
17700
+ var ATTRIBUTION_URL = "https://velacharts.dev/?utm_source=vela&utm_medium=attribution";
17738
17701
  var STYLE_ID5 = "vela-attribution-styles";
17739
17702
  var CSS = `
17740
17703
  .vela-attribution {
17741
17704
  text-decoration: none;
17742
17705
  display: flex;
17743
17706
  align-items: center;
17744
- /* Gutter, wordmark size and offset below are the brand lockup's own ratios,
17745
- measured off the official horizontal logo: gutter 0.157, wordmark height
17746
- 0.966, and wordmark 0.109 LOWER than the symbol \u2014 box-centering the two
17747
- reads wrong because the symbol's ink hangs low and the wordmark descends. */
17748
- gap: 4px;
17707
+ /* No gutter: the V is the wordmark's first letter, and the letters' viewBox starts
17708
+ where the V's ends, so the kit's own letter spacing comes with them. */
17709
+ gap: 0;
17749
17710
  }
17750
17711
  .vela-attribution .vela-attr-symbol {
17751
17712
  flex: none;
@@ -17753,7 +17714,7 @@ var CSS = `
17753
17714
  line-height: 0;
17754
17715
  }
17755
17716
  .vela-attribution .vela-attr-symbol svg {
17756
- height: 28px;
17717
+ height: 24px;
17757
17718
  width: auto;
17758
17719
  display: block;
17759
17720
  filter: drop-shadow(0 1px 2px var(--vela-attr-shadow, rgba(0,0,0,0.45)));
@@ -17764,15 +17725,13 @@ var CSS = `
17764
17725
  opacity: 0;
17765
17726
  display: flex;
17766
17727
  align-items: center;
17767
- /* Offset here, not on the image: the clip box must not crop the descender. */
17768
- position: relative;
17769
- top: 3px;
17728
+ /* Same height and baseline as the V (one viewBox height), so no vertical offset. */
17770
17729
  transform: translateX(-8px);
17771
17730
  transition: max-width 0.3s ease, opacity 0.25s ease, transform 0.3s ease;
17772
17731
  flex: none;
17773
17732
  }
17774
17733
  .vela-attribution .vela-attr-wordmark svg {
17775
- height: 27px;
17734
+ height: 24px;
17776
17735
  width: auto;
17777
17736
  display: block;
17778
17737
  filter: drop-shadow(0 1px 2px var(--vela-attr-shadow, rgba(0,0,0,0.45)));
@@ -17787,14 +17746,11 @@ var CSS = `
17787
17746
  (data-layout): the workspace's single grid-wide mark lives OUTSIDE any renderer
17788
17747
  container, so only the shell attribute reaches it. */
17789
17748
  [data-vela-layout='mobile'] .vela-attribution .vela-attr-symbol svg,
17790
- [data-layout='mobile'] .vela-attribution .vela-attr-symbol svg { height: 22px; }
17749
+ [data-layout='mobile'] .vela-attribution .vela-attr-symbol svg { height: 20px; }
17791
17750
  [data-vela-layout='mobile'] .vela-attribution .vela-attr-wordmark,
17792
- [data-layout='mobile'] .vela-attribution .vela-attr-wordmark {
17793
- top: 2px;
17794
- transform: translateX(-6px);
17795
- }
17751
+ [data-layout='mobile'] .vela-attribution .vela-attr-wordmark { transform: translateX(-6px); }
17796
17752
  [data-vela-layout='mobile'] .vela-attribution .vela-attr-wordmark svg,
17797
- [data-layout='mobile'] .vela-attribution .vela-attr-wordmark svg { height: 21px; }
17753
+ [data-layout='mobile'] .vela-attribution .vela-attr-wordmark svg { height: 20px; }
17798
17754
  [data-vela-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark,
17799
17755
  [data-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark { transform: translateX(0); }
17800
17756
  `;
@@ -17851,10 +17807,10 @@ function createAttributionMark(doc, background) {
17851
17807
  const symbol = doc.createElement("span");
17852
17808
  symbol.className = "vela-attr-symbol";
17853
17809
  symbol.setAttribute("aria-hidden", "true");
17854
- symbol.innerHTML = LUXALGO_SYMBOL_SVG;
17810
+ symbol.innerHTML = VELA_MARK_SVG;
17855
17811
  const wordmark = doc.createElement("span");
17856
17812
  wordmark.className = "vela-attr-wordmark";
17857
- wordmark.innerHTML = LUXALGO_WORDMARK_SVG;
17813
+ wordmark.innerHTML = VELA_LETTERS_SVG;
17858
17814
  a.append(symbol, wordmark);
17859
17815
  return a;
17860
17816
  }
@@ -18434,7 +18390,7 @@ var NativeRenderer = class {
18434
18390
  this.moveIndicatorCbs = /* @__PURE__ */ new Set();
18435
18391
  this.priceStyleCbs = /* @__PURE__ */ new Set();
18436
18392
  this.name = "native";
18437
- this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "animScroll", "animAutoscale", "animLiveBar", "intro", "zoomAnchor", "axisDrag", "paneResize", "candleZOrder", "candleVisible", "seriesOrder", "highlights", "sessionZones", "gridlines", "axisLabels", "scaleMode", "invertScale", "paneScales", "autoScale", "timezone", "keyboard", "historyChords", "priceStyle", "priceBaseline", "baselinePrice", "settings", "attribution", "dialogHost", "tradeMarkers", "marks", "indicatorTitles", "indicatorValues"];
18393
+ this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "animScroll", "animAutoscale", "animLiveBar", "intro", "zoomAnchor", "axisDrag", "paneResize", "candleZOrder", "candleVisible", "seriesOrder", "highlights", "sessionZones", "gridlines", "axisLabels", "scaleMode", "invertScale", "paneScales", "autoScale", "timezone", "keyboard", "historyChords", "priceStyle", "priceBaseline", "baselinePrice", "settings", "attribution", "dialogHost", "tradeMarkers", "marks", "indicatorTitles", "indicatorValues", "crosshairOverride"];
18438
18394
  /** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
18439
18395
  * so moving onto the button doesn't count as leaving). */
18440
18396
  this.onScrollProximityMove = (e) => {
@@ -18613,6 +18569,9 @@ var NativeRenderer = class {
18613
18569
  this.scene.marks = mergeMarksState(this.scene.marks, value);
18614
18570
  this.markPopover?.close();
18615
18571
  break;
18572
+ case "crosshairOverride":
18573
+ this.scene.crosshairOverride = sanitizeCrosshairOverride(value);
18574
+ break;
18616
18575
  case "keyboard":
18617
18576
  this.setKeyboardEnabled(Boolean(value));
18618
18577
  return;
@@ -18726,6 +18685,8 @@ var NativeRenderer = class {
18726
18685
  return { ...this.scene.tradeMarkers, colors: { ...this.scene.tradeMarkers.colors } };
18727
18686
  case "marks":
18728
18687
  return { visible: this.scene.marks.visible, groups: { ...this.scene.marks.groups } };
18688
+ case "crosshairOverride":
18689
+ return this.scene.crosshairOverride ? { ...this.scene.crosshairOverride } : null;
18729
18690
  case "keyboard":
18730
18691
  return this.keyboardEnabled;
18731
18692
  case "historyChords":
@@ -19304,8 +19265,8 @@ var NativeRenderer = class {
19304
19265
  for (const pane of this.scene.panes.values()) pane.scale = { ...pane.scaleTarget };
19305
19266
  this.modelAlpha = 0;
19306
19267
  const start = performance.now();
19307
- const step = (now) => {
19308
- const p = Math.min(1, (now - start) / duration);
19268
+ const step = (now2) => {
19269
+ const p = Math.min(1, (now2 - start) / duration);
19309
19270
  this.scene.bars = p >= 1 ? real : real.map((b, i) => this.revealCandle(b, i, p, n, style));
19310
19271
  this.paintData();
19311
19272
  if (p < 1) {
@@ -19321,8 +19282,8 @@ var NativeRenderer = class {
19321
19282
  fadeInModels() {
19322
19283
  const fade = Math.min(INTRO_MODEL_FADE_MS, this.intro.duration || INTRO_MODEL_FADE_MS);
19323
19284
  const start = performance.now();
19324
- const step = (now) => {
19325
- this.modelAlpha = Math.min(1, (now - start) / fade);
19285
+ const step = (now2) => {
19286
+ this.modelAlpha = Math.min(1, (now2 - start) / fade);
19326
19287
  this.paintData();
19327
19288
  if (this.modelAlpha < 1) {
19328
19289
  this.introRaf = requestAnimationFrame(step);
@@ -19845,10 +19806,13 @@ var NativeRenderer = class {
19845
19806
  this.animator.start();
19846
19807
  }
19847
19808
  } else if (!last || bar.time > last.time) {
19809
+ const vp = this.coords.getViewport();
19810
+ const holdView = n > 0 && vp.rightOffset < 0 && this.scrollTargetRO === null;
19848
19811
  this.bars.push(bar);
19849
19812
  this.syncLiveEase(bar);
19850
19813
  if (this.coords.barInterval > 0) this.coords.appendBar(bar.time);
19851
19814
  else this.coords.setBars(this.bars.map((b) => b.time));
19815
+ if (holdView) this.coords.setViewport({ ...vp, rightOffset: vp.rightOffset - 1 });
19852
19816
  } else {
19853
19817
  return;
19854
19818
  }
@@ -20530,10 +20494,10 @@ var NativeRenderer = class {
20530
20494
  if (this.markPulseRaf !== null || typeof requestAnimationFrame !== "function") return;
20531
20495
  const tick = () => {
20532
20496
  this.markPulseRaf = null;
20533
- const now = frameNow();
20534
- if (this.scene.marksFlash && this.scene.marksFlash.until <= now) this.scene.marksFlash = null;
20497
+ const now2 = frameNow();
20498
+ if (this.scene.marksFlash && this.scene.marksFlash.until <= now2) this.scene.marksFlash = null;
20535
20499
  this.scheduler?.invalidate(2 /* Chrome */);
20536
- const pulsing = this.scene.marksHoverKey !== null && now - this.scene.marksHoverSince < MARK_PULSE_MS;
20500
+ const pulsing = this.scene.marksHoverKey !== null && now2 - this.scene.marksHoverSince < MARK_PULSE_MS;
20537
20501
  if (pulsing || this.scene.marksFlash !== null) this.markPulseRaf = requestAnimationFrame(tick);
20538
20502
  };
20539
20503
  this.markPulseRaf = requestAnimationFrame(tick);
@@ -21094,9 +21058,10 @@ var NativeRenderer = class {
21094
21058
  const ext = this.externalCross;
21095
21059
  if (!ext || this.coords.barCount === 0) return null;
21096
21060
  const logical = Math.floor(this.coords.timeToLogical(ext.time));
21097
- if (logical < 0 || logical >= this.coords.barCount) return null;
21098
- const x = this.coords.logicalToX(logical);
21099
- if (!Number.isFinite(x) || x < 0 || x > this.coords.width) return null;
21061
+ if (!(logical < this.coords.barCount)) return null;
21062
+ const x = this.coords.logicalToX(Math.max(logical, -1));
21063
+ if (!Number.isFinite(x)) return null;
21064
+ const line = logical >= 0 && x >= 0 && x <= this.coords.width;
21100
21065
  let y = null;
21101
21066
  if (ext.price != null) {
21102
21067
  const pricePane = this.scene.orderedPanes().find((p) => p.kind === "price");
@@ -21105,7 +21070,7 @@ var NativeRenderer = class {
21105
21070
  if (!Number.isFinite(y) || y < pricePane.bounds.top || y > pricePane.bounds.top + pricePane.bounds.height) y = null;
21106
21071
  }
21107
21072
  }
21108
- return { x, y, time: this.coords.logicalToTime(logical), price: y != null ? ext.price : null };
21073
+ return { x, y, time: this.coords.logicalToTime(Math.max(logical, 0)), price: y != null ? ext.price : null, line };
21109
21074
  }
21110
21075
  /** Set the sticky magnet mode (called by the drawings toolbar's 3-state button). */
21111
21076
  setSnapMode(mode) {
@@ -22329,6 +22294,7 @@ var CHUNK_BARS = 1e4;
22329
22294
  var FIRST_PAINT_BARS = 20;
22330
22295
  var GAP_FACTOR = 1.5;
22331
22296
  var HEAL_COOLDOWN_MS = 5e3;
22297
+ var MIN_TICK_UPDATE_MS = 16;
22332
22298
  var EngineOrchestrator = class _EngineOrchestrator {
22333
22299
  constructor(container, renderer, feed, engines, config, dataControl) {
22334
22300
  this.renderer = renderer;
@@ -22381,6 +22347,32 @@ var EngineOrchestrator = class _EngineOrchestrator {
22381
22347
  this.lastHealAt = 0;
22382
22348
  /** The `visibilitychange` catch-up listener (removed on destroy). */
22383
22349
  this.onVisible = null;
22350
+ // ── bar replay: `rawBars` holds the history up to the cursor, the rest waits here ──
22351
+ /** Bars still hidden right of the replay cursor, oldest first; null ≡ replay off. */
22352
+ this.replayQueue = null;
22353
+ this.replayTimer = null;
22354
+ this.replayPlaying = false;
22355
+ this.replayIntervalMs = 1e3;
22356
+ /** The depth (`market.bars`) before a replay deepened the history — restored when it ends. */
22357
+ this.replayDepth = null;
22358
+ /** A start is loading older history before it can cut; `seek` = a replay was running
22359
+ * (and left silently to deepen), so abandoning the load must announce its end. */
22360
+ this.replayLoading = null;
22361
+ /** A replay carried over a timeframe/session switch, between the old series' silent end
22362
+ * and the cut of the new one: still ACTIVE for the outside (`replayState`), resumed
22363
+ * at `revealedEnd` once the new bars land, unless stopped (`cancelled`) meanwhile. */
22364
+ this.replayCarrying = null;
22365
+ /** Intrabar prices for the bars replay reveals — null ≡ whole bars. */
22366
+ this.replayTickSource = null;
22367
+ /** The bar being revealed tick by tick: its stored form (what it settles on), its ticks,
22368
+ * the next tick to apply, the candle built so far, the volume each tick adds when the
22369
+ * ticks carry none, and its pacing (ticks per update, ms between updates — one play
22370
+ * interval per tick — and the clock time tick 0 is due at, so late timers catch up). */
22371
+ this.replayForming = null;
22372
+ /** Tick requests by bar time, shared by prefetch and playback; `ticks` once they landed. */
22373
+ this.replayTickFetches = /* @__PURE__ */ new Map();
22374
+ /** Aborts the tick requests in flight — renewed whenever the replay moves on. */
22375
+ this.replayTickAbort = null;
22384
22376
  /** What the last applied bar did: refine the open candle, or open a new one (which
22385
22377
  * settles the previous). Read by the `script:run` cause attribution. */
22386
22378
  this.barCause = "tick";
@@ -22604,6 +22596,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
22604
22596
  const paint = (bars, final) => {
22605
22597
  if (this.generation !== gen || !final && bars.length === 0) return;
22606
22598
  if (!painted && !final && bars.length < Math.min(requested, FIRST_PAINT_BARS)) return;
22599
+ if (painted && this.replayQueue) {
22600
+ this.mergeHistoryIntoReplay(bars);
22601
+ return;
22602
+ }
22607
22603
  this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
22608
22604
  if (!painted && bars.length > 0) {
22609
22605
  painted = true;
@@ -22746,6 +22742,36 @@ var EngineOrchestrator = class _EngineOrchestrator {
22746
22742
  // exactly like a timeframe change; the cache keys the sessions apart.
22747
22743
  next.session !== void 0 && next.session !== m.session || next.data !== void 0;
22748
22744
  const depthChanged = next.bars !== void 0 && next.bars !== m.bars;
22745
+ const sameClock = identityChanged && next.data === void 0 && (next.symbol === void 0 || next.symbol === m.symbol) && (next.timeframe !== void 0 || next.session !== void 0);
22746
+ const pending = identityChanged ? this.replayCarrying : null;
22747
+ if (pending) {
22748
+ this.replayCarrying = null;
22749
+ if (!sameClock) {
22750
+ pending.cancelled = true;
22751
+ this.events.emit("replay:end", { reason: "market" });
22752
+ }
22753
+ }
22754
+ const replayCarry = this.replayQueue && sameClock && this.rawBars.length > 0 ? { revealedEnd: this.rawBars[this.rawBars.length - 1].time + this.barIntervalMs(), playing: this.replayPlaying, cancelled: false } : pending && sameClock ? { revealedEnd: pending.revealedEnd, playing: pending.playing, cancelled: false } : null;
22755
+ if (pending) pending.cancelled = true;
22756
+ if (replayCarry) this.replayCarrying = replayCarry;
22757
+ if (replayCarry && this.replayQueue) {
22758
+ this.endReplay(null, { restore: false, resume: false });
22759
+ const depth = this.replayDepth;
22760
+ this.replayDepth = null;
22761
+ if (depth && next.bars === void 0) {
22762
+ if (depth.bars === void 0) delete m.bars;
22763
+ else m.bars = depth.bars;
22764
+ }
22765
+ }
22766
+ if (this.replayQueue && (identityChanged || depthChanged)) this.endReplay("market", { restore: !identityChanged, resume: false });
22767
+ if (this.replayLoading && (identityChanged || depthChanged)) {
22768
+ const depth = this.replayDepth;
22769
+ this.abandonReplayLoad("market");
22770
+ if (depth && next.bars === void 0) {
22771
+ if (depth.bars === void 0) delete m.bars;
22772
+ else m.bars = depth.bars;
22773
+ }
22774
+ }
22749
22775
  const depthOnly = depthChanged && !identityChanged && this.rawBars.length > 0 && !m.data?.length && ((next.bars ?? 0) <= this.rawBars.length || typeof this.feed.loadRange === "function");
22750
22776
  if (!identityChanged && !depthChanged) {
22751
22777
  if (typeof next.visibleRange === "string") this.setVisibleRangePreset(next.visibleRange);
@@ -22803,15 +22829,20 @@ var EngineOrchestrator = class _EngineOrchestrator {
22803
22829
  const from = bars[Math.max(0, idx - (carriedCount - 1))].time;
22804
22830
  this.renderer.setVisibleRange({ from, to: Math.max(carried.right, bars[idx].time + 1) });
22805
22831
  }
22806
- if (!depthOnly) {
22832
+ const carry = replayCarry && !replayCarry.cancelled ? replayCarry : null;
22833
+ const carryFrom = carry ? carry.revealedEnd - this.barIntervalMs() : 0;
22834
+ const carryNow = carry != null && this.rawBars.length > 0 && carryFrom >= this.rawBars[0].time;
22835
+ const replayBack = carryNow && this.resumeCarriedReplay(this.replayStart({ from: carryFrom }), carry);
22836
+ if (!depthOnly && !replayBack) {
22807
22837
  this.restartNativeIndicators();
22808
22838
  this.restartChartTypeEngine();
22809
22839
  this.reexecuteIndicators();
22810
22840
  }
22811
- this.startLive();
22841
+ if (!replayBack) this.startLive();
22812
22842
  if (identityChanged) {
22813
22843
  this.events.emit("market:changed", { symbol: m.symbol ?? "TEST", timeframe: m.timeframe ?? "60", prev: prev2 });
22814
22844
  }
22845
+ if (carry && !carryNow) this.resumeCarriedReplay(this.replayStart({ from: carryFrom }), carry);
22815
22846
  }
22816
22847
  /**
22817
22848
  * Restart every native indicator over the new market: a `NativeIndicatorContext`
@@ -22935,6 +22966,397 @@ var EngineOrchestrator = class _EngineOrchestrator {
22935
22966
  historyComplete() {
22936
22967
  return this.historyCompletePromise;
22937
22968
  }
22969
+ // ── bar replay ──────────────────────────────────────────────
22970
+ /**
22971
+ * Enter replay — or seek, when already replaying — keeping every bar opened at or before
22972
+ * `opts.from` and queueing the rest. A `from` inside the loaded bars cuts at once, even
22973
+ * while older history is still streaming in: what lands later joins the tape's head
22974
+ * (see {@link mergeHistoryIntoReplay}) and leaves the cut alone. An older `from` waits
22975
+ * for that load, then deepens if it still has to. Consumers restart over the cut exactly
22976
+ * as on a market switch — their models describe bars that are now hidden — and the
22977
+ * active chart-type engine is rebuilt without live data.
22978
+ */
22979
+ async replayStart(opts) {
22980
+ if (this.rawBars.length === 0) {
22981
+ console.warn("[vela] chart.replay.start() ignored \u2014 the chart has no bars yet");
22982
+ return;
22983
+ }
22984
+ let gen = this.generation;
22985
+ if (opts.from < this.rawBars[0].time) {
22986
+ await this.historyCompletePromise;
22987
+ if (this.generation !== gen || this.rawBars.length === 0) return;
22988
+ }
22989
+ const oldest = this.rawBars[0].time;
22990
+ if (opts.from < oldest && this.canHeal() && typeof this.feed.loadRange === "function") {
22991
+ const held = this.rawBars.length + (this.replayQueue?.length ?? 0);
22992
+ const bars = held + Math.ceil((oldest - opts.from) / this.barIntervalMs()) + 1;
22993
+ const seek = this.replayQueue !== null;
22994
+ if (seek) this.endReplay(null, { restore: true, resume: false });
22995
+ this.replayDepth ?? (this.replayDepth = { bars: this.config.market.bars });
22996
+ const deepened = this.setMarket({ bars });
22997
+ this.replayLoading = { seek };
22998
+ await deepened;
22999
+ gen = this.generation;
23000
+ await this.historyCompletePromise;
23001
+ if (!this.replayLoading || this.generation !== gen || this.rawBars.length === 0) return;
23002
+ this.replayLoading = null;
23003
+ }
23004
+ this.settleFormingBar();
23005
+ this.dropTickFetches();
23006
+ const tape = this.replayQueue ? [...this.rawBars, ...this.replayQueue] : this.rawBars;
23007
+ let cut = 0;
23008
+ while (cut < tape.length && tape[cut].time <= opts.from) cut += 1;
23009
+ cut = Math.max(1, cut);
23010
+ if (cut >= tape.length) {
23011
+ console.warn("[vela] chart.replay.start() ignored \u2014 no bar opens after the requested time");
23012
+ return;
23013
+ }
23014
+ const wasPlaying = this.replayPlaying;
23015
+ this.clearReplayTimer();
23016
+ if (!this.replayQueue) this.stopLive();
23017
+ this.replayQueue = tape.slice(cut);
23018
+ this.setBarSeries(tape.slice(0, cut));
23019
+ this.restartBarConsumers({ blank: true });
23020
+ if (wasPlaying) this.events.emit("replay:pause", void 0);
23021
+ this.events.emit("replay:start", { cursorTime: tape[cut - 1].time, remaining: this.replayQueue.length });
23022
+ this.prefetchTicks();
23023
+ }
23024
+ /** Reveal the next queued bar through the live-bar path — or complete the bar forming
23025
+ * tick by tick; the last one ends the replay. */
23026
+ replayStep() {
23027
+ if (this.replayForming) {
23028
+ this.finishFormingBar();
23029
+ return true;
23030
+ }
23031
+ const queue = this.replayQueue;
23032
+ const bar = queue?.shift();
23033
+ if (!queue || !bar) return false;
23034
+ this.replayTickFetches.delete(bar.time);
23035
+ this.applyBar(bar);
23036
+ this.completeReveal(bar);
23037
+ this.prefetchTicks();
23038
+ return true;
23039
+ }
23040
+ /**
23041
+ * Follow up a replay carried onto a new series of the same symbol (another timeframe or
23042
+ * session). The cut asked for keeps the bars that CLOSED by the end of what was revealed —
23043
+ * every finer bar of the last revealed one, never a coarser bar still open at that point
23044
+ * (no look-ahead). Once it settles: playback resumes if it was on; if the replay did not
23045
+ * come back (nothing left after that point, or a later switch took over), it ends as a
23046
+ * market switch would. Returns whether the replay is back already — a start with no
23047
+ * history to wait for cuts synchronously (an async function runs up to its first await).
23048
+ */
23049
+ resumeCarriedReplay(start, carry) {
23050
+ if (this.replayQueue && this.replayCarrying === carry) this.replayCarrying = null;
23051
+ void start.then(() => {
23052
+ if (this.replayCarrying === carry) this.replayCarrying = null;
23053
+ if (carry.cancelled) return;
23054
+ if (!this.replayQueue) this.events.emit("replay:end", { reason: "market" });
23055
+ else if (carry.playing) this.replayPlay();
23056
+ });
23057
+ return this.replayQueue !== null;
23058
+ }
23059
+ replayStepUpdate() {
23060
+ if (!this.replayQueue) return false;
23061
+ if (this.replayForming) {
23062
+ this.applyReplayTicks(1);
23063
+ return true;
23064
+ }
23065
+ const bar = this.replayQueue[0];
23066
+ if (!bar) return false;
23067
+ const entry = this.replayTickSource ? this.requestTicks(bar) : null;
23068
+ if (entry && !entry.ticks) {
23069
+ const epoch = this.replayTickAbort;
23070
+ void entry.done.then(() => {
23071
+ if (!this.replayPlaying && !this.replayForming && this.replayTickAbort === epoch && this.replayQueue?.[0] === bar) this.replayStepUpdate();
23072
+ });
23073
+ return true;
23074
+ }
23075
+ if (!entry?.ticks?.length) return this.replayStep();
23076
+ this.openFormingBar(entry.ticks, 1);
23077
+ return true;
23078
+ }
23079
+ replaySetTicks(source) {
23080
+ this.finishFormingBar();
23081
+ this.dropTickFetches();
23082
+ this.replayTickSource = source;
23083
+ this.prefetchTicks();
23084
+ }
23085
+ replayPlay(intervalMs2) {
23086
+ if (!this.replayQueue) return;
23087
+ if (intervalMs2 !== void 0) {
23088
+ if (!(Number.isFinite(intervalMs2) && intervalMs2 > 0)) {
23089
+ console.warn(`[vela] chart.replay.play(${intervalMs2}) ignored \u2014 the interval must be a positive number of ms`);
23090
+ return;
23091
+ }
23092
+ this.replayIntervalMs = intervalMs2;
23093
+ }
23094
+ this.clearReplayTimer();
23095
+ this.replayPlaying = true;
23096
+ if (this.replayForming) this.paceFormingBar(this.replayForming);
23097
+ this.scheduleReplayStep(this.replayForming?.delayMs);
23098
+ this.events.emit("replay:play", { intervalMs: this.replayIntervalMs });
23099
+ }
23100
+ replayPause() {
23101
+ if (!this.replayPlaying) return;
23102
+ this.clearReplayTimer();
23103
+ this.events.emit("replay:pause", void 0);
23104
+ }
23105
+ replayStop() {
23106
+ const carrying = this.replayCarrying;
23107
+ if (carrying) {
23108
+ carrying.cancelled = true;
23109
+ this.replayCarrying = null;
23110
+ this.events.emit("replay:end", { reason: "stopped" });
23111
+ }
23112
+ if (this.replayLoading) {
23113
+ const depth = this.replayDepth;
23114
+ this.abandonReplayLoad("stopped");
23115
+ void this.setMarket({ bars: depth?.bars ?? 500 });
23116
+ return;
23117
+ }
23118
+ this.endReplay("stopped", { restore: true, resume: true });
23119
+ }
23120
+ abandonReplayLoad(reason) {
23121
+ const loading = this.replayLoading;
23122
+ if (!loading) return;
23123
+ this.replayLoading = null;
23124
+ this.replayDepth = null;
23125
+ if (loading.seek) this.events.emit("replay:end", { reason });
23126
+ }
23127
+ replayState() {
23128
+ const active = this.replayQueue !== null;
23129
+ return {
23130
+ active: active || this.replayCarrying !== null,
23131
+ // a replay carried over a switch stays on while the new bars load
23132
+ playing: this.replayCarrying?.playing ?? this.replayPlaying,
23133
+ cursorTime: active ? this.rawBars[this.rawBars.length - 1]?.time ?? null : null,
23134
+ remaining: this.replayQueue?.length ?? 0,
23135
+ nextTime: this.replayForming?.bar.time ?? this.replayQueue?.[0]?.time ?? null,
23136
+ intervalMs: this.replayIntervalMs
23137
+ };
23138
+ }
23139
+ replayBounds() {
23140
+ const first = this.rawBars[0];
23141
+ const last = this.replayQueue?.[this.replayQueue.length - 1] ?? this.rawBars[this.rawBars.length - 1];
23142
+ return first && last ? { first: first.time, last: last.time } : null;
23143
+ }
23144
+ scheduleReplayStep(delayMs = this.replayIntervalMs) {
23145
+ this.replayTimer = setTimeout(() => {
23146
+ this.replayTimer = null;
23147
+ this.replayAdvance();
23148
+ }, delayMs);
23149
+ }
23150
+ /** One beat of timed playback: the forming bar's next ticks, else the next bar — whole,
23151
+ * or opened from its ticks (waiting for them when they have not landed yet). */
23152
+ replayAdvance() {
23153
+ if (!this.replayPlaying || !this.replayQueue) return;
23154
+ const forming = this.replayForming;
23155
+ if (forming) {
23156
+ const due = Math.floor((now() - forming.origin) / this.replayIntervalMs) + 1;
23157
+ this.applyReplayTicks(Math.max(forming.batch, due - forming.next));
23158
+ if (this.replayPlaying) this.scheduleReplayStep(forming.delayMs);
23159
+ return;
23160
+ }
23161
+ const bar = this.replayQueue[0];
23162
+ if (!bar) return;
23163
+ const entry = this.replayTickSource ? this.requestTicks(bar) : null;
23164
+ if (entry && !entry.ticks) {
23165
+ const epoch = this.replayTickAbort;
23166
+ void entry.done.then(() => {
23167
+ if (this.replayPlaying && this.replayTimer == null && this.replayTickAbort === epoch && this.replayQueue?.[0] === bar) this.replayAdvance();
23168
+ });
23169
+ return;
23170
+ }
23171
+ if (!entry?.ticks?.length) {
23172
+ if (this.replayStep() && this.replayPlaying) this.scheduleReplayStep();
23173
+ return;
23174
+ }
23175
+ this.openFormingBar(entry.ticks);
23176
+ if (this.replayPlaying) this.scheduleReplayStep(this.replayForming?.delayMs);
23177
+ }
23178
+ /** Take the next queued bar off the tape and open it at its first tick (the first
23179
+ * `count` ticks — default: one timed update's worth). */
23180
+ openFormingBar(ticks, count) {
23181
+ const bar = this.replayQueue.shift();
23182
+ const volumeEach = ticks.every((t) => t.volume === void 0) ? (bar.volume ?? 0) / ticks.length : null;
23183
+ const forming = { bar, ticks, next: 0, candle: null, volumeEach, batch: 1, delayMs: this.replayIntervalMs, origin: 0 };
23184
+ this.paceFormingBar(forming);
23185
+ this.replayForming = forming;
23186
+ this.applyReplayTicks(count ?? forming.batch);
23187
+ this.prefetchTicks();
23188
+ }
23189
+ /** One tick per interval, batched when the interval is shorter than a frame; the clock
23190
+ * is anchored on the ticks already applied (a resume or a pace change carries on from
23191
+ * there, never jumps). */
23192
+ paceFormingBar(f) {
23193
+ f.batch = Math.max(1, Math.ceil(MIN_TICK_UPDATE_MS / this.replayIntervalMs));
23194
+ f.delayMs = this.replayIntervalMs * f.batch;
23195
+ f.origin = now() - Math.max(0, f.next - 1) * this.replayIntervalMs;
23196
+ }
23197
+ /** Fold the next `count` ticks into the forming candle; the last one settles the bar. */
23198
+ applyReplayTicks(count) {
23199
+ const f = this.replayForming;
23200
+ if (!f) return;
23201
+ let c = f.candle;
23202
+ const stop = Math.min(f.ticks.length, f.next + count);
23203
+ for (; f.next < stop; f.next += 1) {
23204
+ const t = f.ticks[f.next];
23205
+ const v = f.volumeEach ?? t.volume ?? 0;
23206
+ const hi = Math.max(t.high ?? t.price, t.price);
23207
+ const lo = Math.min(t.low ?? t.price, t.price);
23208
+ if (c) {
23209
+ c = { ...c, high: Math.max(c.high, hi), low: Math.min(c.low, lo), close: t.price, volume: (c.volume ?? 0) + v };
23210
+ } else {
23211
+ const open = t.open ?? t.price;
23212
+ c = { time: f.bar.time, open, high: Math.max(open, hi), low: Math.min(open, lo), close: t.price, volume: v };
23213
+ }
23214
+ }
23215
+ f.candle = c;
23216
+ if (f.next >= f.ticks.length) {
23217
+ this.finishFormingBar();
23218
+ return;
23219
+ }
23220
+ this.applyBar(c);
23221
+ this.events.emit("replay:tick", { cursorTime: f.bar.time, index: f.next - 1, count: f.ticks.length });
23222
+ }
23223
+ /** Settle the forming bar on its stored values and announce it revealed. */
23224
+ finishFormingBar() {
23225
+ const f = this.replayForming;
23226
+ if (!f) return;
23227
+ this.replayForming = null;
23228
+ this.replayTickFetches.delete(f.bar.time);
23229
+ this.applyBar(f.bar);
23230
+ this.events.emit("replay:tick", { cursorTime: f.bar.time, index: f.ticks.length - 1, count: f.ticks.length });
23231
+ this.completeReveal(f.bar);
23232
+ }
23233
+ /** Put the forming bar back in its stored form, silently — a seek or an end rebuilds
23234
+ * from the tape, which must hold the stored bar. */
23235
+ settleFormingBar() {
23236
+ const f = this.replayForming;
23237
+ if (!f) return;
23238
+ this.replayForming = null;
23239
+ this.applyBar(f.bar);
23240
+ }
23241
+ /**
23242
+ * A history answer landing mid-replay (a progressive snapshot — the whole answer so far):
23243
+ * the tape keeps its cut and its hidden queue; bars older than the tape join its head,
23244
+ * bars newer than it join the queue's end.
23245
+ */
23246
+ mergeHistoryIntoReplay(bars) {
23247
+ const queue = this.replayQueue;
23248
+ const head = this.rawBars[0];
23249
+ if (!queue || !head) return;
23250
+ const tail = queue[queue.length - 1] ?? this.rawBars[this.rawBars.length - 1];
23251
+ let older = 0;
23252
+ while (older < bars.length && bars[older].time < head.time) older += 1;
23253
+ let newer = bars.length;
23254
+ while (newer > older && bars[newer - 1].time > tail.time) newer -= 1;
23255
+ if (newer < bars.length) queue.push(...bars.slice(newer));
23256
+ if (older > 0) {
23257
+ this.setBarSeries([...bars.slice(0, older), ...this.rawBars], { preserveView: true });
23258
+ this.notifySessionsBars("backfill");
23259
+ }
23260
+ }
23261
+ /** A bar is fully on screen: announce it, and end the replay if it was the last. */
23262
+ completeReveal(bar) {
23263
+ const queue = this.replayQueue;
23264
+ if (!queue) return;
23265
+ this.events.emit("replay:step", { cursorTime: bar.time, remaining: queue.length });
23266
+ if (queue.length === 0) this.endReplay("finished", { restore: true, resume: true });
23267
+ }
23268
+ /** Ask for the ticks of the next hidden bar ahead of time. */
23269
+ prefetchTicks() {
23270
+ const next = this.replayQueue?.[0];
23271
+ if (next && this.replayTickSource) this.requestTicks(next);
23272
+ }
23273
+ /** The (shared) tick request of a queued bar — the source runs once per bar. */
23274
+ requestTicks(bar) {
23275
+ const known = this.replayTickFetches.get(bar.time);
23276
+ if (known) return known;
23277
+ const source = this.replayTickSource;
23278
+ this.replayTickAbort ?? (this.replayTickAbort = new AbortController());
23279
+ const signal = this.replayTickAbort.signal;
23280
+ const queue = this.replayQueue ?? [];
23281
+ const end = queue[queue.indexOf(bar) + 1]?.time ?? bar.time + this.barIntervalMs();
23282
+ const entry = { ticks: null, done: Promise.resolve() };
23283
+ const land = (ticks) => {
23284
+ entry.ticks = usableTicks(ticks);
23285
+ };
23286
+ const fail = (e) => {
23287
+ if (!signal.aborted) console.warn(`[vela] replay tick source failed \u2014 revealing the bar whole (${e instanceof Error ? e.message : String(e)})`);
23288
+ land([]);
23289
+ };
23290
+ try {
23291
+ const out = source(bar, { end, signal });
23292
+ if (Array.isArray(out)) land(out);
23293
+ else entry.done = Promise.resolve(out).then(land, fail);
23294
+ } catch (e) {
23295
+ fail(e);
23296
+ }
23297
+ this.replayTickFetches.set(bar.time, entry);
23298
+ return entry;
23299
+ }
23300
+ /** Abort the tick requests in flight and forget the landed ones. */
23301
+ dropTickFetches() {
23302
+ this.replayTickAbort?.abort();
23303
+ this.replayTickAbort = null;
23304
+ this.replayTickFetches.clear();
23305
+ }
23306
+ clearReplayTimer() {
23307
+ if (this.replayTimer != null) clearTimeout(this.replayTimer);
23308
+ this.replayTimer = null;
23309
+ this.replayPlaying = false;
23310
+ }
23311
+ /**
23312
+ * Leave replay. `restore` puts the queued bars back and restarts the consumers over the
23313
+ * full history (a market switch skips it: its load replaces everything anyway); `resume`
23314
+ * restarts live updates and heals the bars that closed while replaying. A null reason
23315
+ * leaves silently — a seek that re-enters right after.
23316
+ */
23317
+ endReplay(reason, opts) {
23318
+ const queue = this.replayQueue;
23319
+ if (!queue) return;
23320
+ this.clearReplayTimer();
23321
+ if (opts.restore) this.settleFormingBar();
23322
+ else this.replayForming = null;
23323
+ this.dropTickFetches();
23324
+ this.replayQueue = null;
23325
+ const depth = reason ? this.replayDepth : null;
23326
+ if (reason) this.replayDepth = null;
23327
+ if (depth) {
23328
+ if (depth.bars === void 0) delete this.config.market.bars;
23329
+ else this.config.market.bars = depth.bars;
23330
+ }
23331
+ if (opts.restore) {
23332
+ const keep = depth ? depth.bars ?? 500 : Infinity;
23333
+ if (queue.length > 0 || this.rawBars.length > keep) {
23334
+ const full = [...this.rawBars, ...queue];
23335
+ this.setBarSeries(full.length > keep ? full.slice(full.length - keep) : full);
23336
+ this.restartBarConsumers({ blank: false });
23337
+ } else if (this.config.live) {
23338
+ this.restartNativeIndicators();
23339
+ this.restartChartTypeEngine();
23340
+ }
23341
+ }
23342
+ if (opts.resume && this.config.live) {
23343
+ this.startLive();
23344
+ const last = this.rawBars[this.rawBars.length - 1];
23345
+ if (last && this.canHeal()) void this.healGap(last.time);
23346
+ }
23347
+ if (reason) this.events.emit("replay:end", { reason });
23348
+ }
23349
+ /**
23350
+ * The bars were replaced by a cut or a restore of the SAME market: every consumer re-runs.
23351
+ * A cut also blanks the mounted models first — they still paint the hidden bars' values,
23352
+ * which must not show until the re-runs land.
23353
+ */
23354
+ restartBarConsumers(opts) {
23355
+ if (opts.blank) this.blankIndicatorVisuals();
23356
+ this.restartNativeIndicators();
23357
+ this.restartChartTypeEngine();
23358
+ this.reexecuteIndicators();
23359
+ }
22938
23360
  /** The primary symbol as the data layer resolves it, or null if no symbol is set. */
22939
23361
  qualifiedSymbol() {
22940
23362
  const market = this.config.market;
@@ -23006,7 +23428,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
23006
23428
  engine.start({
23007
23429
  symbol: this.config.market.symbol ?? "TEST",
23008
23430
  timeframe: this.config.market.timeframe ?? "60",
23009
- live: this.config.live ?? false,
23431
+ live: (this.config.live ?? false) && this.replayQueue === null,
23010
23432
  session: this.config.market.session,
23011
23433
  bars: () => this.bars,
23012
23434
  data: this.dataControl,
@@ -23132,6 +23554,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
23132
23554
  record.session.notifyBars(reason);
23133
23555
  } else if (record.native) record.native.instance.onBars();
23134
23556
  }
23557
+ if (this.activeEngineStyle) this.typeEngines.get(this.activeEngineStyle)?.onBars?.();
23135
23558
  }
23136
23559
  /** Gap healing needs a ranged feed and a provider-backed series (offline `data` has no source). */
23137
23560
  canHeal() {
@@ -23154,10 +23577,11 @@ var EngineOrchestrator = class _EngineOrchestrator {
23154
23577
  try {
23155
23578
  const bars = await this.feed.loadRange(this.config.market, { from: fromMs });
23156
23579
  if (this.generation !== gen) return;
23580
+ if (this.replayQueue) return;
23157
23581
  for (const b of bars) this.applyBar(b, false);
23158
23582
  } catch {
23159
23583
  } finally {
23160
- if (this.generation === gen) {
23584
+ if (this.generation === gen && !this.replayQueue) {
23161
23585
  this.healing = false;
23162
23586
  const pending = this.healBuffer;
23163
23587
  this.healBuffer = [];
@@ -23462,6 +23886,13 @@ var EngineOrchestrator = class _EngineOrchestrator {
23462
23886
  destroy() {
23463
23887
  this.bumpGeneration();
23464
23888
  this.resolveHistoryComplete();
23889
+ this.clearReplayTimer();
23890
+ this.replayQueue = null;
23891
+ this.replayForming = null;
23892
+ this.replayCarrying = null;
23893
+ this.dropTickFetches();
23894
+ this.replayDepth = null;
23895
+ this.replayLoading = null;
23465
23896
  this.stopLive();
23466
23897
  if (this.viewportTimer != null) clearTimeout(this.viewportTimer);
23467
23898
  this.viewportUnsub?.();
@@ -23560,6 +23991,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
23560
23991
  this.emitScriptRun(id, cause, first);
23561
23992
  },
23562
23993
  onAlert: (a) => {
23994
+ if (this.replayQueue || this.replayCarrying) return;
23563
23995
  const indicator = record.options?.title ?? record.prepared?.meta.title ?? record.title;
23564
23996
  this.events.emit("alert", { ...a, indicator });
23565
23997
  handle.emit("alert", { id: a.id, message: a.message, title: a.title, time: a.time });
@@ -23586,7 +24018,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
23586
24018
  id,
23587
24019
  symbol: this.config.market.symbol ?? "TEST",
23588
24020
  timeframe: this.config.market.timeframe ?? "60",
23589
- live: this.config.live,
24021
+ live: this.config.live && this.replayQueue === null,
23590
24022
  session: this.config.market.session,
23591
24023
  bars: () => this.bars,
23592
24024
  data: this.dataControl,
@@ -23989,17 +24421,17 @@ var EngineOrchestrator = class _EngineOrchestrator {
23989
24421
  }
23990
24422
  emitContextChanged(id) {
23991
24423
  if (!this.registry.get(id)?.session?.getContext) return;
23992
- const now = Date.now();
23993
- if (now - (this.contextEmitAt.get(id) ?? 0) < 1e3) return;
23994
- this.contextEmitAt.set(id, now);
24424
+ const now2 = Date.now();
24425
+ if (now2 - (this.contextEmitAt.get(id) ?? 0) < 1e3) return;
24426
+ this.contextEmitAt.set(id, now2);
23995
24427
  this.events.emit("context:changed", { id });
23996
24428
  }
23997
24429
  emitScriptRun(id, cause, first) {
23998
24430
  const record = this.registry.get(id);
23999
24431
  if (!record || record.native || !this.events.hasListeners("script:run")) return;
24000
- const now = Date.now();
24001
- if (cause === "tick" && now - (this.runEmitAt.get(id) ?? 0) < RUN_EMIT_THROTTLE_MS) return;
24002
- this.runEmitAt.set(id, now);
24432
+ const now2 = Date.now();
24433
+ if (cause === "tick" && now2 - (this.runEmitAt.get(id) ?? 0) < RUN_EMIT_THROTTLE_MS) return;
24434
+ this.runEmitAt.set(id, now2);
24003
24435
  void Promise.resolve().then(() => this.buildScriptRun(record, cause, first)).then((run) => {
24004
24436
  if (run && this.registry.get(id)) this.events.emit("script:run", run);
24005
24437
  });
@@ -24052,6 +24484,16 @@ var EngineOrchestrator = class _EngineOrchestrator {
24052
24484
  handle?.emit("error", { error });
24053
24485
  }
24054
24486
  };
24487
+ function now() {
24488
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
24489
+ }
24490
+ function usableTicks(value) {
24491
+ if (!Array.isArray(value)) return [];
24492
+ const optional = (n) => n === void 0 || Number.isFinite(n);
24493
+ return value.filter(
24494
+ (t) => t != null && Number.isFinite(t.price) && optional(t.volume) && optional(t.high) && optional(t.low) && optional(t.open)
24495
+ );
24496
+ }
24055
24497
  function yieldToPaint() {
24056
24498
  if (typeof requestAnimationFrame === "function") {
24057
24499
  return new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
@@ -28127,6 +28569,7 @@ var Vela = class {
28127
28569
  this.panesControl = new PanesControl(this.orchestrator);
28128
28570
  this.drawingsControl = new DrawingsControl(this.orchestrator.drawings);
28129
28571
  this.marksControl = new MarksControl(this.orchestrator.marks);
28572
+ this.replayControl = new ReplayControl(this.orchestrator);
28130
28573
  }
28131
28574
  /**
28132
28575
  * Register a scripting engine so `addIndicator({ language })` can run that
@@ -28366,6 +28809,16 @@ var Vela = class {
28366
28809
  get marks() {
28367
28810
  return this.marksControl;
28368
28811
  }
28812
+ /**
28813
+ * The chart's bar-replay control surface: rewind to a past bar and reveal the
28814
+ * following ones by hand or on a timer, from the history already loaded —
28815
+ * `await chart.replay.start({ from })`, `chart.replay.play(2000)`, `chart.replay.step()`,
28816
+ * `chart.replay.stop()`. Revealed bars behave like live bars for indicators; live
28817
+ * updates pause meanwhile. Follow it with the `replay:*` events.
28818
+ */
28819
+ get replay() {
28820
+ return this.replayControl;
28821
+ }
28369
28822
  on(event, handler) {
28370
28823
  return this.orchestrator.events.on(event, handler);
28371
28824
  }
@@ -28424,4 +28877,4 @@ function resolveElement(container) {
28424
28877
  return element;
28425
28878
  }
28426
28879
 
28427
- export { BUILTIN_PRICE_STYLES, BarStore, CachingDataFeed, DARK_THEME, DataControl, DrawingStore, DrawingToolbar, DrawingsControl, LEGEND_AT_TOP_ATTR, LIGHT_THEME, MarksControl, MultiProviderFeed, NativeRenderer, RendererControl, SecondClock, TIMEZONES, TypedEventBus, Vela, applyAttributionMarkTheme, buildToolbar, createAttributionMark, createCustomMark, defaultMemberOf, defaultToolbar, groupKeyOf, groupMembers, isExchangeTimezone, isGroupRow, normalizeSession, normalizeTimezone, parseSymbol, priceStyleIds, registerBuiltinChartTypes, resolveTheme, resolveTimezone, sharedBarStore, timeframeToMs, timezoneMenuRows, tzButtonLabel, tzMenuLabel, tzOffset };
28880
+ export { BUILTIN_PRICE_STYLES, BarStore, CachingDataFeed, DARK_THEME, DataControl, DrawingStore, DrawingToolbar, DrawingsControl, LEGEND_AT_TOP_ATTR, LIGHT_THEME, MarksControl, MultiProviderFeed, NativeRenderer, RendererControl, ReplayControl, SecondClock, TIMEZONES, TypedEventBus, Vela, applyAttributionMarkTheme, buildToolbar, createAttributionMark, createCustomMark, defaultMemberOf, defaultToolbar, groupKeyOf, groupMembers, isExchangeTimezone, isGroupRow, normalizeSession, normalizeTimezone, parseSymbol, priceStyleIds, registerBuiltinChartTypes, resolveTheme, resolveTimezone, sharedBarStore, timeframeToMs, timezoneMenuRows, tzButtonLabel, tzMenuLabel, tzOffset };