@luxalgo/vela 0.7.8 → 0.8.0

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 (46) hide show
  1. package/NOTICE +28 -28
  2. package/dist/{DataProvider-CGa4KHRa.d.cts → DataProvider-BrlxntPN.d.cts} +1 -1
  3. package/dist/{DataProvider-CmWUtDVL.d.ts → DataProvider-DwFIoV4r.d.ts} +1 -1
  4. package/dist/{chunk-WTWGEB7K.js → chunk-2IAETM2I.js} +260 -2
  5. package/dist/{chunk-CAFCLMPF.js → chunk-BZQM2XO7.js} +1 -0
  6. package/dist/{chunk-M6GP7DAX.js → chunk-E3GU6AUI.js} +459 -37
  7. package/dist/{chunk-BFA32GOU.js → chunk-EZ5FWVLA.js} +5 -2
  8. package/dist/{chunk-PVGWNF7V.js → chunk-KA77UTRF.js} +2 -2
  9. package/dist/{chunk-N4ZZYLMP.js → chunk-QBPUT5SU.js} +686 -251
  10. package/dist/{contributions-DF_Ea07m.d.cts → contributions-CtlptxR8.d.cts} +187 -5
  11. package/dist/{contributions-nKOSeLy8.d.ts → contributions-tjNExo1o.d.ts} +187 -5
  12. package/dist/index.cjs +966 -247
  13. package/dist/index.d.cts +24 -8
  14. package/dist/index.d.ts +24 -8
  15. package/dist/index.js +28 -4
  16. package/dist/{options-D1AJKsn_.d.ts → options-BnEPUuVV.d.cts} +1 -1
  17. package/dist/{options-D1AJKsn_.d.cts → options-BnEPUuVV.d.ts} +1 -1
  18. package/dist/{plugin-6F04gjQe.d.cts → plugin-BWxJRKzS.d.cts} +10 -4
  19. package/dist/{plugin-BLsNipkc.d.ts → plugin-e2WUEtiI.d.ts} +10 -4
  20. package/dist/plugin.cjs +1 -0
  21. package/dist/plugin.d.cts +4 -4
  22. package/dist/plugin.d.ts +4 -4
  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/{statusline-model-DAFyqVmR.d.ts → statusline-model-DFY7tuHj.d.cts} +5 -3
  31. package/dist/{statusline-model-BoWJ3cWQ.d.cts → statusline-model-zKXtGIjj.d.ts} +5 -3
  32. package/dist/ui.cjs +263 -0
  33. package/dist/ui.d.cts +75 -2
  34. package/dist/ui.d.ts +75 -2
  35. package/dist/ui.js +3 -3
  36. package/dist/vela.global.js +968 -249
  37. package/dist/vela.global.min.js +73 -71
  38. package/dist/widget.cjs +1411 -292
  39. package/dist/widget.d.cts +26 -7
  40. package/dist/widget.d.ts +26 -7
  41. package/dist/widget.js +7 -7
  42. package/dist/workspace.cjs +1414 -292
  43. package/dist/workspace.d.cts +124 -6
  44. package/dist/workspace.d.ts +124 -6
  45. package/dist/workspace.js +6 -6
  46. package/package.json +1 -1
@@ -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-2IAETM2I.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;
@@ -8039,6 +7942,8 @@ var SceneGraph = class {
8039
7942
  /** VPVR-layer config pushed by the VPVR native indicator (null ⇒ layer off). Ephemeral. */
8040
7943
  this.vpvrLayer = null;
8041
7944
  this.crosshair = null;
7945
+ /** Runtime override of the crosshair's lines and style (the `crosshairOverride` feature). Ephemeral. */
7946
+ this.crosshairOverride = null;
8042
7947
  /** How the base price series is drawn on the price pane (candles by default). */
8043
7948
  this.priceStyle = "candles";
8044
7949
  /** Price-series base painting for the ACTIVE style (see ChartTypeDefinition.basePainting). */
@@ -9955,9 +9860,9 @@ function timeTicks(fromMs, toMs, target = 8, offsetMs = 0) {
9955
9860
  }
9956
9861
 
9957
9862
  // src/renderers/native/chrome/countdown.ts
9958
- function countdownText(barOpen, barMs, now) {
9863
+ function countdownText(barOpen, barMs, now2) {
9959
9864
  if (!(barMs > 0)) return null;
9960
- const remaining = barOpen + barMs - now;
9865
+ const remaining = barOpen + barMs - now2;
9961
9866
  if (remaining <= 0) return null;
9962
9867
  return formatCountdown(remaining);
9963
9868
  }
@@ -10769,19 +10674,35 @@ var CrosshairRenderer = class {
10769
10674
  const dataH = coords.height;
10770
10675
  if (!ch || ch.x < 0 || ch.x > dataW || ch.y < 0 || ch.y > dataH) return;
10771
10676
  const cs = scene.style.crosshair;
10677
+ const ov = scene.crosshairOverride;
10678
+ const vertical = ov?.vertical !== false;
10679
+ const horizontal = ov?.horizontal !== false;
10772
10680
  ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
10773
10681
  ctx.textBaseline = "middle";
10774
10682
  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);
10683
+ const x = crisp(coords.logicalToX(logical), ov?.width ?? cs.width);
10684
+ if (vertical && ov?.shadeRight) {
10685
+ const from = Math.max(0, Math.round(coords.logicalToX(logical + 0.5)));
10686
+ if (from < dataW) {
10687
+ ctx.fillStyle = ov.shadeRight.color;
10688
+ ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
10689
+ ctx.fillRect(from, 0, dataW - from, dataH);
10690
+ ctx.globalAlpha = 1;
10691
+ }
10692
+ }
10693
+ ctx.strokeStyle = ov?.color ?? cs.color ?? theme.textColor;
10694
+ ctx.lineWidth = ov?.width ?? cs.width;
10695
+ ctx.globalAlpha = ov?.opacity ?? cs.opacity;
10696
+ setDash3(ctx, ov?.style ?? cs.style);
10780
10697
  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);
10698
+ if (vertical) {
10699
+ ctx.moveTo(x, 0);
10700
+ ctx.lineTo(x, dataH);
10701
+ }
10702
+ if (horizontal) {
10703
+ ctx.moveTo(0, Math.round(ch.y) + 0.5);
10704
+ ctx.lineTo(dataW, Math.round(ch.y) + 0.5);
10705
+ }
10785
10706
  ctx.stroke();
10786
10707
  ctx.setLineDash([]);
10787
10708
  ctx.lineWidth = 1;
@@ -10794,11 +10715,11 @@ var CrosshairRenderer = class {
10794
10715
  }
10795
10716
  }
10796
10717
  const chipBg = cs.labelBackground ?? theme.borderColor;
10797
- if (pane && pane.axisFormat !== "none") {
10718
+ if (horizontal && pane && pane.axisFormat !== "none") {
10798
10719
  const price = coords.yToPrice(ch.y, pane.scale, pane.bounds);
10799
10720
  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
10721
  }
10801
- this.chip(ctx, x, dataH + 1, formatTimeStamp(coords.logicalToTime(logical), scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
10722
+ if (vertical) this.chip(ctx, x, dataH + 1, formatTimeStamp(coords.logicalToTime(logical), scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
10802
10723
  }
10803
10724
  destroy() {
10804
10725
  this.canvas = null;
@@ -10809,23 +10730,37 @@ var CrosshairRenderer = class {
10809
10730
  * along), with that bar's time chip in this chart's own timezone and — when the
10810
10731
  * level resolved — the price chip on the right axis. The snap happened upstream
10811
10732
  * (`externalCrossPx`, floor-to-containing-bar) — this method only draws. Chips
10812
- * render slightly dimmed so the ghost still reads as foreign. */
10733
+ * render slightly dimmed so the ghost still reads as foreign. A `crosshairOverride`
10734
+ * applies to the ghost as well — its line style, a dropped horizontal line, and the
10735
+ * `shadeRight` veil from the ghost bar's right edge — so a pick that spans several
10736
+ * charts reads the same on every one of them. */
10813
10737
  drawExternal(ctx, ext, scene, coords, theme) {
10814
10738
  const cs = scene.style.crosshair;
10739
+ const ov = scene.crosshairOverride;
10815
10740
  const dataW = coords.width;
10816
10741
  const dataH = coords.height;
10817
- const x = Math.round(ext.x) + 0.5;
10818
- if (x < 0 || x > dataW) return;
10742
+ const x = crisp(ext.x, ov?.width ?? cs.width);
10743
+ if (ov?.vertical === false) return;
10744
+ if (ov?.shadeRight) {
10745
+ const from = Math.max(0, Math.round(coords.logicalToX(Math.round(coords.xToLogical(ext.x)) + 0.5)));
10746
+ if (from < dataW) {
10747
+ ctx.fillStyle = ov.shadeRight.color;
10748
+ ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
10749
+ ctx.fillRect(from, 0, dataW - from, dataH);
10750
+ ctx.globalAlpha = 1;
10751
+ }
10752
+ }
10753
+ if (ext.line === false || x < 0 || x > dataW) return;
10819
10754
  ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
10820
10755
  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);
10756
+ ctx.strokeStyle = ov?.color ?? cs.color ?? theme.textColor;
10757
+ ctx.lineWidth = ov?.width ?? cs.width;
10758
+ ctx.globalAlpha = (ov?.opacity ?? cs.opacity) * 0.55;
10759
+ setDash3(ctx, ov?.style ?? cs.style);
10825
10760
  ctx.beginPath();
10826
10761
  ctx.moveTo(x, 0);
10827
10762
  ctx.lineTo(x, dataH);
10828
- if (ext.y != null) {
10763
+ if (ext.y != null && ov?.horizontal !== false) {
10829
10764
  ctx.moveTo(0, Math.round(ext.y) + 0.5);
10830
10765
  ctx.lineTo(dataW, Math.round(ext.y) + 0.5);
10831
10766
  }
@@ -10834,7 +10769,7 @@ var CrosshairRenderer = class {
10834
10769
  ctx.lineWidth = 1;
10835
10770
  ctx.globalAlpha = 0.8;
10836
10771
  const chipBg = cs.labelBackground ?? theme.borderColor;
10837
- if (ext.y != null && ext.price != null) {
10772
+ if (ext.y != null && ext.price != null && ov?.horizontal !== false) {
10838
10773
  let pane;
10839
10774
  for (const p of scene.panes.values()) {
10840
10775
  if (ext.y >= p.bounds.top && ext.y <= p.bounds.top + p.bounds.height) {
@@ -10874,6 +10809,9 @@ var CrosshairRenderer = class {
10874
10809
  ctx.textAlign = "start";
10875
10810
  }
10876
10811
  };
10812
+ function crisp(px, width) {
10813
+ return Math.round(px) + (Math.round(width) % 2 === 1 ? 0.5 : 0);
10814
+ }
10877
10815
  function setDash3(ctx, style) {
10878
10816
  if (style === "dashed") ctx.setLineDash([6, 4]);
10879
10817
  else if (style === "dotted") ctx.setLineDash([2, 3]);
@@ -18434,7 +18372,7 @@ var NativeRenderer = class {
18434
18372
  this.moveIndicatorCbs = /* @__PURE__ */ new Set();
18435
18373
  this.priceStyleCbs = /* @__PURE__ */ new Set();
18436
18374
  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"];
18375
+ 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
18376
  /** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
18439
18377
  * so moving onto the button doesn't count as leaving). */
18440
18378
  this.onScrollProximityMove = (e) => {
@@ -18613,6 +18551,9 @@ var NativeRenderer = class {
18613
18551
  this.scene.marks = mergeMarksState(this.scene.marks, value);
18614
18552
  this.markPopover?.close();
18615
18553
  break;
18554
+ case "crosshairOverride":
18555
+ this.scene.crosshairOverride = sanitizeCrosshairOverride(value);
18556
+ break;
18616
18557
  case "keyboard":
18617
18558
  this.setKeyboardEnabled(Boolean(value));
18618
18559
  return;
@@ -18726,6 +18667,8 @@ var NativeRenderer = class {
18726
18667
  return { ...this.scene.tradeMarkers, colors: { ...this.scene.tradeMarkers.colors } };
18727
18668
  case "marks":
18728
18669
  return { visible: this.scene.marks.visible, groups: { ...this.scene.marks.groups } };
18670
+ case "crosshairOverride":
18671
+ return this.scene.crosshairOverride ? { ...this.scene.crosshairOverride } : null;
18729
18672
  case "keyboard":
18730
18673
  return this.keyboardEnabled;
18731
18674
  case "historyChords":
@@ -19304,8 +19247,8 @@ var NativeRenderer = class {
19304
19247
  for (const pane of this.scene.panes.values()) pane.scale = { ...pane.scaleTarget };
19305
19248
  this.modelAlpha = 0;
19306
19249
  const start = performance.now();
19307
- const step = (now) => {
19308
- const p = Math.min(1, (now - start) / duration);
19250
+ const step = (now2) => {
19251
+ const p = Math.min(1, (now2 - start) / duration);
19309
19252
  this.scene.bars = p >= 1 ? real : real.map((b, i) => this.revealCandle(b, i, p, n, style));
19310
19253
  this.paintData();
19311
19254
  if (p < 1) {
@@ -19321,8 +19264,8 @@ var NativeRenderer = class {
19321
19264
  fadeInModels() {
19322
19265
  const fade = Math.min(INTRO_MODEL_FADE_MS, this.intro.duration || INTRO_MODEL_FADE_MS);
19323
19266
  const start = performance.now();
19324
- const step = (now) => {
19325
- this.modelAlpha = Math.min(1, (now - start) / fade);
19267
+ const step = (now2) => {
19268
+ this.modelAlpha = Math.min(1, (now2 - start) / fade);
19326
19269
  this.paintData();
19327
19270
  if (this.modelAlpha < 1) {
19328
19271
  this.introRaf = requestAnimationFrame(step);
@@ -19845,10 +19788,13 @@ var NativeRenderer = class {
19845
19788
  this.animator.start();
19846
19789
  }
19847
19790
  } else if (!last || bar.time > last.time) {
19791
+ const vp = this.coords.getViewport();
19792
+ const holdView = n > 0 && vp.rightOffset < 0 && this.scrollTargetRO === null;
19848
19793
  this.bars.push(bar);
19849
19794
  this.syncLiveEase(bar);
19850
19795
  if (this.coords.barInterval > 0) this.coords.appendBar(bar.time);
19851
19796
  else this.coords.setBars(this.bars.map((b) => b.time));
19797
+ if (holdView) this.coords.setViewport({ ...vp, rightOffset: vp.rightOffset - 1 });
19852
19798
  } else {
19853
19799
  return;
19854
19800
  }
@@ -20530,10 +20476,10 @@ var NativeRenderer = class {
20530
20476
  if (this.markPulseRaf !== null || typeof requestAnimationFrame !== "function") return;
20531
20477
  const tick = () => {
20532
20478
  this.markPulseRaf = null;
20533
- const now = frameNow();
20534
- if (this.scene.marksFlash && this.scene.marksFlash.until <= now) this.scene.marksFlash = null;
20479
+ const now2 = frameNow();
20480
+ if (this.scene.marksFlash && this.scene.marksFlash.until <= now2) this.scene.marksFlash = null;
20535
20481
  this.scheduler?.invalidate(2 /* Chrome */);
20536
- const pulsing = this.scene.marksHoverKey !== null && now - this.scene.marksHoverSince < MARK_PULSE_MS;
20482
+ const pulsing = this.scene.marksHoverKey !== null && now2 - this.scene.marksHoverSince < MARK_PULSE_MS;
20537
20483
  if (pulsing || this.scene.marksFlash !== null) this.markPulseRaf = requestAnimationFrame(tick);
20538
20484
  };
20539
20485
  this.markPulseRaf = requestAnimationFrame(tick);
@@ -21094,9 +21040,10 @@ var NativeRenderer = class {
21094
21040
  const ext = this.externalCross;
21095
21041
  if (!ext || this.coords.barCount === 0) return null;
21096
21042
  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;
21043
+ if (!(logical < this.coords.barCount)) return null;
21044
+ const x = this.coords.logicalToX(Math.max(logical, -1));
21045
+ if (!Number.isFinite(x)) return null;
21046
+ const line = logical >= 0 && x >= 0 && x <= this.coords.width;
21100
21047
  let y = null;
21101
21048
  if (ext.price != null) {
21102
21049
  const pricePane = this.scene.orderedPanes().find((p) => p.kind === "price");
@@ -21105,7 +21052,7 @@ var NativeRenderer = class {
21105
21052
  if (!Number.isFinite(y) || y < pricePane.bounds.top || y > pricePane.bounds.top + pricePane.bounds.height) y = null;
21106
21053
  }
21107
21054
  }
21108
- return { x, y, time: this.coords.logicalToTime(logical), price: y != null ? ext.price : null };
21055
+ return { x, y, time: this.coords.logicalToTime(Math.max(logical, 0)), price: y != null ? ext.price : null, line };
21109
21056
  }
21110
21057
  /** Set the sticky magnet mode (called by the drawings toolbar's 3-state button). */
21111
21058
  setSnapMode(mode) {
@@ -22329,6 +22276,7 @@ var CHUNK_BARS = 1e4;
22329
22276
  var FIRST_PAINT_BARS = 20;
22330
22277
  var GAP_FACTOR = 1.5;
22331
22278
  var HEAL_COOLDOWN_MS = 5e3;
22279
+ var MIN_TICK_UPDATE_MS = 16;
22332
22280
  var EngineOrchestrator = class _EngineOrchestrator {
22333
22281
  constructor(container, renderer, feed, engines, config, dataControl) {
22334
22282
  this.renderer = renderer;
@@ -22381,6 +22329,32 @@ var EngineOrchestrator = class _EngineOrchestrator {
22381
22329
  this.lastHealAt = 0;
22382
22330
  /** The `visibilitychange` catch-up listener (removed on destroy). */
22383
22331
  this.onVisible = null;
22332
+ // ── bar replay: `rawBars` holds the history up to the cursor, the rest waits here ──
22333
+ /** Bars still hidden right of the replay cursor, oldest first; null ≡ replay off. */
22334
+ this.replayQueue = null;
22335
+ this.replayTimer = null;
22336
+ this.replayPlaying = false;
22337
+ this.replayIntervalMs = 1e3;
22338
+ /** The depth (`market.bars`) before a replay deepened the history — restored when it ends. */
22339
+ this.replayDepth = null;
22340
+ /** A start is loading older history before it can cut; `seek` = a replay was running
22341
+ * (and left silently to deepen), so abandoning the load must announce its end. */
22342
+ this.replayLoading = null;
22343
+ /** A replay carried over a timeframe/session switch, between the old series' silent end
22344
+ * and the cut of the new one: still ACTIVE for the outside (`replayState`), resumed
22345
+ * at `revealedEnd` once the new bars land, unless stopped (`cancelled`) meanwhile. */
22346
+ this.replayCarrying = null;
22347
+ /** Intrabar prices for the bars replay reveals — null ≡ whole bars. */
22348
+ this.replayTickSource = null;
22349
+ /** The bar being revealed tick by tick: its stored form (what it settles on), its ticks,
22350
+ * the next tick to apply, the candle built so far, the volume each tick adds when the
22351
+ * ticks carry none, and its pacing (ticks per update, ms between updates — one play
22352
+ * interval per tick — and the clock time tick 0 is due at, so late timers catch up). */
22353
+ this.replayForming = null;
22354
+ /** Tick requests by bar time, shared by prefetch and playback; `ticks` once they landed. */
22355
+ this.replayTickFetches = /* @__PURE__ */ new Map();
22356
+ /** Aborts the tick requests in flight — renewed whenever the replay moves on. */
22357
+ this.replayTickAbort = null;
22384
22358
  /** What the last applied bar did: refine the open candle, or open a new one (which
22385
22359
  * settles the previous). Read by the `script:run` cause attribution. */
22386
22360
  this.barCause = "tick";
@@ -22604,6 +22578,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
22604
22578
  const paint = (bars, final) => {
22605
22579
  if (this.generation !== gen || !final && bars.length === 0) return;
22606
22580
  if (!painted && !final && bars.length < Math.min(requested, FIRST_PAINT_BARS)) return;
22581
+ if (painted && this.replayQueue) {
22582
+ this.mergeHistoryIntoReplay(bars);
22583
+ return;
22584
+ }
22607
22585
  this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
22608
22586
  if (!painted && bars.length > 0) {
22609
22587
  painted = true;
@@ -22746,6 +22724,36 @@ var EngineOrchestrator = class _EngineOrchestrator {
22746
22724
  // exactly like a timeframe change; the cache keys the sessions apart.
22747
22725
  next.session !== void 0 && next.session !== m.session || next.data !== void 0;
22748
22726
  const depthChanged = next.bars !== void 0 && next.bars !== m.bars;
22727
+ const sameClock = identityChanged && next.data === void 0 && (next.symbol === void 0 || next.symbol === m.symbol) && (next.timeframe !== void 0 || next.session !== void 0);
22728
+ const pending = identityChanged ? this.replayCarrying : null;
22729
+ if (pending) {
22730
+ this.replayCarrying = null;
22731
+ if (!sameClock) {
22732
+ pending.cancelled = true;
22733
+ this.events.emit("replay:end", { reason: "market" });
22734
+ }
22735
+ }
22736
+ 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;
22737
+ if (pending) pending.cancelled = true;
22738
+ if (replayCarry) this.replayCarrying = replayCarry;
22739
+ if (replayCarry && this.replayQueue) {
22740
+ this.endReplay(null, { restore: false, resume: false });
22741
+ const depth = this.replayDepth;
22742
+ this.replayDepth = null;
22743
+ if (depth && next.bars === void 0) {
22744
+ if (depth.bars === void 0) delete m.bars;
22745
+ else m.bars = depth.bars;
22746
+ }
22747
+ }
22748
+ if (this.replayQueue && (identityChanged || depthChanged)) this.endReplay("market", { restore: !identityChanged, resume: false });
22749
+ if (this.replayLoading && (identityChanged || depthChanged)) {
22750
+ const depth = this.replayDepth;
22751
+ this.abandonReplayLoad("market");
22752
+ if (depth && next.bars === void 0) {
22753
+ if (depth.bars === void 0) delete m.bars;
22754
+ else m.bars = depth.bars;
22755
+ }
22756
+ }
22749
22757
  const depthOnly = depthChanged && !identityChanged && this.rawBars.length > 0 && !m.data?.length && ((next.bars ?? 0) <= this.rawBars.length || typeof this.feed.loadRange === "function");
22750
22758
  if (!identityChanged && !depthChanged) {
22751
22759
  if (typeof next.visibleRange === "string") this.setVisibleRangePreset(next.visibleRange);
@@ -22803,15 +22811,20 @@ var EngineOrchestrator = class _EngineOrchestrator {
22803
22811
  const from = bars[Math.max(0, idx - (carriedCount - 1))].time;
22804
22812
  this.renderer.setVisibleRange({ from, to: Math.max(carried.right, bars[idx].time + 1) });
22805
22813
  }
22806
- if (!depthOnly) {
22814
+ const carry = replayCarry && !replayCarry.cancelled ? replayCarry : null;
22815
+ const carryFrom = carry ? carry.revealedEnd - this.barIntervalMs() : 0;
22816
+ const carryNow = carry != null && this.rawBars.length > 0 && carryFrom >= this.rawBars[0].time;
22817
+ const replayBack = carryNow && this.resumeCarriedReplay(this.replayStart({ from: carryFrom }), carry);
22818
+ if (!depthOnly && !replayBack) {
22807
22819
  this.restartNativeIndicators();
22808
22820
  this.restartChartTypeEngine();
22809
22821
  this.reexecuteIndicators();
22810
22822
  }
22811
- this.startLive();
22823
+ if (!replayBack) this.startLive();
22812
22824
  if (identityChanged) {
22813
22825
  this.events.emit("market:changed", { symbol: m.symbol ?? "TEST", timeframe: m.timeframe ?? "60", prev: prev2 });
22814
22826
  }
22827
+ if (carry && !carryNow) this.resumeCarriedReplay(this.replayStart({ from: carryFrom }), carry);
22815
22828
  }
22816
22829
  /**
22817
22830
  * Restart every native indicator over the new market: a `NativeIndicatorContext`
@@ -22935,6 +22948,397 @@ var EngineOrchestrator = class _EngineOrchestrator {
22935
22948
  historyComplete() {
22936
22949
  return this.historyCompletePromise;
22937
22950
  }
22951
+ // ── bar replay ──────────────────────────────────────────────
22952
+ /**
22953
+ * Enter replay — or seek, when already replaying — keeping every bar opened at or before
22954
+ * `opts.from` and queueing the rest. A `from` inside the loaded bars cuts at once, even
22955
+ * while older history is still streaming in: what lands later joins the tape's head
22956
+ * (see {@link mergeHistoryIntoReplay}) and leaves the cut alone. An older `from` waits
22957
+ * for that load, then deepens if it still has to. Consumers restart over the cut exactly
22958
+ * as on a market switch — their models describe bars that are now hidden — and the
22959
+ * active chart-type engine is rebuilt without live data.
22960
+ */
22961
+ async replayStart(opts) {
22962
+ if (this.rawBars.length === 0) {
22963
+ console.warn("[vela] chart.replay.start() ignored \u2014 the chart has no bars yet");
22964
+ return;
22965
+ }
22966
+ let gen = this.generation;
22967
+ if (opts.from < this.rawBars[0].time) {
22968
+ await this.historyCompletePromise;
22969
+ if (this.generation !== gen || this.rawBars.length === 0) return;
22970
+ }
22971
+ const oldest = this.rawBars[0].time;
22972
+ if (opts.from < oldest && this.canHeal() && typeof this.feed.loadRange === "function") {
22973
+ const held = this.rawBars.length + (this.replayQueue?.length ?? 0);
22974
+ const bars = held + Math.ceil((oldest - opts.from) / this.barIntervalMs()) + 1;
22975
+ const seek = this.replayQueue !== null;
22976
+ if (seek) this.endReplay(null, { restore: true, resume: false });
22977
+ this.replayDepth ?? (this.replayDepth = { bars: this.config.market.bars });
22978
+ const deepened = this.setMarket({ bars });
22979
+ this.replayLoading = { seek };
22980
+ await deepened;
22981
+ gen = this.generation;
22982
+ await this.historyCompletePromise;
22983
+ if (!this.replayLoading || this.generation !== gen || this.rawBars.length === 0) return;
22984
+ this.replayLoading = null;
22985
+ }
22986
+ this.settleFormingBar();
22987
+ this.dropTickFetches();
22988
+ const tape = this.replayQueue ? [...this.rawBars, ...this.replayQueue] : this.rawBars;
22989
+ let cut = 0;
22990
+ while (cut < tape.length && tape[cut].time <= opts.from) cut += 1;
22991
+ cut = Math.max(1, cut);
22992
+ if (cut >= tape.length) {
22993
+ console.warn("[vela] chart.replay.start() ignored \u2014 no bar opens after the requested time");
22994
+ return;
22995
+ }
22996
+ const wasPlaying = this.replayPlaying;
22997
+ this.clearReplayTimer();
22998
+ if (!this.replayQueue) this.stopLive();
22999
+ this.replayQueue = tape.slice(cut);
23000
+ this.setBarSeries(tape.slice(0, cut));
23001
+ this.restartBarConsumers({ blank: true });
23002
+ if (wasPlaying) this.events.emit("replay:pause", void 0);
23003
+ this.events.emit("replay:start", { cursorTime: tape[cut - 1].time, remaining: this.replayQueue.length });
23004
+ this.prefetchTicks();
23005
+ }
23006
+ /** Reveal the next queued bar through the live-bar path — or complete the bar forming
23007
+ * tick by tick; the last one ends the replay. */
23008
+ replayStep() {
23009
+ if (this.replayForming) {
23010
+ this.finishFormingBar();
23011
+ return true;
23012
+ }
23013
+ const queue = this.replayQueue;
23014
+ const bar = queue?.shift();
23015
+ if (!queue || !bar) return false;
23016
+ this.replayTickFetches.delete(bar.time);
23017
+ this.applyBar(bar);
23018
+ this.completeReveal(bar);
23019
+ this.prefetchTicks();
23020
+ return true;
23021
+ }
23022
+ /**
23023
+ * Follow up a replay carried onto a new series of the same symbol (another timeframe or
23024
+ * session). The cut asked for keeps the bars that CLOSED by the end of what was revealed —
23025
+ * every finer bar of the last revealed one, never a coarser bar still open at that point
23026
+ * (no look-ahead). Once it settles: playback resumes if it was on; if the replay did not
23027
+ * come back (nothing left after that point, or a later switch took over), it ends as a
23028
+ * market switch would. Returns whether the replay is back already — a start with no
23029
+ * history to wait for cuts synchronously (an async function runs up to its first await).
23030
+ */
23031
+ resumeCarriedReplay(start, carry) {
23032
+ if (this.replayQueue && this.replayCarrying === carry) this.replayCarrying = null;
23033
+ void start.then(() => {
23034
+ if (this.replayCarrying === carry) this.replayCarrying = null;
23035
+ if (carry.cancelled) return;
23036
+ if (!this.replayQueue) this.events.emit("replay:end", { reason: "market" });
23037
+ else if (carry.playing) this.replayPlay();
23038
+ });
23039
+ return this.replayQueue !== null;
23040
+ }
23041
+ replayStepUpdate() {
23042
+ if (!this.replayQueue) return false;
23043
+ if (this.replayForming) {
23044
+ this.applyReplayTicks(1);
23045
+ return true;
23046
+ }
23047
+ const bar = this.replayQueue[0];
23048
+ if (!bar) return false;
23049
+ const entry = this.replayTickSource ? this.requestTicks(bar) : null;
23050
+ if (entry && !entry.ticks) {
23051
+ const epoch = this.replayTickAbort;
23052
+ void entry.done.then(() => {
23053
+ if (!this.replayPlaying && !this.replayForming && this.replayTickAbort === epoch && this.replayQueue?.[0] === bar) this.replayStepUpdate();
23054
+ });
23055
+ return true;
23056
+ }
23057
+ if (!entry?.ticks?.length) return this.replayStep();
23058
+ this.openFormingBar(entry.ticks, 1);
23059
+ return true;
23060
+ }
23061
+ replaySetTicks(source) {
23062
+ this.finishFormingBar();
23063
+ this.dropTickFetches();
23064
+ this.replayTickSource = source;
23065
+ this.prefetchTicks();
23066
+ }
23067
+ replayPlay(intervalMs2) {
23068
+ if (!this.replayQueue) return;
23069
+ if (intervalMs2 !== void 0) {
23070
+ if (!(Number.isFinite(intervalMs2) && intervalMs2 > 0)) {
23071
+ console.warn(`[vela] chart.replay.play(${intervalMs2}) ignored \u2014 the interval must be a positive number of ms`);
23072
+ return;
23073
+ }
23074
+ this.replayIntervalMs = intervalMs2;
23075
+ }
23076
+ this.clearReplayTimer();
23077
+ this.replayPlaying = true;
23078
+ if (this.replayForming) this.paceFormingBar(this.replayForming);
23079
+ this.scheduleReplayStep(this.replayForming?.delayMs);
23080
+ this.events.emit("replay:play", { intervalMs: this.replayIntervalMs });
23081
+ }
23082
+ replayPause() {
23083
+ if (!this.replayPlaying) return;
23084
+ this.clearReplayTimer();
23085
+ this.events.emit("replay:pause", void 0);
23086
+ }
23087
+ replayStop() {
23088
+ const carrying = this.replayCarrying;
23089
+ if (carrying) {
23090
+ carrying.cancelled = true;
23091
+ this.replayCarrying = null;
23092
+ this.events.emit("replay:end", { reason: "stopped" });
23093
+ }
23094
+ if (this.replayLoading) {
23095
+ const depth = this.replayDepth;
23096
+ this.abandonReplayLoad("stopped");
23097
+ void this.setMarket({ bars: depth?.bars ?? 500 });
23098
+ return;
23099
+ }
23100
+ this.endReplay("stopped", { restore: true, resume: true });
23101
+ }
23102
+ abandonReplayLoad(reason) {
23103
+ const loading = this.replayLoading;
23104
+ if (!loading) return;
23105
+ this.replayLoading = null;
23106
+ this.replayDepth = null;
23107
+ if (loading.seek) this.events.emit("replay:end", { reason });
23108
+ }
23109
+ replayState() {
23110
+ const active = this.replayQueue !== null;
23111
+ return {
23112
+ active: active || this.replayCarrying !== null,
23113
+ // a replay carried over a switch stays on while the new bars load
23114
+ playing: this.replayCarrying?.playing ?? this.replayPlaying,
23115
+ cursorTime: active ? this.rawBars[this.rawBars.length - 1]?.time ?? null : null,
23116
+ remaining: this.replayQueue?.length ?? 0,
23117
+ nextTime: this.replayForming?.bar.time ?? this.replayQueue?.[0]?.time ?? null,
23118
+ intervalMs: this.replayIntervalMs
23119
+ };
23120
+ }
23121
+ replayBounds() {
23122
+ const first = this.rawBars[0];
23123
+ const last = this.replayQueue?.[this.replayQueue.length - 1] ?? this.rawBars[this.rawBars.length - 1];
23124
+ return first && last ? { first: first.time, last: last.time } : null;
23125
+ }
23126
+ scheduleReplayStep(delayMs = this.replayIntervalMs) {
23127
+ this.replayTimer = setTimeout(() => {
23128
+ this.replayTimer = null;
23129
+ this.replayAdvance();
23130
+ }, delayMs);
23131
+ }
23132
+ /** One beat of timed playback: the forming bar's next ticks, else the next bar — whole,
23133
+ * or opened from its ticks (waiting for them when they have not landed yet). */
23134
+ replayAdvance() {
23135
+ if (!this.replayPlaying || !this.replayQueue) return;
23136
+ const forming = this.replayForming;
23137
+ if (forming) {
23138
+ const due = Math.floor((now() - forming.origin) / this.replayIntervalMs) + 1;
23139
+ this.applyReplayTicks(Math.max(forming.batch, due - forming.next));
23140
+ if (this.replayPlaying) this.scheduleReplayStep(forming.delayMs);
23141
+ return;
23142
+ }
23143
+ const bar = this.replayQueue[0];
23144
+ if (!bar) return;
23145
+ const entry = this.replayTickSource ? this.requestTicks(bar) : null;
23146
+ if (entry && !entry.ticks) {
23147
+ const epoch = this.replayTickAbort;
23148
+ void entry.done.then(() => {
23149
+ if (this.replayPlaying && this.replayTimer == null && this.replayTickAbort === epoch && this.replayQueue?.[0] === bar) this.replayAdvance();
23150
+ });
23151
+ return;
23152
+ }
23153
+ if (!entry?.ticks?.length) {
23154
+ if (this.replayStep() && this.replayPlaying) this.scheduleReplayStep();
23155
+ return;
23156
+ }
23157
+ this.openFormingBar(entry.ticks);
23158
+ if (this.replayPlaying) this.scheduleReplayStep(this.replayForming?.delayMs);
23159
+ }
23160
+ /** Take the next queued bar off the tape and open it at its first tick (the first
23161
+ * `count` ticks — default: one timed update's worth). */
23162
+ openFormingBar(ticks, count) {
23163
+ const bar = this.replayQueue.shift();
23164
+ const volumeEach = ticks.every((t) => t.volume === void 0) ? (bar.volume ?? 0) / ticks.length : null;
23165
+ const forming = { bar, ticks, next: 0, candle: null, volumeEach, batch: 1, delayMs: this.replayIntervalMs, origin: 0 };
23166
+ this.paceFormingBar(forming);
23167
+ this.replayForming = forming;
23168
+ this.applyReplayTicks(count ?? forming.batch);
23169
+ this.prefetchTicks();
23170
+ }
23171
+ /** One tick per interval, batched when the interval is shorter than a frame; the clock
23172
+ * is anchored on the ticks already applied (a resume or a pace change carries on from
23173
+ * there, never jumps). */
23174
+ paceFormingBar(f) {
23175
+ f.batch = Math.max(1, Math.ceil(MIN_TICK_UPDATE_MS / this.replayIntervalMs));
23176
+ f.delayMs = this.replayIntervalMs * f.batch;
23177
+ f.origin = now() - Math.max(0, f.next - 1) * this.replayIntervalMs;
23178
+ }
23179
+ /** Fold the next `count` ticks into the forming candle; the last one settles the bar. */
23180
+ applyReplayTicks(count) {
23181
+ const f = this.replayForming;
23182
+ if (!f) return;
23183
+ let c = f.candle;
23184
+ const stop = Math.min(f.ticks.length, f.next + count);
23185
+ for (; f.next < stop; f.next += 1) {
23186
+ const t = f.ticks[f.next];
23187
+ const v = f.volumeEach ?? t.volume ?? 0;
23188
+ const hi = Math.max(t.high ?? t.price, t.price);
23189
+ const lo = Math.min(t.low ?? t.price, t.price);
23190
+ if (c) {
23191
+ c = { ...c, high: Math.max(c.high, hi), low: Math.min(c.low, lo), close: t.price, volume: (c.volume ?? 0) + v };
23192
+ } else {
23193
+ const open = t.open ?? t.price;
23194
+ c = { time: f.bar.time, open, high: Math.max(open, hi), low: Math.min(open, lo), close: t.price, volume: v };
23195
+ }
23196
+ }
23197
+ f.candle = c;
23198
+ if (f.next >= f.ticks.length) {
23199
+ this.finishFormingBar();
23200
+ return;
23201
+ }
23202
+ this.applyBar(c);
23203
+ this.events.emit("replay:tick", { cursorTime: f.bar.time, index: f.next - 1, count: f.ticks.length });
23204
+ }
23205
+ /** Settle the forming bar on its stored values and announce it revealed. */
23206
+ finishFormingBar() {
23207
+ const f = this.replayForming;
23208
+ if (!f) return;
23209
+ this.replayForming = null;
23210
+ this.replayTickFetches.delete(f.bar.time);
23211
+ this.applyBar(f.bar);
23212
+ this.events.emit("replay:tick", { cursorTime: f.bar.time, index: f.ticks.length - 1, count: f.ticks.length });
23213
+ this.completeReveal(f.bar);
23214
+ }
23215
+ /** Put the forming bar back in its stored form, silently — a seek or an end rebuilds
23216
+ * from the tape, which must hold the stored bar. */
23217
+ settleFormingBar() {
23218
+ const f = this.replayForming;
23219
+ if (!f) return;
23220
+ this.replayForming = null;
23221
+ this.applyBar(f.bar);
23222
+ }
23223
+ /**
23224
+ * A history answer landing mid-replay (a progressive snapshot — the whole answer so far):
23225
+ * the tape keeps its cut and its hidden queue; bars older than the tape join its head,
23226
+ * bars newer than it join the queue's end.
23227
+ */
23228
+ mergeHistoryIntoReplay(bars) {
23229
+ const queue = this.replayQueue;
23230
+ const head = this.rawBars[0];
23231
+ if (!queue || !head) return;
23232
+ const tail = queue[queue.length - 1] ?? this.rawBars[this.rawBars.length - 1];
23233
+ let older = 0;
23234
+ while (older < bars.length && bars[older].time < head.time) older += 1;
23235
+ let newer = bars.length;
23236
+ while (newer > older && bars[newer - 1].time > tail.time) newer -= 1;
23237
+ if (newer < bars.length) queue.push(...bars.slice(newer));
23238
+ if (older > 0) {
23239
+ this.setBarSeries([...bars.slice(0, older), ...this.rawBars], { preserveView: true });
23240
+ this.notifySessionsBars("backfill");
23241
+ }
23242
+ }
23243
+ /** A bar is fully on screen: announce it, and end the replay if it was the last. */
23244
+ completeReveal(bar) {
23245
+ const queue = this.replayQueue;
23246
+ if (!queue) return;
23247
+ this.events.emit("replay:step", { cursorTime: bar.time, remaining: queue.length });
23248
+ if (queue.length === 0) this.endReplay("finished", { restore: true, resume: true });
23249
+ }
23250
+ /** Ask for the ticks of the next hidden bar ahead of time. */
23251
+ prefetchTicks() {
23252
+ const next = this.replayQueue?.[0];
23253
+ if (next && this.replayTickSource) this.requestTicks(next);
23254
+ }
23255
+ /** The (shared) tick request of a queued bar — the source runs once per bar. */
23256
+ requestTicks(bar) {
23257
+ const known = this.replayTickFetches.get(bar.time);
23258
+ if (known) return known;
23259
+ const source = this.replayTickSource;
23260
+ this.replayTickAbort ?? (this.replayTickAbort = new AbortController());
23261
+ const signal = this.replayTickAbort.signal;
23262
+ const queue = this.replayQueue ?? [];
23263
+ const end = queue[queue.indexOf(bar) + 1]?.time ?? bar.time + this.barIntervalMs();
23264
+ const entry = { ticks: null, done: Promise.resolve() };
23265
+ const land = (ticks) => {
23266
+ entry.ticks = usableTicks(ticks);
23267
+ };
23268
+ const fail = (e) => {
23269
+ if (!signal.aborted) console.warn(`[vela] replay tick source failed \u2014 revealing the bar whole (${e instanceof Error ? e.message : String(e)})`);
23270
+ land([]);
23271
+ };
23272
+ try {
23273
+ const out = source(bar, { end, signal });
23274
+ if (Array.isArray(out)) land(out);
23275
+ else entry.done = Promise.resolve(out).then(land, fail);
23276
+ } catch (e) {
23277
+ fail(e);
23278
+ }
23279
+ this.replayTickFetches.set(bar.time, entry);
23280
+ return entry;
23281
+ }
23282
+ /** Abort the tick requests in flight and forget the landed ones. */
23283
+ dropTickFetches() {
23284
+ this.replayTickAbort?.abort();
23285
+ this.replayTickAbort = null;
23286
+ this.replayTickFetches.clear();
23287
+ }
23288
+ clearReplayTimer() {
23289
+ if (this.replayTimer != null) clearTimeout(this.replayTimer);
23290
+ this.replayTimer = null;
23291
+ this.replayPlaying = false;
23292
+ }
23293
+ /**
23294
+ * Leave replay. `restore` puts the queued bars back and restarts the consumers over the
23295
+ * full history (a market switch skips it: its load replaces everything anyway); `resume`
23296
+ * restarts live updates and heals the bars that closed while replaying. A null reason
23297
+ * leaves silently — a seek that re-enters right after.
23298
+ */
23299
+ endReplay(reason, opts) {
23300
+ const queue = this.replayQueue;
23301
+ if (!queue) return;
23302
+ this.clearReplayTimer();
23303
+ if (opts.restore) this.settleFormingBar();
23304
+ else this.replayForming = null;
23305
+ this.dropTickFetches();
23306
+ this.replayQueue = null;
23307
+ const depth = reason ? this.replayDepth : null;
23308
+ if (reason) this.replayDepth = null;
23309
+ if (depth) {
23310
+ if (depth.bars === void 0) delete this.config.market.bars;
23311
+ else this.config.market.bars = depth.bars;
23312
+ }
23313
+ if (opts.restore) {
23314
+ const keep = depth ? depth.bars ?? 500 : Infinity;
23315
+ if (queue.length > 0 || this.rawBars.length > keep) {
23316
+ const full = [...this.rawBars, ...queue];
23317
+ this.setBarSeries(full.length > keep ? full.slice(full.length - keep) : full);
23318
+ this.restartBarConsumers({ blank: false });
23319
+ } else if (this.config.live) {
23320
+ this.restartNativeIndicators();
23321
+ this.restartChartTypeEngine();
23322
+ }
23323
+ }
23324
+ if (opts.resume && this.config.live) {
23325
+ this.startLive();
23326
+ const last = this.rawBars[this.rawBars.length - 1];
23327
+ if (last && this.canHeal()) void this.healGap(last.time);
23328
+ }
23329
+ if (reason) this.events.emit("replay:end", { reason });
23330
+ }
23331
+ /**
23332
+ * The bars were replaced by a cut or a restore of the SAME market: every consumer re-runs.
23333
+ * A cut also blanks the mounted models first — they still paint the hidden bars' values,
23334
+ * which must not show until the re-runs land.
23335
+ */
23336
+ restartBarConsumers(opts) {
23337
+ if (opts.blank) this.blankIndicatorVisuals();
23338
+ this.restartNativeIndicators();
23339
+ this.restartChartTypeEngine();
23340
+ this.reexecuteIndicators();
23341
+ }
22938
23342
  /** The primary symbol as the data layer resolves it, or null if no symbol is set. */
22939
23343
  qualifiedSymbol() {
22940
23344
  const market = this.config.market;
@@ -23006,7 +23410,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
23006
23410
  engine.start({
23007
23411
  symbol: this.config.market.symbol ?? "TEST",
23008
23412
  timeframe: this.config.market.timeframe ?? "60",
23009
- live: this.config.live ?? false,
23413
+ live: (this.config.live ?? false) && this.replayQueue === null,
23010
23414
  session: this.config.market.session,
23011
23415
  bars: () => this.bars,
23012
23416
  data: this.dataControl,
@@ -23132,6 +23536,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
23132
23536
  record.session.notifyBars(reason);
23133
23537
  } else if (record.native) record.native.instance.onBars();
23134
23538
  }
23539
+ if (this.activeEngineStyle) this.typeEngines.get(this.activeEngineStyle)?.onBars?.();
23135
23540
  }
23136
23541
  /** Gap healing needs a ranged feed and a provider-backed series (offline `data` has no source). */
23137
23542
  canHeal() {
@@ -23154,10 +23559,11 @@ var EngineOrchestrator = class _EngineOrchestrator {
23154
23559
  try {
23155
23560
  const bars = await this.feed.loadRange(this.config.market, { from: fromMs });
23156
23561
  if (this.generation !== gen) return;
23562
+ if (this.replayQueue) return;
23157
23563
  for (const b of bars) this.applyBar(b, false);
23158
23564
  } catch {
23159
23565
  } finally {
23160
- if (this.generation === gen) {
23566
+ if (this.generation === gen && !this.replayQueue) {
23161
23567
  this.healing = false;
23162
23568
  const pending = this.healBuffer;
23163
23569
  this.healBuffer = [];
@@ -23462,6 +23868,13 @@ var EngineOrchestrator = class _EngineOrchestrator {
23462
23868
  destroy() {
23463
23869
  this.bumpGeneration();
23464
23870
  this.resolveHistoryComplete();
23871
+ this.clearReplayTimer();
23872
+ this.replayQueue = null;
23873
+ this.replayForming = null;
23874
+ this.replayCarrying = null;
23875
+ this.dropTickFetches();
23876
+ this.replayDepth = null;
23877
+ this.replayLoading = null;
23465
23878
  this.stopLive();
23466
23879
  if (this.viewportTimer != null) clearTimeout(this.viewportTimer);
23467
23880
  this.viewportUnsub?.();
@@ -23560,6 +23973,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
23560
23973
  this.emitScriptRun(id, cause, first);
23561
23974
  },
23562
23975
  onAlert: (a) => {
23976
+ if (this.replayQueue || this.replayCarrying) return;
23563
23977
  const indicator = record.options?.title ?? record.prepared?.meta.title ?? record.title;
23564
23978
  this.events.emit("alert", { ...a, indicator });
23565
23979
  handle.emit("alert", { id: a.id, message: a.message, title: a.title, time: a.time });
@@ -23586,7 +24000,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
23586
24000
  id,
23587
24001
  symbol: this.config.market.symbol ?? "TEST",
23588
24002
  timeframe: this.config.market.timeframe ?? "60",
23589
- live: this.config.live,
24003
+ live: this.config.live && this.replayQueue === null,
23590
24004
  session: this.config.market.session,
23591
24005
  bars: () => this.bars,
23592
24006
  data: this.dataControl,
@@ -23989,17 +24403,17 @@ var EngineOrchestrator = class _EngineOrchestrator {
23989
24403
  }
23990
24404
  emitContextChanged(id) {
23991
24405
  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);
24406
+ const now2 = Date.now();
24407
+ if (now2 - (this.contextEmitAt.get(id) ?? 0) < 1e3) return;
24408
+ this.contextEmitAt.set(id, now2);
23995
24409
  this.events.emit("context:changed", { id });
23996
24410
  }
23997
24411
  emitScriptRun(id, cause, first) {
23998
24412
  const record = this.registry.get(id);
23999
24413
  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);
24414
+ const now2 = Date.now();
24415
+ if (cause === "tick" && now2 - (this.runEmitAt.get(id) ?? 0) < RUN_EMIT_THROTTLE_MS) return;
24416
+ this.runEmitAt.set(id, now2);
24003
24417
  void Promise.resolve().then(() => this.buildScriptRun(record, cause, first)).then((run) => {
24004
24418
  if (run && this.registry.get(id)) this.events.emit("script:run", run);
24005
24419
  });
@@ -24052,6 +24466,16 @@ var EngineOrchestrator = class _EngineOrchestrator {
24052
24466
  handle?.emit("error", { error });
24053
24467
  }
24054
24468
  };
24469
+ function now() {
24470
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
24471
+ }
24472
+ function usableTicks(value) {
24473
+ if (!Array.isArray(value)) return [];
24474
+ const optional = (n) => n === void 0 || Number.isFinite(n);
24475
+ return value.filter(
24476
+ (t) => t != null && Number.isFinite(t.price) && optional(t.volume) && optional(t.high) && optional(t.low) && optional(t.open)
24477
+ );
24478
+ }
24055
24479
  function yieldToPaint() {
24056
24480
  if (typeof requestAnimationFrame === "function") {
24057
24481
  return new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
@@ -28127,6 +28551,7 @@ var Vela = class {
28127
28551
  this.panesControl = new PanesControl(this.orchestrator);
28128
28552
  this.drawingsControl = new DrawingsControl(this.orchestrator.drawings);
28129
28553
  this.marksControl = new MarksControl(this.orchestrator.marks);
28554
+ this.replayControl = new ReplayControl(this.orchestrator);
28130
28555
  }
28131
28556
  /**
28132
28557
  * Register a scripting engine so `addIndicator({ language })` can run that
@@ -28366,6 +28791,16 @@ var Vela = class {
28366
28791
  get marks() {
28367
28792
  return this.marksControl;
28368
28793
  }
28794
+ /**
28795
+ * The chart's bar-replay control surface: rewind to a past bar and reveal the
28796
+ * following ones by hand or on a timer, from the history already loaded —
28797
+ * `await chart.replay.start({ from })`, `chart.replay.play(2000)`, `chart.replay.step()`,
28798
+ * `chart.replay.stop()`. Revealed bars behave like live bars for indicators; live
28799
+ * updates pause meanwhile. Follow it with the `replay:*` events.
28800
+ */
28801
+ get replay() {
28802
+ return this.replayControl;
28803
+ }
28369
28804
  on(event, handler) {
28370
28805
  return this.orchestrator.events.on(event, handler);
28371
28806
  }
@@ -28424,4 +28859,4 @@ function resolveElement(container) {
28424
28859
  return element;
28425
28860
  }
28426
28861
 
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 };
28862
+ 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 };