@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
@@ -1230,6 +1230,7 @@ registerIcon("chevron-down", S('<path d="M3.5 6 8 10.5 12.5 6"/>'));
1230
1230
  registerIcon("chevron-up", S('<path d="M3.5 10 8 5.5 12.5 10"/>'));
1231
1231
  registerIcon("chevrons-right", S('<path d="m4 3.5 4.5 4.5L4 12.5"/><path d="m8.5 3.5 4.5 4.5-4.5 4.5"/>'));
1232
1232
  registerIcon("chevrons-left", S('<path d="M12 3.5 7.5 8l4.5 4.5"/><path d="M7.5 3.5 3 8l4.5 4.5"/>'));
1233
+ registerIcon("replay", S('<path d="M14 4.2v7.6L8.6 8z"/><path d="M7.4 4.2v7.6L2 8z"/>'));
1233
1234
  registerIcon("check", S('<path d="m2.8 8.4 3.4 3.4 7-7.6"/>'));
1234
1235
  registerIcon("close", S('<path d="m3.8 3.8 8.4 8.4M12.2 3.8l-8.4 8.4"/>'));
1235
1236
  registerIcon("pin", S('<path d="M5.6 2.5h4.8M6.4 2.5v3.6L4.4 8.4v.9h7.2v-.9L9.6 6.1V2.5M8 9.3v4.2"/>'));
@@ -3616,14 +3617,49 @@ function buildColorPicker(color, theme, onChange, options = {}) {
3616
3617
  const knob = document.createElement("div");
3617
3618
  knob.style.cssText = "position:absolute;top:50%;width:15px;height:15px;border-radius:50%;background:var(--vela-selected-bg);box-shadow:0 1px 3px rgba(0,0,0,0.55);transform:translate(-50%,-50%);pointer-events:none;";
3618
3619
  track.appendChild(knob);
3619
- const pctBox = document.createElement("div");
3620
- pctBox.style.cssText = `min-width:42px;text-align:center;font:var(--vela-font-size-md) ${theme.fontFamily};color:var(--vela-fg);border:1px solid var(--vela-border);border-radius:5px;padding:3px 4px;`;
3620
+ const pctBox = document.createElement("input");
3621
+ pctBox.type = "text";
3622
+ pctBox.inputMode = "numeric";
3623
+ pctBox.title = "Opacity (%)";
3624
+ pctBox.style.cssText = `width:46px;box-sizing:border-box;text-align:center;font:var(--vela-font-size-md) ${theme.fontFamily};color:var(--vela-fg);background:transparent;border:1px solid var(--vela-border);border-radius:5px;padding:3px 4px;outline:none;`;
3625
+ pctBox.addEventListener("focus", () => {
3626
+ pctBox.style.borderColor = "var(--vela-border-strong)";
3627
+ pctBox.select();
3628
+ });
3621
3629
  opRow.append(track, pctBox);
3622
3630
  const paintOpacity = () => {
3623
3631
  track.style.background = `linear-gradient(to right, ${curHex}00, ${curHex}ff), ${CHECKER}`;
3624
3632
  knob.style.left = `${curAlpha * 100}%`;
3625
- pctBox.textContent = `${Math.round(curAlpha * 100)}%`;
3633
+ pctBox.value = `${Math.round(curAlpha * 100)}%`;
3626
3634
  };
3635
+ const commitTyped = () => {
3636
+ pctBox.style.borderColor = "var(--vela-border)";
3637
+ const n = parseFloat(pctBox.value.replace("%", ""));
3638
+ if (Number.isFinite(n)) {
3639
+ const next = Math.max(0, Math.min(100, n)) / 100;
3640
+ if (Math.round(next * 100) !== Math.round(curAlpha * 100)) {
3641
+ curAlpha = next;
3642
+ paintOpacity();
3643
+ emit();
3644
+ return;
3645
+ }
3646
+ }
3647
+ paintOpacity();
3648
+ };
3649
+ pctBox.addEventListener("change", commitTyped);
3650
+ pctBox.addEventListener("blur", () => {
3651
+ pctBox.style.borderColor = "var(--vela-border)";
3652
+ paintOpacity();
3653
+ });
3654
+ pctBox.addEventListener("keydown", (e) => {
3655
+ if (e.key === "Escape") return;
3656
+ e.stopPropagation();
3657
+ if (e.key === "Enter") {
3658
+ e.preventDefault();
3659
+ commitTyped();
3660
+ pctBox.blur();
3661
+ }
3662
+ });
3627
3663
  let dragging = false;
3628
3664
  const onDrag = (clientX) => {
3629
3665
  const r = track.getBoundingClientRect();
@@ -4550,6 +4586,264 @@ var CalloutBubble = class {
4550
4586
  }
4551
4587
  };
4552
4588
 
4589
+ // src/ui/components/date-picker/controller.ts
4590
+ var MONTH_LABELS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
4591
+ var MONTH_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
4592
+ var WEEKDAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
4593
+ function isoDate(d) {
4594
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
4595
+ }
4596
+ function normalizeDateInput(raw) {
4597
+ const m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(raw.trim());
4598
+ if (!m) return null;
4599
+ const y = Number(m[1]);
4600
+ const mo = Number(m[2]);
4601
+ const d = Number(m[3]);
4602
+ const dt = new Date(y, mo - 1, d);
4603
+ if (dt.getFullYear() !== y || dt.getMonth() !== mo - 1 || dt.getDate() !== d) return null;
4604
+ return isoDate(dt);
4605
+ }
4606
+ function parseIsoDate(raw) {
4607
+ const iso = raw ? normalizeDateInput(raw) : null;
4608
+ if (!iso) return null;
4609
+ const [y, mo, d] = iso.split("-").map(Number);
4610
+ return new Date(y, mo - 1, d);
4611
+ }
4612
+ function datePickerController(opts = {}) {
4613
+ const today = opts.today ?? /* @__PURE__ */ new Date();
4614
+ let selected = parseIsoDate(opts.value);
4615
+ let year = selected?.getFullYear() ?? today.getFullYear();
4616
+ let month = selected?.getMonth() ?? today.getMonth();
4617
+ let panel = "date";
4618
+ const decade = () => Math.floor(year / 10) * 10;
4619
+ return {
4620
+ get panel() {
4621
+ return panel;
4622
+ },
4623
+ get year() {
4624
+ return year;
4625
+ },
4626
+ get month() {
4627
+ return month;
4628
+ },
4629
+ get value() {
4630
+ return selected ? isoDate(selected) : null;
4631
+ },
4632
+ header() {
4633
+ return {
4634
+ month: MONTH_LABELS[month] ?? "",
4635
+ monthVisible: panel === "date",
4636
+ year: panel === "year" ? `${decade()}-${decade() + 9}` : String(year),
4637
+ yearDisabled: panel === "year"
4638
+ // in the decade panel the year reads as its title
4639
+ };
4640
+ },
4641
+ navLabels() {
4642
+ if (panel === "date") return { prev: "Previous month", next: "Next month" };
4643
+ if (panel === "month") return { prev: "Previous year", next: "Next year" };
4644
+ return { prev: "Previous decade", next: "Next decade" };
4645
+ },
4646
+ cells() {
4647
+ const out = [];
4648
+ if (panel === "date") {
4649
+ const startPad = new Date(year, month, 1).getDay();
4650
+ const days = new Date(year, month + 1, 0).getDate();
4651
+ const todayIso = isoDate(today);
4652
+ const selectedIso = selected ? isoDate(selected) : "";
4653
+ for (let i = 0; i < startPad; i++) out.push({ label: "", value: "", checked: false, today: false, blank: true });
4654
+ for (let day = 1; day <= days; day++) {
4655
+ const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
4656
+ out.push({ label: String(day), value: iso, checked: iso === selectedIso, today: iso === todayIso });
4657
+ }
4658
+ return out;
4659
+ }
4660
+ if (panel === "month") {
4661
+ for (let m = 0; m < 12; m++) {
4662
+ out.push({
4663
+ label: MONTH_SHORT[m] ?? "",
4664
+ value: m,
4665
+ checked: selected?.getFullYear() === year && selected.getMonth() === m,
4666
+ today: today.getFullYear() === year && today.getMonth() === m
4667
+ });
4668
+ }
4669
+ return out;
4670
+ }
4671
+ const d = decade();
4672
+ for (let y = d - 1; y <= d + 10; y++) {
4673
+ out.push({ label: String(y), value: y, checked: selected?.getFullYear() === y, today: today.getFullYear() === y, outside: y < d || y > d + 9 });
4674
+ }
4675
+ return out;
4676
+ },
4677
+ step(dir) {
4678
+ if (panel === "date") {
4679
+ month += dir;
4680
+ if (month < 0) {
4681
+ month = 11;
4682
+ year -= 1;
4683
+ }
4684
+ if (month > 11) {
4685
+ month = 0;
4686
+ year += 1;
4687
+ }
4688
+ } else {
4689
+ year += panel === "month" ? dir : dir * 10;
4690
+ }
4691
+ },
4692
+ showMonths() {
4693
+ panel = "month";
4694
+ },
4695
+ showYears() {
4696
+ panel = "year";
4697
+ },
4698
+ choose(cell) {
4699
+ if (cell.blank) return null;
4700
+ if (panel === "date") {
4701
+ selected = parseIsoDate(String(cell.value));
4702
+ return selected ? isoDate(selected) : null;
4703
+ }
4704
+ if (panel === "month") {
4705
+ month = Number(cell.value);
4706
+ panel = "date";
4707
+ return null;
4708
+ }
4709
+ year = Number(cell.value);
4710
+ panel = "month";
4711
+ return null;
4712
+ },
4713
+ setValue(value) {
4714
+ selected = parseIsoDate(value);
4715
+ if (selected) {
4716
+ year = selected.getFullYear();
4717
+ month = selected.getMonth();
4718
+ }
4719
+ panel = "date";
4720
+ }
4721
+ };
4722
+ }
4723
+
4724
+ // src/ui/components/date-picker/styles.ts
4725
+ var DATE_PICKER_STYLE_ID = "vela-date-picker-styles";
4726
+ var DATE_PICKER_CSS = `
4727
+ .vela-date-picker{color:var(--vela-fg);font:14px var(--vela-font);user-select:none;}
4728
+ .vela-date-picker [hidden]{display:none !important;}
4729
+ .vela-date-picker-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;}
4730
+ .vela-date-picker-title{flex:1;display:flex;align-items:center;justify-content:center;gap:2px;min-width:0;}
4731
+ .vela-date-picker-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;}
4732
+ .vela-date-picker-switch:not(:disabled):hover{background:var(--vela-hover);}
4733
+ .vela-date-picker-switch:disabled{cursor:default;}
4734
+ .vela-date-picker-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;font-size:14px;}
4735
+ .vela-date-picker-nav:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
4736
+ .vela-date-picker-week,.vela-date-picker-grid{display:grid;grid-template-columns:repeat(7,28px);gap:2px;}
4737
+ .vela-date-picker-week{margin-bottom:4px;color:var(--vela-fg-muted);font-size:11px;text-align:center;}
4738
+ .vela-date-picker-week span{line-height:20px;}
4739
+ .vela-date-picker-blank{width:28px;height:28px;}
4740
+ .vela-date-picker-day{width:28px;height:28px;border:none;background:transparent;color:inherit;border-radius:4px;padding:0;cursor:pointer;font:inherit;font-size:14px;}
4741
+ .vela-date-picker-day:hover{background:var(--vela-hover);}
4742
+ .vela-date-picker-day[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
4743
+ .vela-date-picker-day[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
4744
+ .vela-date-picker-cells{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;width:calc(7 * 28px + 6 * 2px);}
4745
+ .vela-date-picker-cell{height:36px;border:none;background:transparent;color:inherit;border-radius:4px;padding:0 4px;cursor:pointer;font:inherit;font-size:14px;}
4746
+ .vela-date-picker-cell:hover{background:var(--vela-hover);}
4747
+ .vela-date-picker-cell[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
4748
+ .vela-date-picker-cell[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
4749
+ .vela-date-picker-cell[data-outside]{opacity:0.45;}
4750
+ `;
4751
+
4752
+ // src/ui/components/date-picker/view.ts
4753
+ var DatePicker = class {
4754
+ constructor(opts = {}) {
4755
+ this.opts = opts;
4756
+ const doc = document;
4757
+ injectStyles(DATE_PICKER_STYLE_ID, DATE_PICKER_CSS, doc);
4758
+ this.ctrl = datePickerController(opts);
4759
+ const button = (className) => {
4760
+ const b = doc.createElement("button");
4761
+ b.type = "button";
4762
+ b.className = className;
4763
+ return b;
4764
+ };
4765
+ this.prev = button("vela-date-picker-nav");
4766
+ this.prev.append(iconEl("chevron-left", doc));
4767
+ this.next = button("vela-date-picker-nav");
4768
+ this.next.append(iconEl("chevron-right", doc));
4769
+ this.monthBtn = button("vela-date-picker-switch");
4770
+ this.monthBtn.setAttribute("aria-label", "Choose month");
4771
+ this.yearBtn = button("vela-date-picker-switch");
4772
+ this.yearBtn.setAttribute("aria-label", "Choose year");
4773
+ const title = doc.createElement("div");
4774
+ title.className = "vela-date-picker-title";
4775
+ title.append(this.monthBtn, this.yearBtn);
4776
+ const head = doc.createElement("div");
4777
+ head.className = "vela-date-picker-head";
4778
+ head.append(this.prev, title, this.next);
4779
+ this.week = doc.createElement("div");
4780
+ this.week.className = "vela-date-picker-week";
4781
+ for (const d of WEEKDAY_LABELS) {
4782
+ const cell = doc.createElement("span");
4783
+ cell.textContent = d;
4784
+ this.week.appendChild(cell);
4785
+ }
4786
+ this.grid = doc.createElement("div");
4787
+ const on = (el, run) => el.addEventListener("click", (e) => {
4788
+ e.stopPropagation();
4789
+ run();
4790
+ this.paint();
4791
+ });
4792
+ on(this.prev, () => this.ctrl.step(-1));
4793
+ on(this.next, () => this.ctrl.step(1));
4794
+ on(this.monthBtn, () => this.ctrl.showMonths());
4795
+ on(this.yearBtn, () => this.ctrl.showYears());
4796
+ this.el = doc.createElement("div");
4797
+ this.el.className = "vela-date-picker";
4798
+ this.el.append(head, this.week, this.grid);
4799
+ this.paint();
4800
+ }
4801
+ get value() {
4802
+ return this.ctrl.value;
4803
+ }
4804
+ /** Rewrite the selection without emitting (the calendar opens on its month). */
4805
+ setValue(value) {
4806
+ this.ctrl.setValue(value);
4807
+ this.paint();
4808
+ }
4809
+ paint() {
4810
+ const doc = this.el.ownerDocument;
4811
+ const h = this.ctrl.header();
4812
+ this.monthBtn.hidden = !h.monthVisible;
4813
+ this.monthBtn.textContent = h.month;
4814
+ this.yearBtn.textContent = h.year;
4815
+ this.yearBtn.disabled = h.yearDisabled;
4816
+ const nav = this.ctrl.navLabels();
4817
+ this.prev.setAttribute("aria-label", nav.prev);
4818
+ this.next.setAttribute("aria-label", nav.next);
4819
+ const dates = this.ctrl.panel === "date";
4820
+ this.week.hidden = !dates;
4821
+ this.grid.className = dates ? "vela-date-picker-grid" : "vela-date-picker-cells";
4822
+ this.grid.replaceChildren(...this.ctrl.cells().map((cell) => this.cellEl(doc, cell, dates)));
4823
+ }
4824
+ cellEl(doc, cell, dates) {
4825
+ if (cell.blank) {
4826
+ const blank = doc.createElement("span");
4827
+ blank.className = "vela-date-picker-blank";
4828
+ return blank;
4829
+ }
4830
+ const b = doc.createElement("button");
4831
+ b.type = "button";
4832
+ b.className = dates ? "vela-date-picker-day" : "vela-date-picker-cell";
4833
+ b.textContent = cell.label;
4834
+ if (cell.checked) b.dataset.checked = "1";
4835
+ if (cell.today) b.dataset.today = "1";
4836
+ if (cell.outside) b.dataset.outside = "1";
4837
+ b.addEventListener("click", (e) => {
4838
+ e.stopPropagation();
4839
+ const picked = this.ctrl.choose(cell);
4840
+ if (picked) this.opts.onPick?.(picked);
4841
+ else this.paint();
4842
+ });
4843
+ return b;
4844
+ }
4845
+ };
4846
+
4553
4847
  // src/widget/layout-picker.ts
4554
4848
  var STYLE_ID = "vela-widget-layout-picker-v14";
4555
4849
  var CSS2 = `
@@ -4963,6 +5257,9 @@ function topbarHas(comp, id) {
4963
5257
  }
4964
5258
 
4965
5259
  // src/widget/contributions.ts
5260
+ function mobilePlacement(action) {
5261
+ return action.mobile ?? (action.align === "left" ? "bar" : "menu");
5262
+ }
4966
5263
  var registry3 = /* @__PURE__ */ new Map();
4967
5264
  var attachments = /* @__PURE__ */ new Map();
4968
5265
  var panels = /* @__PURE__ */ new Map();
@@ -5150,6 +5447,22 @@ function priceStyleIds() {
5150
5447
  for (const t of chartTypes()) if (!out.includes(t.id)) out.push(t.id);
5151
5448
  return out;
5152
5449
  }
5450
+ function sanitizeCrosshairOverride(value) {
5451
+ if (!value || typeof value !== "object") return null;
5452
+ const v = value;
5453
+ const out = {};
5454
+ if (isBool(v.vertical)) out.vertical = v.vertical;
5455
+ if (isBool(v.horizontal)) out.horizontal = v.horizontal;
5456
+ if (isColor(v.color)) out.color = v.color;
5457
+ if (isNum(v.width)) out.width = Math.max(0.5, Math.min(8, v.width));
5458
+ if (isLineStyle(v.style)) out.style = v.style;
5459
+ if (isNum(v.opacity)) out.opacity = clampOpacity(v.opacity);
5460
+ const shade = v.shadeRight;
5461
+ if (shade && typeof shade === "object" && isColor(shade.color)) {
5462
+ out.shadeRight = isNum(shade.opacity) ? { color: shade.color, opacity: clampOpacity(shade.opacity) } : { color: shade.color };
5463
+ }
5464
+ return Object.keys(out).length > 0 ? out : null;
5465
+ }
5153
5466
  function isColor(v) {
5154
5467
  return typeof v === "string" && v.trim().length > 0;
5155
5468
  }
@@ -6037,8 +6350,8 @@ var Topbar = class {
6037
6350
  // src/core/util/wall-clock.ts
6038
6351
  var BOUNDARY_SLACK_MS = 5;
6039
6352
  var SecondClock = class _SecondClock {
6040
- constructor(now = () => Date.now()) {
6041
- this.now = now;
6353
+ constructor(now2 = () => Date.now()) {
6354
+ this.now = now2;
6042
6355
  this.subs = /* @__PURE__ */ new Set();
6043
6356
  this.timer = null;
6044
6357
  }
@@ -6054,15 +6367,15 @@ var SecondClock = class _SecondClock {
6054
6367
  };
6055
6368
  }
6056
6369
  /** Milliseconds from `now` to just past the next second boundary. */
6057
- static delayToNextSecond(now) {
6058
- const intoSecond = (now % 1e3 + 1e3) % 1e3;
6370
+ static delayToNextSecond(now2) {
6371
+ const intoSecond = (now2 % 1e3 + 1e3) % 1e3;
6059
6372
  return 1e3 - intoSecond + BOUNDARY_SLACK_MS;
6060
6373
  }
6061
6374
  arm() {
6062
6375
  this.timer = setTimeout(() => {
6063
6376
  this.timer = null;
6064
- const now = this.now();
6065
- for (const cb of this.subs) cb(now);
6377
+ const now2 = this.now();
6378
+ for (const cb of this.subs) cb(now2);
6066
6379
  if (this.subs.size > 0) this.arm();
6067
6380
  }, _SecondClock.delayToNextSecond(this.now()));
6068
6381
  }
@@ -14851,7 +15164,12 @@ var TimeframeQuick = class {
14851
15164
  content: (body) => body.append(this.input, this.hint),
14852
15165
  onOpenChange: (open2) => opts.onOpenChange?.(open2)
14853
15166
  });
14854
- this.input.addEventListener("input", () => this.renderHint());
15167
+ this.input.addEventListener("input", () => {
15168
+ const { selectionStart, selectionEnd } = this.input;
15169
+ this.input.value = this.input.value.toUpperCase();
15170
+ this.input.setSelectionRange(selectionStart, selectionEnd);
15171
+ this.renderHint();
15172
+ });
14855
15173
  this.input.addEventListener("keydown", (e) => {
14856
15174
  if (e.key !== "Enter") return;
14857
15175
  const parsed = parseTimeframe(this.input.value);
@@ -14863,7 +15181,7 @@ var TimeframeQuick = class {
14863
15181
  }
14864
15182
  open(seed = "") {
14865
15183
  this.dialog.show();
14866
- this.input.value = seed;
15184
+ this.input.value = seed.toUpperCase();
14867
15185
  this.renderHint();
14868
15186
  setTimeout(() => {
14869
15187
  this.input.focus();
@@ -14881,7 +15199,7 @@ var TimeframeQuick = class {
14881
15199
  this.open(text);
14882
15200
  return;
14883
15201
  }
14884
- this.input.value += text;
15202
+ this.input.value += text.toUpperCase();
14885
15203
  this.renderHint();
14886
15204
  }
14887
15205
  close() {
@@ -14894,7 +15212,7 @@ var TimeframeQuick = class {
14894
15212
  const raw = this.input.value.trim();
14895
15213
  const parsed = parseTimeframe(raw);
14896
15214
  if (!raw) {
14897
- this.hint.textContent = "e.g. 15, 4h, D, 3M";
15215
+ this.hint.textContent = "e.g. 15, 4H, D, 3M";
14898
15216
  delete this.hint.dataset.invalid;
14899
15217
  } else if (parsed.valid) {
14900
15218
  this.hint.textContent = parsed.label ?? "";
@@ -14997,7 +15315,7 @@ var ShortcutsHelp = class {
14997
15315
  addRow("Delete the drawing under the cursor", ["Middle-click"]);
14998
15316
  const s = doc.createElement("div");
14999
15317
  s.className = "vela-sh-static";
15000
- s.textContent = "Typing a letter opens the symbol search; typing a digit opens the timeframe entry.";
15318
+ s.textContent = "Typing a letter or 0 opens the symbol search; typing a digit from 1 to 9 opens the timeframe entry.";
15001
15319
  this.list.appendChild(s);
15002
15320
  }
15003
15321
  };
@@ -15288,16 +15606,16 @@ var MobileBar = class {
15288
15606
  host.appendChild(this.el);
15289
15607
  this.renderActions();
15290
15608
  }
15291
- /** Re-project the left-aligned contributed actions as icon-only stops in the
15292
- * indicators slot (call after registrations change). Right-aligned actions stay
15293
- * in the three-dots drawer — a primary stop is what `align: 'left'` opts into. */
15609
+ /** Re-project the contributed actions placed on the bar (left-aligned by default —
15610
+ * see `mobilePlacement`) as icon-only stops in the indicators slot (call after
15611
+ * registrations change). The rest are three-dots drawer rows. */
15294
15612
  renderActions() {
15295
15613
  const ctx = this.opts.getContext?.();
15296
15614
  if (!ctx) return;
15297
15615
  const doc = this.el.ownerDocument;
15298
15616
  this.actionsHost.replaceChildren();
15299
15617
  const builtin = new Set(TOPBAR_BUILTIN_IDS);
15300
- for (const action of widgetActions("topbar", ctx).filter((a) => a.align === "left" && !builtin.has(a.id))) {
15618
+ for (const action of widgetActions("topbar", ctx).filter((a) => mobilePlacement(a) === "bar" && !builtin.has(a.id))) {
15301
15619
  const b = doc.createElement("button");
15302
15620
  b.className = "vela-mb-item";
15303
15621
  b.setAttribute("aria-label", action.label);
@@ -15853,6 +16171,7 @@ var MoreDrawer = class {
15853
16171
  const value = shape ? `${shape.cols} \xD7 ${shape.rows}` : this.opts.layout.presets().find((p) => p.checked)?.label;
15854
16172
  list.appendChild(this.row(doc, "Layout", { icon: "layout", value, chevron: true, onClick: () => this.show("layout") }));
15855
16173
  }
16174
+ for (const act of this.opts.primaryActions?.() ?? []) list.appendChild(this.actionRow(doc, act));
15856
16175
  for (const panel of this.opts.panels()) {
15857
16176
  list.appendChild(
15858
16177
  this.row(doc, panel.title, {
@@ -15868,19 +16187,19 @@ var MoreDrawer = class {
15868
16187
  const alertCount = this.opts.alerts().length;
15869
16188
  list.appendChild(this.row(doc, "Alerts", { icon: "bell", value: alertCount > 0 ? String(alertCount) : void 0, chevron: true, onClick: () => this.show("alerts") }));
15870
16189
  }
15871
- for (const act of this.opts.actions()) {
15872
- list.appendChild(
15873
- this.row(doc, act.label, {
15874
- icon: act.icon,
15875
- onClick: () => {
15876
- act.run();
15877
- this.drawer.hide();
15878
- }
15879
- })
15880
- );
15881
- }
16190
+ for (const act of this.opts.actions()) list.appendChild(this.actionRow(doc, act));
15882
16191
  this.drawer.body.appendChild(list);
15883
16192
  }
16193
+ /** A contributed action's row: runs it and closes the drawer. */
16194
+ actionRow(doc, act) {
16195
+ return this.row(doc, act.label, {
16196
+ icon: act.icon,
16197
+ onClick: () => {
16198
+ act.run();
16199
+ this.drawer.hide();
16200
+ }
16201
+ });
16202
+ }
15884
16203
  renderStyle(doc) {
15885
16204
  const list = doc.createElement("div");
15886
16205
  list.className = "vela-md-list";
@@ -18018,23 +18337,21 @@ function arrowHitHtml(svg) {
18018
18337
  return `<span class="vela-dtb-hit vela-dtb-hit--arrow">${iconSpan(svg, 11)}</span>`;
18019
18338
  }
18020
18339
 
18021
- // src/renderers/native/chrome/luxalgo-logos.ts
18022
- 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>';
18023
- 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>';
18340
+ // src/renderers/native/chrome/vela-logos.ts
18341
+ 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>';
18342
+ 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>';
18024
18343
 
18025
18344
  // src/renderers/native/chrome/AttributionMark.ts
18026
- var ATTRIBUTION_URL = "https://luxalgo.com/vela";
18345
+ var ATTRIBUTION_URL = "https://velacharts.dev/?utm_source=vela&utm_medium=attribution";
18027
18346
  var STYLE_ID20 = "vela-attribution-styles";
18028
18347
  var CSS20 = `
18029
18348
  .vela-attribution {
18030
18349
  text-decoration: none;
18031
18350
  display: flex;
18032
18351
  align-items: center;
18033
- /* Gutter, wordmark size and offset below are the brand lockup's own ratios,
18034
- measured off the official horizontal logo: gutter 0.157, wordmark height
18035
- 0.966, and wordmark 0.109 LOWER than the symbol \u2014 box-centering the two
18036
- reads wrong because the symbol's ink hangs low and the wordmark descends. */
18037
- gap: 4px;
18352
+ /* No gutter: the V is the wordmark's first letter, and the letters' viewBox starts
18353
+ where the V's ends, so the kit's own letter spacing comes with them. */
18354
+ gap: 0;
18038
18355
  }
18039
18356
  .vela-attribution .vela-attr-symbol {
18040
18357
  flex: none;
@@ -18042,7 +18359,7 @@ var CSS20 = `
18042
18359
  line-height: 0;
18043
18360
  }
18044
18361
  .vela-attribution .vela-attr-symbol svg {
18045
- height: 28px;
18362
+ height: 24px;
18046
18363
  width: auto;
18047
18364
  display: block;
18048
18365
  filter: drop-shadow(0 1px 2px var(--vela-attr-shadow, rgba(0,0,0,0.45)));
@@ -18053,15 +18370,13 @@ var CSS20 = `
18053
18370
  opacity: 0;
18054
18371
  display: flex;
18055
18372
  align-items: center;
18056
- /* Offset here, not on the image: the clip box must not crop the descender. */
18057
- position: relative;
18058
- top: 3px;
18373
+ /* Same height and baseline as the V (one viewBox height), so no vertical offset. */
18059
18374
  transform: translateX(-8px);
18060
18375
  transition: max-width 0.3s ease, opacity 0.25s ease, transform 0.3s ease;
18061
18376
  flex: none;
18062
18377
  }
18063
18378
  .vela-attribution .vela-attr-wordmark svg {
18064
- height: 27px;
18379
+ height: 24px;
18065
18380
  width: auto;
18066
18381
  display: block;
18067
18382
  filter: drop-shadow(0 1px 2px var(--vela-attr-shadow, rgba(0,0,0,0.45)));
@@ -18076,14 +18391,11 @@ var CSS20 = `
18076
18391
  (data-layout): the workspace's single grid-wide mark lives OUTSIDE any renderer
18077
18392
  container, so only the shell attribute reaches it. */
18078
18393
  [data-vela-layout='mobile'] .vela-attribution .vela-attr-symbol svg,
18079
- [data-layout='mobile'] .vela-attribution .vela-attr-symbol svg { height: 22px; }
18394
+ [data-layout='mobile'] .vela-attribution .vela-attr-symbol svg { height: 20px; }
18080
18395
  [data-vela-layout='mobile'] .vela-attribution .vela-attr-wordmark,
18081
- [data-layout='mobile'] .vela-attribution .vela-attr-wordmark {
18082
- top: 2px;
18083
- transform: translateX(-6px);
18084
- }
18396
+ [data-layout='mobile'] .vela-attribution .vela-attr-wordmark { transform: translateX(-6px); }
18085
18397
  [data-vela-layout='mobile'] .vela-attribution .vela-attr-wordmark svg,
18086
- [data-layout='mobile'] .vela-attribution .vela-attr-wordmark svg { height: 21px; }
18398
+ [data-layout='mobile'] .vela-attribution .vela-attr-wordmark svg { height: 20px; }
18087
18399
  [data-vela-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark,
18088
18400
  [data-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark { transform: translateX(0); }
18089
18401
  `;
@@ -18140,10 +18452,10 @@ function createAttributionMark(doc, background) {
18140
18452
  const symbol = doc.createElement("span");
18141
18453
  symbol.className = "vela-attr-symbol";
18142
18454
  symbol.setAttribute("aria-hidden", "true");
18143
- symbol.innerHTML = LUXALGO_SYMBOL_SVG;
18455
+ symbol.innerHTML = VELA_MARK_SVG;
18144
18456
  const wordmark = doc.createElement("span");
18145
18457
  wordmark.className = "vela-attr-wordmark";
18146
- wordmark.innerHTML = LUXALGO_WORDMARK_SVG;
18458
+ wordmark.innerHTML = VELA_LETTERS_SVG;
18147
18459
  a.append(symbol, wordmark);
18148
18460
  return a;
18149
18461
  }
@@ -18254,6 +18566,7 @@ function sanitizeCell(raw) {
18254
18566
  if (typeof c.bars === "number" && Number.isFinite(c.bars) && c.bars > 0) out.bars = c.bars;
18255
18567
  if (c.session === "regular" || c.session === "extended") out.session = c.session;
18256
18568
  if (typeof c.watermark === "boolean") out.watermark = c.watermark;
18569
+ if (typeof c.replayWatermark === "boolean") out.replayWatermark = c.replayWatermark;
18257
18570
  if (typeof c.indicatorTitles === "boolean") out.indicatorTitles = c.indicatorTitles;
18258
18571
  if (typeof c.indicatorValues === "boolean") out.indicatorValues = c.indicatorValues;
18259
18572
  if (c.rendererConfig != null && typeof c.rendererConfig === "object") out.rendererConfig = c.rendererConfig;
@@ -19006,6 +19319,7 @@ var CHUNK_BARS = 1e4;
19006
19319
  var FIRST_PAINT_BARS = 20;
19007
19320
  var GAP_FACTOR = 1.5;
19008
19321
  var HEAL_COOLDOWN_MS = 5e3;
19322
+ var MIN_TICK_UPDATE_MS = 16;
19009
19323
  var EngineOrchestrator = class _EngineOrchestrator {
19010
19324
  constructor(container, renderer, feed, engines, config, dataControl) {
19011
19325
  this.renderer = renderer;
@@ -19058,6 +19372,32 @@ var EngineOrchestrator = class _EngineOrchestrator {
19058
19372
  this.lastHealAt = 0;
19059
19373
  /** The `visibilitychange` catch-up listener (removed on destroy). */
19060
19374
  this.onVisible = null;
19375
+ // ── bar replay: `rawBars` holds the history up to the cursor, the rest waits here ──
19376
+ /** Bars still hidden right of the replay cursor, oldest first; null ≡ replay off. */
19377
+ this.replayQueue = null;
19378
+ this.replayTimer = null;
19379
+ this.replayPlaying = false;
19380
+ this.replayIntervalMs = 1e3;
19381
+ /** The depth (`market.bars`) before a replay deepened the history — restored when it ends. */
19382
+ this.replayDepth = null;
19383
+ /** A start is loading older history before it can cut; `seek` = a replay was running
19384
+ * (and left silently to deepen), so abandoning the load must announce its end. */
19385
+ this.replayLoading = null;
19386
+ /** A replay carried over a timeframe/session switch, between the old series' silent end
19387
+ * and the cut of the new one: still ACTIVE for the outside (`replayState`), resumed
19388
+ * at `revealedEnd` once the new bars land, unless stopped (`cancelled`) meanwhile. */
19389
+ this.replayCarrying = null;
19390
+ /** Intrabar prices for the bars replay reveals — null ≡ whole bars. */
19391
+ this.replayTickSource = null;
19392
+ /** The bar being revealed tick by tick: its stored form (what it settles on), its ticks,
19393
+ * the next tick to apply, the candle built so far, the volume each tick adds when the
19394
+ * ticks carry none, and its pacing (ticks per update, ms between updates — one play
19395
+ * interval per tick — and the clock time tick 0 is due at, so late timers catch up). */
19396
+ this.replayForming = null;
19397
+ /** Tick requests by bar time, shared by prefetch and playback; `ticks` once they landed. */
19398
+ this.replayTickFetches = /* @__PURE__ */ new Map();
19399
+ /** Aborts the tick requests in flight — renewed whenever the replay moves on. */
19400
+ this.replayTickAbort = null;
19061
19401
  /** What the last applied bar did: refine the open candle, or open a new one (which
19062
19402
  * settles the previous). Read by the `script:run` cause attribution. */
19063
19403
  this.barCause = "tick";
@@ -19281,6 +19621,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
19281
19621
  const paint = (bars, final) => {
19282
19622
  if (this.generation !== gen || !final && bars.length === 0) return;
19283
19623
  if (!painted && !final && bars.length < Math.min(requested, FIRST_PAINT_BARS)) return;
19624
+ if (painted && this.replayQueue) {
19625
+ this.mergeHistoryIntoReplay(bars);
19626
+ return;
19627
+ }
19284
19628
  this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
19285
19629
  if (!painted && bars.length > 0) {
19286
19630
  painted = true;
@@ -19423,6 +19767,36 @@ var EngineOrchestrator = class _EngineOrchestrator {
19423
19767
  // exactly like a timeframe change; the cache keys the sessions apart.
19424
19768
  next.session !== void 0 && next.session !== m.session || next.data !== void 0;
19425
19769
  const depthChanged = next.bars !== void 0 && next.bars !== m.bars;
19770
+ const sameClock = identityChanged && next.data === void 0 && (next.symbol === void 0 || next.symbol === m.symbol) && (next.timeframe !== void 0 || next.session !== void 0);
19771
+ const pending = identityChanged ? this.replayCarrying : null;
19772
+ if (pending) {
19773
+ this.replayCarrying = null;
19774
+ if (!sameClock) {
19775
+ pending.cancelled = true;
19776
+ this.events.emit("replay:end", { reason: "market" });
19777
+ }
19778
+ }
19779
+ 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;
19780
+ if (pending) pending.cancelled = true;
19781
+ if (replayCarry) this.replayCarrying = replayCarry;
19782
+ if (replayCarry && this.replayQueue) {
19783
+ this.endReplay(null, { restore: false, resume: false });
19784
+ const depth = this.replayDepth;
19785
+ this.replayDepth = null;
19786
+ if (depth && next.bars === void 0) {
19787
+ if (depth.bars === void 0) delete m.bars;
19788
+ else m.bars = depth.bars;
19789
+ }
19790
+ }
19791
+ if (this.replayQueue && (identityChanged || depthChanged)) this.endReplay("market", { restore: !identityChanged, resume: false });
19792
+ if (this.replayLoading && (identityChanged || depthChanged)) {
19793
+ const depth = this.replayDepth;
19794
+ this.abandonReplayLoad("market");
19795
+ if (depth && next.bars === void 0) {
19796
+ if (depth.bars === void 0) delete m.bars;
19797
+ else m.bars = depth.bars;
19798
+ }
19799
+ }
19426
19800
  const depthOnly = depthChanged && !identityChanged && this.rawBars.length > 0 && !m.data?.length && ((next.bars ?? 0) <= this.rawBars.length || typeof this.feed.loadRange === "function");
19427
19801
  if (!identityChanged && !depthChanged) {
19428
19802
  if (typeof next.visibleRange === "string") this.setVisibleRangePreset(next.visibleRange);
@@ -19480,15 +19854,20 @@ var EngineOrchestrator = class _EngineOrchestrator {
19480
19854
  const from = bars[Math.max(0, idx - (carriedCount - 1))].time;
19481
19855
  this.renderer.setVisibleRange({ from, to: Math.max(carried.right, bars[idx].time + 1) });
19482
19856
  }
19483
- if (!depthOnly) {
19857
+ const carry = replayCarry && !replayCarry.cancelled ? replayCarry : null;
19858
+ const carryFrom = carry ? carry.revealedEnd - this.barIntervalMs() : 0;
19859
+ const carryNow = carry != null && this.rawBars.length > 0 && carryFrom >= this.rawBars[0].time;
19860
+ const replayBack = carryNow && this.resumeCarriedReplay(this.replayStart({ from: carryFrom }), carry);
19861
+ if (!depthOnly && !replayBack) {
19484
19862
  this.restartNativeIndicators();
19485
19863
  this.restartChartTypeEngine();
19486
19864
  this.reexecuteIndicators();
19487
19865
  }
19488
- this.startLive();
19866
+ if (!replayBack) this.startLive();
19489
19867
  if (identityChanged) {
19490
19868
  this.events.emit("market:changed", { symbol: m.symbol ?? "TEST", timeframe: m.timeframe ?? "60", prev: prev2 });
19491
19869
  }
19870
+ if (carry && !carryNow) this.resumeCarriedReplay(this.replayStart({ from: carryFrom }), carry);
19492
19871
  }
19493
19872
  /**
19494
19873
  * Restart every native indicator over the new market: a `NativeIndicatorContext`
@@ -19612,6 +19991,397 @@ var EngineOrchestrator = class _EngineOrchestrator {
19612
19991
  historyComplete() {
19613
19992
  return this.historyCompletePromise;
19614
19993
  }
19994
+ // ── bar replay ──────────────────────────────────────────────
19995
+ /**
19996
+ * Enter replay — or seek, when already replaying — keeping every bar opened at or before
19997
+ * `opts.from` and queueing the rest. A `from` inside the loaded bars cuts at once, even
19998
+ * while older history is still streaming in: what lands later joins the tape's head
19999
+ * (see {@link mergeHistoryIntoReplay}) and leaves the cut alone. An older `from` waits
20000
+ * for that load, then deepens if it still has to. Consumers restart over the cut exactly
20001
+ * as on a market switch — their models describe bars that are now hidden — and the
20002
+ * active chart-type engine is rebuilt without live data.
20003
+ */
20004
+ async replayStart(opts) {
20005
+ if (this.rawBars.length === 0) {
20006
+ console.warn("[vela] chart.replay.start() ignored \u2014 the chart has no bars yet");
20007
+ return;
20008
+ }
20009
+ let gen = this.generation;
20010
+ if (opts.from < this.rawBars[0].time) {
20011
+ await this.historyCompletePromise;
20012
+ if (this.generation !== gen || this.rawBars.length === 0) return;
20013
+ }
20014
+ const oldest = this.rawBars[0].time;
20015
+ if (opts.from < oldest && this.canHeal() && typeof this.feed.loadRange === "function") {
20016
+ const held = this.rawBars.length + (this.replayQueue?.length ?? 0);
20017
+ const bars = held + Math.ceil((oldest - opts.from) / this.barIntervalMs()) + 1;
20018
+ const seek = this.replayQueue !== null;
20019
+ if (seek) this.endReplay(null, { restore: true, resume: false });
20020
+ this.replayDepth ?? (this.replayDepth = { bars: this.config.market.bars });
20021
+ const deepened = this.setMarket({ bars });
20022
+ this.replayLoading = { seek };
20023
+ await deepened;
20024
+ gen = this.generation;
20025
+ await this.historyCompletePromise;
20026
+ if (!this.replayLoading || this.generation !== gen || this.rawBars.length === 0) return;
20027
+ this.replayLoading = null;
20028
+ }
20029
+ this.settleFormingBar();
20030
+ this.dropTickFetches();
20031
+ const tape = this.replayQueue ? [...this.rawBars, ...this.replayQueue] : this.rawBars;
20032
+ let cut = 0;
20033
+ while (cut < tape.length && tape[cut].time <= opts.from) cut += 1;
20034
+ cut = Math.max(1, cut);
20035
+ if (cut >= tape.length) {
20036
+ console.warn("[vela] chart.replay.start() ignored \u2014 no bar opens after the requested time");
20037
+ return;
20038
+ }
20039
+ const wasPlaying = this.replayPlaying;
20040
+ this.clearReplayTimer();
20041
+ if (!this.replayQueue) this.stopLive();
20042
+ this.replayQueue = tape.slice(cut);
20043
+ this.setBarSeries(tape.slice(0, cut));
20044
+ this.restartBarConsumers({ blank: true });
20045
+ if (wasPlaying) this.events.emit("replay:pause", void 0);
20046
+ this.events.emit("replay:start", { cursorTime: tape[cut - 1].time, remaining: this.replayQueue.length });
20047
+ this.prefetchTicks();
20048
+ }
20049
+ /** Reveal the next queued bar through the live-bar path — or complete the bar forming
20050
+ * tick by tick; the last one ends the replay. */
20051
+ replayStep() {
20052
+ if (this.replayForming) {
20053
+ this.finishFormingBar();
20054
+ return true;
20055
+ }
20056
+ const queue = this.replayQueue;
20057
+ const bar = queue?.shift();
20058
+ if (!queue || !bar) return false;
20059
+ this.replayTickFetches.delete(bar.time);
20060
+ this.applyBar(bar);
20061
+ this.completeReveal(bar);
20062
+ this.prefetchTicks();
20063
+ return true;
20064
+ }
20065
+ /**
20066
+ * Follow up a replay carried onto a new series of the same symbol (another timeframe or
20067
+ * session). The cut asked for keeps the bars that CLOSED by the end of what was revealed —
20068
+ * every finer bar of the last revealed one, never a coarser bar still open at that point
20069
+ * (no look-ahead). Once it settles: playback resumes if it was on; if the replay did not
20070
+ * come back (nothing left after that point, or a later switch took over), it ends as a
20071
+ * market switch would. Returns whether the replay is back already — a start with no
20072
+ * history to wait for cuts synchronously (an async function runs up to its first await).
20073
+ */
20074
+ resumeCarriedReplay(start, carry) {
20075
+ if (this.replayQueue && this.replayCarrying === carry) this.replayCarrying = null;
20076
+ void start.then(() => {
20077
+ if (this.replayCarrying === carry) this.replayCarrying = null;
20078
+ if (carry.cancelled) return;
20079
+ if (!this.replayQueue) this.events.emit("replay:end", { reason: "market" });
20080
+ else if (carry.playing) this.replayPlay();
20081
+ });
20082
+ return this.replayQueue !== null;
20083
+ }
20084
+ replayStepUpdate() {
20085
+ if (!this.replayQueue) return false;
20086
+ if (this.replayForming) {
20087
+ this.applyReplayTicks(1);
20088
+ return true;
20089
+ }
20090
+ const bar = this.replayQueue[0];
20091
+ if (!bar) return false;
20092
+ const entry = this.replayTickSource ? this.requestTicks(bar) : null;
20093
+ if (entry && !entry.ticks) {
20094
+ const epoch = this.replayTickAbort;
20095
+ void entry.done.then(() => {
20096
+ if (!this.replayPlaying && !this.replayForming && this.replayTickAbort === epoch && this.replayQueue?.[0] === bar) this.replayStepUpdate();
20097
+ });
20098
+ return true;
20099
+ }
20100
+ if (!entry?.ticks?.length) return this.replayStep();
20101
+ this.openFormingBar(entry.ticks, 1);
20102
+ return true;
20103
+ }
20104
+ replaySetTicks(source) {
20105
+ this.finishFormingBar();
20106
+ this.dropTickFetches();
20107
+ this.replayTickSource = source;
20108
+ this.prefetchTicks();
20109
+ }
20110
+ replayPlay(intervalMs2) {
20111
+ if (!this.replayQueue) return;
20112
+ if (intervalMs2 !== void 0) {
20113
+ if (!(Number.isFinite(intervalMs2) && intervalMs2 > 0)) {
20114
+ console.warn(`[vela] chart.replay.play(${intervalMs2}) ignored \u2014 the interval must be a positive number of ms`);
20115
+ return;
20116
+ }
20117
+ this.replayIntervalMs = intervalMs2;
20118
+ }
20119
+ this.clearReplayTimer();
20120
+ this.replayPlaying = true;
20121
+ if (this.replayForming) this.paceFormingBar(this.replayForming);
20122
+ this.scheduleReplayStep(this.replayForming?.delayMs);
20123
+ this.events.emit("replay:play", { intervalMs: this.replayIntervalMs });
20124
+ }
20125
+ replayPause() {
20126
+ if (!this.replayPlaying) return;
20127
+ this.clearReplayTimer();
20128
+ this.events.emit("replay:pause", void 0);
20129
+ }
20130
+ replayStop() {
20131
+ const carrying = this.replayCarrying;
20132
+ if (carrying) {
20133
+ carrying.cancelled = true;
20134
+ this.replayCarrying = null;
20135
+ this.events.emit("replay:end", { reason: "stopped" });
20136
+ }
20137
+ if (this.replayLoading) {
20138
+ const depth = this.replayDepth;
20139
+ this.abandonReplayLoad("stopped");
20140
+ void this.setMarket({ bars: depth?.bars ?? 500 });
20141
+ return;
20142
+ }
20143
+ this.endReplay("stopped", { restore: true, resume: true });
20144
+ }
20145
+ abandonReplayLoad(reason) {
20146
+ const loading = this.replayLoading;
20147
+ if (!loading) return;
20148
+ this.replayLoading = null;
20149
+ this.replayDepth = null;
20150
+ if (loading.seek) this.events.emit("replay:end", { reason });
20151
+ }
20152
+ replayState() {
20153
+ const active = this.replayQueue !== null;
20154
+ return {
20155
+ active: active || this.replayCarrying !== null,
20156
+ // a replay carried over a switch stays on while the new bars load
20157
+ playing: this.replayCarrying?.playing ?? this.replayPlaying,
20158
+ cursorTime: active ? this.rawBars[this.rawBars.length - 1]?.time ?? null : null,
20159
+ remaining: this.replayQueue?.length ?? 0,
20160
+ nextTime: this.replayForming?.bar.time ?? this.replayQueue?.[0]?.time ?? null,
20161
+ intervalMs: this.replayIntervalMs
20162
+ };
20163
+ }
20164
+ replayBounds() {
20165
+ const first = this.rawBars[0];
20166
+ const last = this.replayQueue?.[this.replayQueue.length - 1] ?? this.rawBars[this.rawBars.length - 1];
20167
+ return first && last ? { first: first.time, last: last.time } : null;
20168
+ }
20169
+ scheduleReplayStep(delayMs = this.replayIntervalMs) {
20170
+ this.replayTimer = setTimeout(() => {
20171
+ this.replayTimer = null;
20172
+ this.replayAdvance();
20173
+ }, delayMs);
20174
+ }
20175
+ /** One beat of timed playback: the forming bar's next ticks, else the next bar — whole,
20176
+ * or opened from its ticks (waiting for them when they have not landed yet). */
20177
+ replayAdvance() {
20178
+ if (!this.replayPlaying || !this.replayQueue) return;
20179
+ const forming = this.replayForming;
20180
+ if (forming) {
20181
+ const due = Math.floor((now() - forming.origin) / this.replayIntervalMs) + 1;
20182
+ this.applyReplayTicks(Math.max(forming.batch, due - forming.next));
20183
+ if (this.replayPlaying) this.scheduleReplayStep(forming.delayMs);
20184
+ return;
20185
+ }
20186
+ const bar = this.replayQueue[0];
20187
+ if (!bar) return;
20188
+ const entry = this.replayTickSource ? this.requestTicks(bar) : null;
20189
+ if (entry && !entry.ticks) {
20190
+ const epoch = this.replayTickAbort;
20191
+ void entry.done.then(() => {
20192
+ if (this.replayPlaying && this.replayTimer == null && this.replayTickAbort === epoch && this.replayQueue?.[0] === bar) this.replayAdvance();
20193
+ });
20194
+ return;
20195
+ }
20196
+ if (!entry?.ticks?.length) {
20197
+ if (this.replayStep() && this.replayPlaying) this.scheduleReplayStep();
20198
+ return;
20199
+ }
20200
+ this.openFormingBar(entry.ticks);
20201
+ if (this.replayPlaying) this.scheduleReplayStep(this.replayForming?.delayMs);
20202
+ }
20203
+ /** Take the next queued bar off the tape and open it at its first tick (the first
20204
+ * `count` ticks — default: one timed update's worth). */
20205
+ openFormingBar(ticks, count) {
20206
+ const bar = this.replayQueue.shift();
20207
+ const volumeEach = ticks.every((t) => t.volume === void 0) ? (bar.volume ?? 0) / ticks.length : null;
20208
+ const forming = { bar, ticks, next: 0, candle: null, volumeEach, batch: 1, delayMs: this.replayIntervalMs, origin: 0 };
20209
+ this.paceFormingBar(forming);
20210
+ this.replayForming = forming;
20211
+ this.applyReplayTicks(count ?? forming.batch);
20212
+ this.prefetchTicks();
20213
+ }
20214
+ /** One tick per interval, batched when the interval is shorter than a frame; the clock
20215
+ * is anchored on the ticks already applied (a resume or a pace change carries on from
20216
+ * there, never jumps). */
20217
+ paceFormingBar(f) {
20218
+ f.batch = Math.max(1, Math.ceil(MIN_TICK_UPDATE_MS / this.replayIntervalMs));
20219
+ f.delayMs = this.replayIntervalMs * f.batch;
20220
+ f.origin = now() - Math.max(0, f.next - 1) * this.replayIntervalMs;
20221
+ }
20222
+ /** Fold the next `count` ticks into the forming candle; the last one settles the bar. */
20223
+ applyReplayTicks(count) {
20224
+ const f = this.replayForming;
20225
+ if (!f) return;
20226
+ let c = f.candle;
20227
+ const stop = Math.min(f.ticks.length, f.next + count);
20228
+ for (; f.next < stop; f.next += 1) {
20229
+ const t = f.ticks[f.next];
20230
+ const v = f.volumeEach ?? t.volume ?? 0;
20231
+ const hi = Math.max(t.high ?? t.price, t.price);
20232
+ const lo = Math.min(t.low ?? t.price, t.price);
20233
+ if (c) {
20234
+ c = { ...c, high: Math.max(c.high, hi), low: Math.min(c.low, lo), close: t.price, volume: (c.volume ?? 0) + v };
20235
+ } else {
20236
+ const open2 = t.open ?? t.price;
20237
+ c = { time: f.bar.time, open: open2, high: Math.max(open2, hi), low: Math.min(open2, lo), close: t.price, volume: v };
20238
+ }
20239
+ }
20240
+ f.candle = c;
20241
+ if (f.next >= f.ticks.length) {
20242
+ this.finishFormingBar();
20243
+ return;
20244
+ }
20245
+ this.applyBar(c);
20246
+ this.events.emit("replay:tick", { cursorTime: f.bar.time, index: f.next - 1, count: f.ticks.length });
20247
+ }
20248
+ /** Settle the forming bar on its stored values and announce it revealed. */
20249
+ finishFormingBar() {
20250
+ const f = this.replayForming;
20251
+ if (!f) return;
20252
+ this.replayForming = null;
20253
+ this.replayTickFetches.delete(f.bar.time);
20254
+ this.applyBar(f.bar);
20255
+ this.events.emit("replay:tick", { cursorTime: f.bar.time, index: f.ticks.length - 1, count: f.ticks.length });
20256
+ this.completeReveal(f.bar);
20257
+ }
20258
+ /** Put the forming bar back in its stored form, silently — a seek or an end rebuilds
20259
+ * from the tape, which must hold the stored bar. */
20260
+ settleFormingBar() {
20261
+ const f = this.replayForming;
20262
+ if (!f) return;
20263
+ this.replayForming = null;
20264
+ this.applyBar(f.bar);
20265
+ }
20266
+ /**
20267
+ * A history answer landing mid-replay (a progressive snapshot — the whole answer so far):
20268
+ * the tape keeps its cut and its hidden queue; bars older than the tape join its head,
20269
+ * bars newer than it join the queue's end.
20270
+ */
20271
+ mergeHistoryIntoReplay(bars) {
20272
+ const queue = this.replayQueue;
20273
+ const head = this.rawBars[0];
20274
+ if (!queue || !head) return;
20275
+ const tail = queue[queue.length - 1] ?? this.rawBars[this.rawBars.length - 1];
20276
+ let older = 0;
20277
+ while (older < bars.length && bars[older].time < head.time) older += 1;
20278
+ let newer = bars.length;
20279
+ while (newer > older && bars[newer - 1].time > tail.time) newer -= 1;
20280
+ if (newer < bars.length) queue.push(...bars.slice(newer));
20281
+ if (older > 0) {
20282
+ this.setBarSeries([...bars.slice(0, older), ...this.rawBars], { preserveView: true });
20283
+ this.notifySessionsBars("backfill");
20284
+ }
20285
+ }
20286
+ /** A bar is fully on screen: announce it, and end the replay if it was the last. */
20287
+ completeReveal(bar) {
20288
+ const queue = this.replayQueue;
20289
+ if (!queue) return;
20290
+ this.events.emit("replay:step", { cursorTime: bar.time, remaining: queue.length });
20291
+ if (queue.length === 0) this.endReplay("finished", { restore: true, resume: true });
20292
+ }
20293
+ /** Ask for the ticks of the next hidden bar ahead of time. */
20294
+ prefetchTicks() {
20295
+ const next = this.replayQueue?.[0];
20296
+ if (next && this.replayTickSource) this.requestTicks(next);
20297
+ }
20298
+ /** The (shared) tick request of a queued bar — the source runs once per bar. */
20299
+ requestTicks(bar) {
20300
+ const known = this.replayTickFetches.get(bar.time);
20301
+ if (known) return known;
20302
+ const source = this.replayTickSource;
20303
+ this.replayTickAbort ?? (this.replayTickAbort = new AbortController());
20304
+ const signal = this.replayTickAbort.signal;
20305
+ const queue = this.replayQueue ?? [];
20306
+ const end = queue[queue.indexOf(bar) + 1]?.time ?? bar.time + this.barIntervalMs();
20307
+ const entry = { ticks: null, done: Promise.resolve() };
20308
+ const land = (ticks) => {
20309
+ entry.ticks = usableTicks(ticks);
20310
+ };
20311
+ const fail = (e) => {
20312
+ if (!signal.aborted) console.warn(`[vela] replay tick source failed \u2014 revealing the bar whole (${e instanceof Error ? e.message : String(e)})`);
20313
+ land([]);
20314
+ };
20315
+ try {
20316
+ const out = source(bar, { end, signal });
20317
+ if (Array.isArray(out)) land(out);
20318
+ else entry.done = Promise.resolve(out).then(land, fail);
20319
+ } catch (e) {
20320
+ fail(e);
20321
+ }
20322
+ this.replayTickFetches.set(bar.time, entry);
20323
+ return entry;
20324
+ }
20325
+ /** Abort the tick requests in flight and forget the landed ones. */
20326
+ dropTickFetches() {
20327
+ this.replayTickAbort?.abort();
20328
+ this.replayTickAbort = null;
20329
+ this.replayTickFetches.clear();
20330
+ }
20331
+ clearReplayTimer() {
20332
+ if (this.replayTimer != null) clearTimeout(this.replayTimer);
20333
+ this.replayTimer = null;
20334
+ this.replayPlaying = false;
20335
+ }
20336
+ /**
20337
+ * Leave replay. `restore` puts the queued bars back and restarts the consumers over the
20338
+ * full history (a market switch skips it: its load replaces everything anyway); `resume`
20339
+ * restarts live updates and heals the bars that closed while replaying. A null reason
20340
+ * leaves silently — a seek that re-enters right after.
20341
+ */
20342
+ endReplay(reason, opts) {
20343
+ const queue = this.replayQueue;
20344
+ if (!queue) return;
20345
+ this.clearReplayTimer();
20346
+ if (opts.restore) this.settleFormingBar();
20347
+ else this.replayForming = null;
20348
+ this.dropTickFetches();
20349
+ this.replayQueue = null;
20350
+ const depth = reason ? this.replayDepth : null;
20351
+ if (reason) this.replayDepth = null;
20352
+ if (depth) {
20353
+ if (depth.bars === void 0) delete this.config.market.bars;
20354
+ else this.config.market.bars = depth.bars;
20355
+ }
20356
+ if (opts.restore) {
20357
+ const keep = depth ? depth.bars ?? 500 : Infinity;
20358
+ if (queue.length > 0 || this.rawBars.length > keep) {
20359
+ const full = [...this.rawBars, ...queue];
20360
+ this.setBarSeries(full.length > keep ? full.slice(full.length - keep) : full);
20361
+ this.restartBarConsumers({ blank: false });
20362
+ } else if (this.config.live) {
20363
+ this.restartNativeIndicators();
20364
+ this.restartChartTypeEngine();
20365
+ }
20366
+ }
20367
+ if (opts.resume && this.config.live) {
20368
+ this.startLive();
20369
+ const last = this.rawBars[this.rawBars.length - 1];
20370
+ if (last && this.canHeal()) void this.healGap(last.time);
20371
+ }
20372
+ if (reason) this.events.emit("replay:end", { reason });
20373
+ }
20374
+ /**
20375
+ * The bars were replaced by a cut or a restore of the SAME market: every consumer re-runs.
20376
+ * A cut also blanks the mounted models first — they still paint the hidden bars' values,
20377
+ * which must not show until the re-runs land.
20378
+ */
20379
+ restartBarConsumers(opts) {
20380
+ if (opts.blank) this.blankIndicatorVisuals();
20381
+ this.restartNativeIndicators();
20382
+ this.restartChartTypeEngine();
20383
+ this.reexecuteIndicators();
20384
+ }
19615
20385
  /** The primary symbol as the data layer resolves it, or null if no symbol is set. */
19616
20386
  qualifiedSymbol() {
19617
20387
  const market = this.config.market;
@@ -19683,7 +20453,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19683
20453
  engine.start({
19684
20454
  symbol: this.config.market.symbol ?? "TEST",
19685
20455
  timeframe: this.config.market.timeframe ?? "60",
19686
- live: this.config.live ?? false,
20456
+ live: (this.config.live ?? false) && this.replayQueue === null,
19687
20457
  session: this.config.market.session,
19688
20458
  bars: () => this.bars,
19689
20459
  data: this.dataControl,
@@ -19809,6 +20579,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19809
20579
  record.session.notifyBars(reason);
19810
20580
  } else if (record.native) record.native.instance.onBars();
19811
20581
  }
20582
+ if (this.activeEngineStyle) this.typeEngines.get(this.activeEngineStyle)?.onBars?.();
19812
20583
  }
19813
20584
  /** Gap healing needs a ranged feed and a provider-backed series (offline `data` has no source). */
19814
20585
  canHeal() {
@@ -19831,10 +20602,11 @@ var EngineOrchestrator = class _EngineOrchestrator {
19831
20602
  try {
19832
20603
  const bars = await this.feed.loadRange(this.config.market, { from: fromMs });
19833
20604
  if (this.generation !== gen) return;
20605
+ if (this.replayQueue) return;
19834
20606
  for (const b of bars) this.applyBar(b, false);
19835
20607
  } catch {
19836
20608
  } finally {
19837
- if (this.generation === gen) {
20609
+ if (this.generation === gen && !this.replayQueue) {
19838
20610
  this.healing = false;
19839
20611
  const pending = this.healBuffer;
19840
20612
  this.healBuffer = [];
@@ -20139,6 +20911,13 @@ var EngineOrchestrator = class _EngineOrchestrator {
20139
20911
  destroy() {
20140
20912
  this.bumpGeneration();
20141
20913
  this.resolveHistoryComplete();
20914
+ this.clearReplayTimer();
20915
+ this.replayQueue = null;
20916
+ this.replayForming = null;
20917
+ this.replayCarrying = null;
20918
+ this.dropTickFetches();
20919
+ this.replayDepth = null;
20920
+ this.replayLoading = null;
20142
20921
  this.stopLive();
20143
20922
  if (this.viewportTimer != null) clearTimeout(this.viewportTimer);
20144
20923
  this.viewportUnsub?.();
@@ -20237,6 +21016,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
20237
21016
  this.emitScriptRun(id, cause, first);
20238
21017
  },
20239
21018
  onAlert: (a) => {
21019
+ if (this.replayQueue || this.replayCarrying) return;
20240
21020
  const indicator = record.options?.title ?? record.prepared?.meta.title ?? record.title;
20241
21021
  this.events.emit("alert", { ...a, indicator });
20242
21022
  handle.emit("alert", { id: a.id, message: a.message, title: a.title, time: a.time });
@@ -20263,7 +21043,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
20263
21043
  id,
20264
21044
  symbol: this.config.market.symbol ?? "TEST",
20265
21045
  timeframe: this.config.market.timeframe ?? "60",
20266
- live: this.config.live,
21046
+ live: this.config.live && this.replayQueue === null,
20267
21047
  session: this.config.market.session,
20268
21048
  bars: () => this.bars,
20269
21049
  data: this.dataControl,
@@ -20666,17 +21446,17 @@ var EngineOrchestrator = class _EngineOrchestrator {
20666
21446
  }
20667
21447
  emitContextChanged(id) {
20668
21448
  if (!this.registry.get(id)?.session?.getContext) return;
20669
- const now = Date.now();
20670
- if (now - (this.contextEmitAt.get(id) ?? 0) < 1e3) return;
20671
- this.contextEmitAt.set(id, now);
21449
+ const now2 = Date.now();
21450
+ if (now2 - (this.contextEmitAt.get(id) ?? 0) < 1e3) return;
21451
+ this.contextEmitAt.set(id, now2);
20672
21452
  this.events.emit("context:changed", { id });
20673
21453
  }
20674
21454
  emitScriptRun(id, cause, first) {
20675
21455
  const record = this.registry.get(id);
20676
21456
  if (!record || record.native || !this.events.hasListeners("script:run")) return;
20677
- const now = Date.now();
20678
- if (cause === "tick" && now - (this.runEmitAt.get(id) ?? 0) < RUN_EMIT_THROTTLE_MS) return;
20679
- this.runEmitAt.set(id, now);
21457
+ const now2 = Date.now();
21458
+ if (cause === "tick" && now2 - (this.runEmitAt.get(id) ?? 0) < RUN_EMIT_THROTTLE_MS) return;
21459
+ this.runEmitAt.set(id, now2);
20680
21460
  void Promise.resolve().then(() => this.buildScriptRun(record, cause, first)).then((run) => {
20681
21461
  if (run && this.registry.get(id)) this.events.emit("script:run", run);
20682
21462
  });
@@ -20729,6 +21509,16 @@ var EngineOrchestrator = class _EngineOrchestrator {
20729
21509
  handle?.emit("error", { error });
20730
21510
  }
20731
21511
  };
21512
+ function now() {
21513
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
21514
+ }
21515
+ function usableTicks(value) {
21516
+ if (!Array.isArray(value)) return [];
21517
+ const optional = (n) => n === void 0 || Number.isFinite(n);
21518
+ return value.filter(
21519
+ (t) => t != null && Number.isFinite(t.price) && optional(t.volume) && optional(t.high) && optional(t.low) && optional(t.open)
21520
+ );
21521
+ }
20732
21522
  function yieldToPaint() {
20733
21523
  if (typeof requestAnimationFrame === "function") {
20734
21524
  return new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
@@ -20929,6 +21719,12 @@ var RendererControl = class {
20929
21719
  onCrosshairMove(cb) {
20930
21720
  return this.renderer.onCrosshairMove(cb);
20931
21721
  }
21722
+ /** A click — or a touch tap — on the plot, never the end of a pan: the open time of
21723
+ * the bar under it (`null` off the bars). On touch, where a tap moves no crosshair,
21724
+ * this is how an interaction learns which bar was chosen. */
21725
+ onClick(cb) {
21726
+ return this.renderer.onClick(cb);
21727
+ }
20932
21728
  /** Touch long-press on a price or time axis strip — silent no-op without the seam. */
20933
21729
  onAxisLongPress(cb) {
20934
21730
  return this.renderer.onAxisLongPress?.(cb) ?? (() => void 0);
@@ -21427,6 +22223,71 @@ var MarksControl = class {
21427
22223
  }
21428
22224
  };
21429
22225
 
22226
+ // src/core/ReplayControl.ts
22227
+ var ReplayControl = class {
22228
+ constructor(ctrl) {
22229
+ this.ctrl = ctrl;
22230
+ }
22231
+ /**
22232
+ * Enter replay (or seek, when already replaying) at `opts.from`. Resolves once the
22233
+ * chart shows the rewound history — after any in-flight history load has finished.
22234
+ * Starts paused; call {@link play} or {@link step}.
22235
+ */
22236
+ start(opts) {
22237
+ return this.ctrl.replayStart(opts);
22238
+ }
22239
+ /** Reveal the next bar — or, mid-way through a bar played tick by tick, complete it.
22240
+ * Returns false when replay is off or nothing is left. */
22241
+ step() {
22242
+ return this.ctrl.replayStep();
22243
+ }
22244
+ /** Reveal the next UPDATE: with {@link setTicks}, the next tick of the forming bar — or
22245
+ * the next bar opened at its first tick (once its ticks are in, if still loading);
22246
+ * otherwise the next whole bar, like {@link step}. Returns false when nothing is left. */
22247
+ stepUpdate() {
22248
+ return this.ctrl.replayStepUpdate();
22249
+ }
22250
+ /** Reveal one bar (one tick, with {@link setTicks}) every `intervalMs` (default: the last
22251
+ * interval used, else 1000). Calling it again while playing changes the pace. */
22252
+ play(intervalMs2) {
22253
+ this.ctrl.replayPlay(intervalMs2);
22254
+ return this;
22255
+ }
22256
+ pause() {
22257
+ this.ctrl.replayPause();
22258
+ return this;
22259
+ }
22260
+ /** Leave replay: the full history comes back and live updates resume. Also cancels a
22261
+ * `start()` still loading older history (the chart returns to its previous depth). */
22262
+ stop() {
22263
+ this.ctrl.replayStop();
22264
+ return this;
22265
+ }
22266
+ get state() {
22267
+ return this.ctrl.replayState();
22268
+ }
22269
+ /** The loaded history a replay can walk — hidden bars included while replaying. Null
22270
+ * before any bar loaded. A start older than `first` deepens the history first. */
22271
+ get bounds() {
22272
+ return this.ctrl.replayBounds();
22273
+ }
22274
+ /**
22275
+ * Play each revealed bar as a series of intrabar updates instead of whole: while
22276
+ * playing, the forming candle opens at the first tick, then every tick moves its close
22277
+ * and stretches its high and low, exactly like a live tick — indicators, chart types
22278
+ * and the `bar` event follow — and the last one settles it on the stored bar. The play
22279
+ * interval is then the time between two TICKS: a bar lasts as many intervals as it
22280
+ * has ticks (below ~16 ms, ticks are batched per frame). The next bar's ticks are
22281
+ * requested while the current one plays; playback waits for them. `null` goes back to
22282
+ * whole bars. Takes effect from the next bar; a bar forming when the source changes
22283
+ * completes at once.
22284
+ */
22285
+ setTicks(source) {
22286
+ this.ctrl.replaySetTicks(source);
22287
+ return this;
22288
+ }
22289
+ };
22290
+
21430
22291
  // src/core/model/inputs.ts
21431
22292
  function inputVisible(when, values) {
21432
22293
  if (!when) return true;
@@ -22036,10 +22897,16 @@ var IndicatorInputsDialog = class {
22036
22897
  this.calendarPop = null;
22037
22898
  this.calendarAnchor = null;
22038
22899
  },
22039
- content: (el) => fillCalendar(el, current, (iso) => {
22040
- pop.hide();
22041
- onPick(iso);
22042
- })
22900
+ content: (el) => {
22901
+ const picker = new DatePicker({
22902
+ value: current,
22903
+ onPick: (iso) => {
22904
+ pop.hide();
22905
+ onPick(iso);
22906
+ }
22907
+ });
22908
+ el.append(picker.el);
22909
+ }
22043
22910
  });
22044
22911
  this.calendarPop = pop;
22045
22912
  this.calendarAnchor = anchor;
@@ -22164,28 +23031,6 @@ function ensureDialogStyles() {
22164
23031
  .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;}
22165
23032
  .vela-ind-combo-chevron:hover{opacity:0.9;}
22166
23033
  .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;}
22167
- .vela-ind-cal [hidden]{display:none !important;}
22168
- .vela-ind-cal-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;}
22169
- .vela-ind-cal-title{flex:1;display:flex;align-items:center;justify-content:center;gap:2px;min-width:0;}
22170
- .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;}
22171
- .vela-ind-cal-switch:not(:disabled):hover{background:var(--vela-hover);}
22172
- .vela-ind-cal-switch:disabled{cursor:default;}
22173
- .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;}
22174
- .vela-ind-cal-nav:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
22175
- .vela-ind-cal-week,.vela-ind-cal-grid{display:grid;grid-template-columns:repeat(7,28px);gap:2px;}
22176
- .vela-ind-cal-week{margin-bottom:4px;color:var(--vela-fg-muted);font-size:11px;text-align:center;}
22177
- .vela-ind-cal-week span{line-height:20px;}
22178
- .vela-ind-cal-blank{width:28px;height:28px;}
22179
- .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;}
22180
- .vela-ind-cal-day:hover{background:var(--vela-hover);}
22181
- .vela-ind-cal-day[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
22182
- .vela-ind-cal-day[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
22183
- .vela-ind-cal-cells{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;width:calc(7 * 28px + 6 * 2px);}
22184
- .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;}
22185
- .vela-ind-cal-cell:hover{background:var(--vela-hover);}
22186
- .vela-ind-cal-cell[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
22187
- .vela-ind-cal-cell[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
22188
- .vela-ind-cal-cell[data-outside]{opacity:0.45;}
22189
23034
  ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
22190
23035
  .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;}
22191
23036
  .vela-ind-tab:not(.vela-ind-tab-active):hover{color:var(--vela-fg-bright);}
@@ -22219,174 +23064,6 @@ var TIME_OPTIONS = Array.from({ length: 48 }, (_, i) => {
22219
23064
  const v = `${hh}:${mm}`;
22220
23065
  return { value: v, label: v };
22221
23066
  });
22222
- var MONTH_LABELS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
22223
- var MONTH_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
22224
- var WEEKDAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
22225
- function normalizeDateInput(raw) {
22226
- const t = raw.trim();
22227
- const m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(t);
22228
- if (!m) return null;
22229
- const y = Number(m[1]);
22230
- const mo = Number(m[2]);
22231
- const d = Number(m[3]);
22232
- const dt = new Date(y, mo - 1, d);
22233
- if (dt.getFullYear() !== y || dt.getMonth() !== mo - 1 || dt.getDate() !== d) return null;
22234
- return isoDate(dt);
22235
- }
22236
- function parseIsoDate(raw) {
22237
- const iso = normalizeDateInput(raw);
22238
- if (!iso) return null;
22239
- const [y, mo, d] = iso.split("-").map(Number);
22240
- return new Date(y, mo - 1, d);
22241
- }
22242
- function isoDate(d) {
22243
- return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
22244
- }
22245
- function fillCalendar(el, current, onPick) {
22246
- const today = /* @__PURE__ */ new Date();
22247
- const selected = parseIsoDate(current);
22248
- let year = selected?.getFullYear() ?? today.getFullYear();
22249
- let month = selected?.getMonth() ?? today.getMonth();
22250
- let mode = "date";
22251
- const prev2 = document.createElement("button");
22252
- prev2.type = "button";
22253
- prev2.className = "vela-ind-cal-nav";
22254
- prev2.innerHTML = iconAt("chevron-left", 14);
22255
- const next = document.createElement("button");
22256
- next.type = "button";
22257
- next.className = "vela-ind-cal-nav";
22258
- next.innerHTML = iconAt("chevron-right", 14);
22259
- const monthBtn = document.createElement("button");
22260
- monthBtn.type = "button";
22261
- monthBtn.className = "vela-ind-cal-switch";
22262
- monthBtn.setAttribute("aria-label", "Choose month");
22263
- const yearBtn = document.createElement("button");
22264
- yearBtn.type = "button";
22265
- yearBtn.className = "vela-ind-cal-switch";
22266
- yearBtn.setAttribute("aria-label", "Choose year");
22267
- const title = document.createElement("div");
22268
- title.className = "vela-ind-cal-title";
22269
- title.append(monthBtn, yearBtn);
22270
- const head = document.createElement("div");
22271
- head.className = "vela-ind-cal-head";
22272
- head.append(prev2, title, next);
22273
- const week = document.createElement("div");
22274
- week.className = "vela-ind-cal-week";
22275
- for (const d of WEEKDAY_LABELS) {
22276
- const cell = document.createElement("span");
22277
- cell.textContent = d;
22278
- week.appendChild(cell);
22279
- }
22280
- const grid = document.createElement("div");
22281
- const paint = () => {
22282
- const decade = Math.floor(year / 10) * 10;
22283
- monthBtn.hidden = mode !== "date";
22284
- monthBtn.textContent = MONTH_LABELS[month] ?? "";
22285
- yearBtn.textContent = mode === "year" ? `${decade}-${decade + 9}` : String(year);
22286
- yearBtn.disabled = mode === "year";
22287
- week.hidden = mode !== "date";
22288
- prev2.setAttribute("aria-label", mode === "date" ? "Previous month" : mode === "month" ? "Previous year" : "Previous decade");
22289
- next.setAttribute("aria-label", mode === "date" ? "Next month" : mode === "month" ? "Next year" : "Next decade");
22290
- grid.className = mode === "date" ? "vela-ind-cal-grid" : "vela-ind-cal-cells";
22291
- grid.replaceChildren();
22292
- if (mode === "date") {
22293
- const startPad = new Date(year, month, 1).getDay();
22294
- const days = new Date(year, month + 1, 0).getDate();
22295
- const todayIso = isoDate(today);
22296
- const selectedIso = selected ? isoDate(selected) : "";
22297
- for (let i = 0; i < startPad; i++) {
22298
- const blank = document.createElement("span");
22299
- blank.className = "vela-ind-cal-blank";
22300
- grid.appendChild(blank);
22301
- }
22302
- for (let day = 1; day <= days; day++) {
22303
- const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
22304
- const b = document.createElement("button");
22305
- b.type = "button";
22306
- b.className = "vela-ind-cal-day";
22307
- b.textContent = String(day);
22308
- if (iso === selectedIso) b.dataset.checked = "1";
22309
- if (iso === todayIso) b.dataset.today = "1";
22310
- b.addEventListener("click", (e) => {
22311
- e.stopPropagation();
22312
- onPick(iso);
22313
- });
22314
- grid.appendChild(b);
22315
- }
22316
- return;
22317
- }
22318
- if (mode === "month") {
22319
- for (let m = 0; m < 12; m++) {
22320
- const b = document.createElement("button");
22321
- b.type = "button";
22322
- b.className = "vela-ind-cal-cell";
22323
- b.textContent = MONTH_SHORT[m] ?? "";
22324
- if (selected?.getFullYear() === year && selected.getMonth() === m) b.dataset.checked = "1";
22325
- if (today.getFullYear() === year && today.getMonth() === m) b.dataset.today = "1";
22326
- b.addEventListener("click", (e) => {
22327
- e.stopPropagation();
22328
- month = m;
22329
- mode = "date";
22330
- paint();
22331
- });
22332
- grid.appendChild(b);
22333
- }
22334
- return;
22335
- }
22336
- for (let y = decade - 1; y <= decade + 10; y++) {
22337
- const b = document.createElement("button");
22338
- b.type = "button";
22339
- b.className = "vela-ind-cal-cell";
22340
- b.textContent = String(y);
22341
- if (y < decade || y > decade + 9) b.dataset.outside = "1";
22342
- if (selected?.getFullYear() === y) b.dataset.checked = "1";
22343
- if (today.getFullYear() === y) b.dataset.today = "1";
22344
- b.addEventListener("click", (e) => {
22345
- e.stopPropagation();
22346
- year = y;
22347
- mode = "month";
22348
- paint();
22349
- });
22350
- grid.appendChild(b);
22351
- }
22352
- };
22353
- const step = (dir) => {
22354
- if (mode === "date") {
22355
- month += dir;
22356
- if (month < 0) {
22357
- month = 11;
22358
- year -= 1;
22359
- }
22360
- if (month > 11) {
22361
- month = 0;
22362
- year += 1;
22363
- }
22364
- } else {
22365
- year += mode === "month" ? dir : dir * 10;
22366
- }
22367
- paint();
22368
- };
22369
- prev2.addEventListener("click", (e) => {
22370
- e.stopPropagation();
22371
- step(-1);
22372
- });
22373
- next.addEventListener("click", (e) => {
22374
- e.stopPropagation();
22375
- step(1);
22376
- });
22377
- monthBtn.addEventListener("click", (e) => {
22378
- e.stopPropagation();
22379
- mode = "month";
22380
- paint();
22381
- });
22382
- yearBtn.addEventListener("click", (e) => {
22383
- e.stopPropagation();
22384
- mode = "year";
22385
- paint();
22386
- });
22387
- paint();
22388
- el.append(head, week, grid);
22389
- }
22390
23067
  function normalizeTimeInput(raw) {
22391
23068
  const t = raw.trim();
22392
23069
  const m = /^(\d{1,2}):(\d{2})$/.exec(t) ?? /^(\d{2})(\d{2})$/.exec(t);
@@ -25090,7 +25767,7 @@ var Scheduler = class {
25090
25767
 
25091
25768
  // src/renderers/native/core/Animator.ts
25092
25769
  var Animator = class {
25093
- constructor(tick, raf, cancel, now) {
25770
+ constructor(tick, raf, cancel, now2) {
25094
25771
  this.tick = tick;
25095
25772
  this.handle = null;
25096
25773
  this.last = 0;
@@ -25104,7 +25781,7 @@ var Animator = class {
25104
25781
  };
25105
25782
  this.raf = raf ?? ((cb) => requestAnimationFrame(cb));
25106
25783
  this.cancel = cancel ?? ((h) => cancelAnimationFrame(h));
25107
- this.now = now ?? (() => performance.now());
25784
+ this.now = now2 ?? (() => performance.now());
25108
25785
  }
25109
25786
  get active() {
25110
25787
  return this.handle !== null;
@@ -25236,6 +25913,12 @@ var InputController = class {
25236
25913
  this.lastTapT = 0;
25237
25914
  this.lastTapX = 0;
25238
25915
  this.lastTapY = 0;
25916
+ // Whether the last two primary presses were claimed by the drawings layer. A double-click
25917
+ // is the last press plus the one before it; a tool-driven click pair must never fall through
25918
+ // to the pane maximize toggle, even when the tool disarmed itself between the two clicks
25919
+ // (a finished placement leaves plain cursor mode for the second click).
25920
+ this.lastPressDrawing = false;
25921
+ this.prevPressDrawing = false;
25239
25922
  this.onTouchContextMenu = (e) => {
25240
25923
  if (this.touches.size === 0) return;
25241
25924
  if (!(e.target instanceof Node) || !this.el?.contains(e.target)) return;
@@ -25290,19 +25973,24 @@ var InputController = class {
25290
25973
  this.moved = false;
25291
25974
  this.startX = x;
25292
25975
  this.startY = y;
25976
+ this.prevPressDrawing = this.lastPressDrawing;
25977
+ this.lastPressDrawing = false;
25293
25978
  if (this.deps.drawingsClaim?.(x, y)) {
25294
25979
  this.region = "drawing";
25980
+ this.lastPressDrawing = true;
25295
25981
  this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
25296
25982
  this.capture(e.pointerId);
25297
25983
  return;
25298
25984
  }
25299
25985
  if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y, this.snapMode(e))) {
25300
25986
  this.region = "drawing";
25987
+ this.lastPressDrawing = true;
25301
25988
  this.capture(e.pointerId);
25302
25989
  return;
25303
25990
  }
25304
25991
  if ((e.ctrlKey || e.metaKey) && this.regionAt(x, y) === "data" && this.deps.drawingsMarqueeStart?.(x, y)) {
25305
25992
  this.region = "drawing";
25993
+ this.lastPressDrawing = true;
25306
25994
  this.capture(e.pointerId);
25307
25995
  return;
25308
25996
  }
@@ -25624,6 +26312,7 @@ var InputController = class {
25624
26312
  if (region === "price") this.deps.resetPriceScale(x, y);
25625
26313
  else if (region === "separator") this.deps.resetPaneSize(y);
25626
26314
  else if (region === "time") this.deps.resetView();
26315
+ else if (this.lastPressDrawing || this.prevPressDrawing) return;
25627
26316
  else this.deps.dataDblClick(x, y);
25628
26317
  }
25629
26318
  };
@@ -25903,6 +26592,8 @@ var SceneGraph = class {
25903
26592
  /** VPVR-layer config pushed by the VPVR native indicator (null ⇒ layer off). Ephemeral. */
25904
26593
  this.vpvrLayer = null;
25905
26594
  this.crosshair = null;
26595
+ /** Runtime override of the crosshair's lines and style (the `crosshairOverride` feature). Ephemeral. */
26596
+ this.crosshairOverride = null;
25906
26597
  /** How the base price series is drawn on the price pane (candles by default). */
25907
26598
  this.priceStyle = "candles";
25908
26599
  /** Price-series base painting for the ACTIVE style (see ChartTypeDefinition.basePainting). */
@@ -27787,6 +28478,7 @@ function timeTicks(fromMs, toMs, target = 8, offsetMs = 0) {
27787
28478
  const step = pickStep(span / Math.max(1, target));
27788
28479
  const zFrom = fromMs + offsetMs;
27789
28480
  const zTo = toMs + offsetMs;
28481
+ if (step >= MONTH) return calendarTicks(zFrom, zTo, step >= YEAR ? 12 : step >= 3 * MONTH ? 3 : 1, offsetMs);
27790
28482
  const first = Math.ceil(zFrom / step) * step;
27791
28483
  const out = [];
27792
28484
  for (let zt = first; zt <= zTo; zt += step) {
@@ -27817,11 +28509,23 @@ function timeTicks(fromMs, toMs, target = 8, offsetMs = 0) {
27817
28509
  }
27818
28510
  return out;
27819
28511
  }
28512
+ function calendarTicks(zFrom, zTo, months, offsetMs) {
28513
+ const d0 = new Date(zFrom);
28514
+ let idx = d0.getUTCFullYear() * 12 + d0.getUTCMonth();
28515
+ if (Date.UTC(Math.floor(idx / 12), idx % 12, 1) < zFrom) idx += 1;
28516
+ idx = Math.ceil(idx / months) * months;
28517
+ const out = [];
28518
+ 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)) {
28519
+ const label = months === 12 ? String(Math.floor(idx / 12)) : MONTHS[idx % 12];
28520
+ out.push({ time: zt - offsetMs, label, major: true });
28521
+ }
28522
+ return out;
28523
+ }
27820
28524
 
27821
28525
  // src/renderers/native/chrome/countdown.ts
27822
- function countdownText(barOpen, barMs, now) {
28526
+ function countdownText(barOpen, barMs, now2) {
27823
28527
  if (!(barMs > 0)) return null;
27824
- const remaining = barOpen + barMs - now;
28528
+ const remaining = barOpen + barMs - now2;
27825
28529
  if (remaining <= 0) return null;
27826
28530
  return formatCountdown(remaining);
27827
28531
  }
@@ -28633,19 +29337,35 @@ var CrosshairRenderer = class {
28633
29337
  const dataH = coords.height;
28634
29338
  if (!ch || ch.x < 0 || ch.x > dataW || ch.y < 0 || ch.y > dataH) return;
28635
29339
  const cs = scene.style.crosshair;
29340
+ const ov = scene.crosshairOverride;
29341
+ const vertical = ov?.vertical !== false;
29342
+ const horizontal = ov?.horizontal !== false;
28636
29343
  ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
28637
29344
  ctx.textBaseline = "middle";
28638
29345
  const logical = Math.round(coords.xToLogical(ch.x));
28639
- const x = Math.round(coords.logicalToX(logical)) + 0.5;
28640
- ctx.strokeStyle = cs.color ?? theme.textColor;
28641
- ctx.lineWidth = cs.width;
28642
- ctx.globalAlpha = cs.opacity;
28643
- setDash3(ctx, cs.style);
29346
+ const x = crisp(coords.logicalToX(logical), ov?.width ?? cs.width);
29347
+ if (vertical && ov?.shadeRight) {
29348
+ const from = Math.max(0, Math.round(coords.logicalToX(logical + 0.5)));
29349
+ if (from < dataW) {
29350
+ ctx.fillStyle = ov.shadeRight.color;
29351
+ ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
29352
+ ctx.fillRect(from, 0, dataW - from, dataH);
29353
+ ctx.globalAlpha = 1;
29354
+ }
29355
+ }
29356
+ ctx.strokeStyle = ov?.color ?? cs.color ?? theme.textColor;
29357
+ ctx.lineWidth = ov?.width ?? cs.width;
29358
+ ctx.globalAlpha = ov?.opacity ?? cs.opacity;
29359
+ setDash3(ctx, ov?.style ?? cs.style);
28644
29360
  ctx.beginPath();
28645
- ctx.moveTo(x, 0);
28646
- ctx.lineTo(x, dataH);
28647
- ctx.moveTo(0, Math.round(ch.y) + 0.5);
28648
- ctx.lineTo(dataW, Math.round(ch.y) + 0.5);
29361
+ if (vertical) {
29362
+ ctx.moveTo(x, 0);
29363
+ ctx.lineTo(x, dataH);
29364
+ }
29365
+ if (horizontal) {
29366
+ ctx.moveTo(0, Math.round(ch.y) + 0.5);
29367
+ ctx.lineTo(dataW, Math.round(ch.y) + 0.5);
29368
+ }
28649
29369
  ctx.stroke();
28650
29370
  ctx.setLineDash([]);
28651
29371
  ctx.lineWidth = 1;
@@ -28658,11 +29378,11 @@ var CrosshairRenderer = class {
28658
29378
  }
28659
29379
  }
28660
29380
  const chipBg = cs.labelBackground ?? theme.borderColor;
28661
- if (pane && pane.axisFormat !== "none") {
29381
+ if (horizontal && pane && pane.axisFormat !== "none") {
28662
29382
  const price = coords.yToPrice(ch.y, pane.scale, pane.bounds);
28663
29383
  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);
28664
29384
  }
28665
- this.chip(ctx, x, dataH + 1, formatTimeStamp(coords.logicalToTime(logical), scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
29385
+ if (vertical) this.chip(ctx, x, dataH + 1, formatTimeStamp(coords.logicalToTime(logical), scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
28666
29386
  }
28667
29387
  destroy() {
28668
29388
  this.canvas = null;
@@ -28673,23 +29393,37 @@ var CrosshairRenderer = class {
28673
29393
  * along), with that bar's time chip in this chart's own timezone and — when the
28674
29394
  * level resolved — the price chip on the right axis. The snap happened upstream
28675
29395
  * (`externalCrossPx`, floor-to-containing-bar) — this method only draws. Chips
28676
- * render slightly dimmed so the ghost still reads as foreign. */
29396
+ * render slightly dimmed so the ghost still reads as foreign. A `crosshairOverride`
29397
+ * applies to the ghost as well — its line style, a dropped horizontal line, and the
29398
+ * `shadeRight` veil from the ghost bar's right edge — so a pick that spans several
29399
+ * charts reads the same on every one of them. */
28677
29400
  drawExternal(ctx, ext, scene, coords, theme) {
28678
29401
  const cs = scene.style.crosshair;
29402
+ const ov = scene.crosshairOverride;
28679
29403
  const dataW = coords.width;
28680
29404
  const dataH = coords.height;
28681
- const x = Math.round(ext.x) + 0.5;
28682
- if (x < 0 || x > dataW) return;
29405
+ const x = crisp(ext.x, ov?.width ?? cs.width);
29406
+ if (ov?.vertical === false) return;
29407
+ if (ov?.shadeRight) {
29408
+ const from = Math.max(0, Math.round(coords.logicalToX(Math.round(coords.xToLogical(ext.x)) + 0.5)));
29409
+ if (from < dataW) {
29410
+ ctx.fillStyle = ov.shadeRight.color;
29411
+ ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
29412
+ ctx.fillRect(from, 0, dataW - from, dataH);
29413
+ ctx.globalAlpha = 1;
29414
+ }
29415
+ }
29416
+ if (ext.line === false || x < 0 || x > dataW) return;
28683
29417
  ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
28684
29418
  ctx.textBaseline = "middle";
28685
- ctx.strokeStyle = cs.color ?? theme.textColor;
28686
- ctx.lineWidth = cs.width;
28687
- ctx.globalAlpha = cs.opacity * 0.55;
28688
- setDash3(ctx, cs.style);
29419
+ ctx.strokeStyle = ov?.color ?? cs.color ?? theme.textColor;
29420
+ ctx.lineWidth = ov?.width ?? cs.width;
29421
+ ctx.globalAlpha = (ov?.opacity ?? cs.opacity) * 0.55;
29422
+ setDash3(ctx, ov?.style ?? cs.style);
28689
29423
  ctx.beginPath();
28690
29424
  ctx.moveTo(x, 0);
28691
29425
  ctx.lineTo(x, dataH);
28692
- if (ext.y != null) {
29426
+ if (ext.y != null && ov?.horizontal !== false) {
28693
29427
  ctx.moveTo(0, Math.round(ext.y) + 0.5);
28694
29428
  ctx.lineTo(dataW, Math.round(ext.y) + 0.5);
28695
29429
  }
@@ -28698,7 +29432,7 @@ var CrosshairRenderer = class {
28698
29432
  ctx.lineWidth = 1;
28699
29433
  ctx.globalAlpha = 0.8;
28700
29434
  const chipBg = cs.labelBackground ?? theme.borderColor;
28701
- if (ext.y != null && ext.price != null) {
29435
+ if (ext.y != null && ext.price != null && ov?.horizontal !== false) {
28702
29436
  let pane;
28703
29437
  for (const p of scene.panes.values()) {
28704
29438
  if (ext.y >= p.bounds.top && ext.y <= p.bounds.top + p.bounds.height) {
@@ -28738,6 +29472,9 @@ var CrosshairRenderer = class {
28738
29472
  ctx.textAlign = "start";
28739
29473
  }
28740
29474
  };
29475
+ function crisp(px, width) {
29476
+ return Math.round(px) + (Math.round(width) % 2 === 1 ? 0.5 : 0);
29477
+ }
28741
29478
  function setDash3(ctx, style) {
28742
29479
  if (style === "dashed") ctx.setLineDash([6, 4]);
28743
29480
  else if (style === "dotted") ctx.setLineDash([2, 3]);
@@ -36176,7 +36913,7 @@ var NativeRenderer = class {
36176
36913
  this.moveIndicatorCbs = /* @__PURE__ */ new Set();
36177
36914
  this.priceStyleCbs = /* @__PURE__ */ new Set();
36178
36915
  this.name = "native";
36179
- 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"];
36916
+ 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"];
36180
36917
  /** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
36181
36918
  * so moving onto the button doesn't count as leaving). */
36182
36919
  this.onScrollProximityMove = (e) => {
@@ -36355,6 +37092,9 @@ var NativeRenderer = class {
36355
37092
  this.scene.marks = mergeMarksState(this.scene.marks, value);
36356
37093
  this.markPopover?.close();
36357
37094
  break;
37095
+ case "crosshairOverride":
37096
+ this.scene.crosshairOverride = sanitizeCrosshairOverride(value);
37097
+ break;
36358
37098
  case "keyboard":
36359
37099
  this.setKeyboardEnabled(Boolean(value));
36360
37100
  return;
@@ -36468,6 +37208,8 @@ var NativeRenderer = class {
36468
37208
  return { ...this.scene.tradeMarkers, colors: { ...this.scene.tradeMarkers.colors } };
36469
37209
  case "marks":
36470
37210
  return { visible: this.scene.marks.visible, groups: { ...this.scene.marks.groups } };
37211
+ case "crosshairOverride":
37212
+ return this.scene.crosshairOverride ? { ...this.scene.crosshairOverride } : null;
36471
37213
  case "keyboard":
36472
37214
  return this.keyboardEnabled;
36473
37215
  case "historyChords":
@@ -37046,8 +37788,8 @@ var NativeRenderer = class {
37046
37788
  for (const pane of this.scene.panes.values()) pane.scale = { ...pane.scaleTarget };
37047
37789
  this.modelAlpha = 0;
37048
37790
  const start = performance.now();
37049
- const step = (now) => {
37050
- const p = Math.min(1, (now - start) / duration);
37791
+ const step = (now2) => {
37792
+ const p = Math.min(1, (now2 - start) / duration);
37051
37793
  this.scene.bars = p >= 1 ? real : real.map((b, i) => this.revealCandle(b, i, p, n, style));
37052
37794
  this.paintData();
37053
37795
  if (p < 1) {
@@ -37063,8 +37805,8 @@ var NativeRenderer = class {
37063
37805
  fadeInModels() {
37064
37806
  const fade = Math.min(INTRO_MODEL_FADE_MS, this.intro.duration || INTRO_MODEL_FADE_MS);
37065
37807
  const start = performance.now();
37066
- const step = (now) => {
37067
- this.modelAlpha = Math.min(1, (now - start) / fade);
37808
+ const step = (now2) => {
37809
+ this.modelAlpha = Math.min(1, (now2 - start) / fade);
37068
37810
  this.paintData();
37069
37811
  if (this.modelAlpha < 1) {
37070
37812
  this.introRaf = requestAnimationFrame(step);
@@ -37587,10 +38329,13 @@ var NativeRenderer = class {
37587
38329
  this.animator.start();
37588
38330
  }
37589
38331
  } else if (!last || bar.time > last.time) {
38332
+ const vp = this.coords.getViewport();
38333
+ const holdView = n > 0 && vp.rightOffset < 0 && this.scrollTargetRO === null;
37590
38334
  this.bars.push(bar);
37591
38335
  this.syncLiveEase(bar);
37592
38336
  if (this.coords.barInterval > 0) this.coords.appendBar(bar.time);
37593
38337
  else this.coords.setBars(this.bars.map((b) => b.time));
38338
+ if (holdView) this.coords.setViewport({ ...vp, rightOffset: vp.rightOffset - 1 });
37594
38339
  } else {
37595
38340
  return;
37596
38341
  }
@@ -38272,10 +39017,10 @@ var NativeRenderer = class {
38272
39017
  if (this.markPulseRaf !== null || typeof requestAnimationFrame !== "function") return;
38273
39018
  const tick = () => {
38274
39019
  this.markPulseRaf = null;
38275
- const now = frameNow();
38276
- if (this.scene.marksFlash && this.scene.marksFlash.until <= now) this.scene.marksFlash = null;
39020
+ const now2 = frameNow();
39021
+ if (this.scene.marksFlash && this.scene.marksFlash.until <= now2) this.scene.marksFlash = null;
38277
39022
  this.scheduler?.invalidate(2 /* Chrome */);
38278
- const pulsing = this.scene.marksHoverKey !== null && now - this.scene.marksHoverSince < MARK_PULSE_MS;
39023
+ const pulsing = this.scene.marksHoverKey !== null && now2 - this.scene.marksHoverSince < MARK_PULSE_MS;
38279
39024
  if (pulsing || this.scene.marksFlash !== null) this.markPulseRaf = requestAnimationFrame(tick);
38280
39025
  };
38281
39026
  this.markPulseRaf = requestAnimationFrame(tick);
@@ -38836,9 +39581,10 @@ var NativeRenderer = class {
38836
39581
  const ext = this.externalCross;
38837
39582
  if (!ext || this.coords.barCount === 0) return null;
38838
39583
  const logical = Math.floor(this.coords.timeToLogical(ext.time));
38839
- if (logical < 0 || logical >= this.coords.barCount) return null;
38840
- const x = this.coords.logicalToX(logical);
38841
- if (!Number.isFinite(x) || x < 0 || x > this.coords.width) return null;
39584
+ if (!(logical < this.coords.barCount)) return null;
39585
+ const x = this.coords.logicalToX(Math.max(logical, -1));
39586
+ if (!Number.isFinite(x)) return null;
39587
+ const line = logical >= 0 && x >= 0 && x <= this.coords.width;
38842
39588
  let y = null;
38843
39589
  if (ext.price != null) {
38844
39590
  const pricePane = this.scene.orderedPanes().find((p) => p.kind === "price");
@@ -38847,7 +39593,7 @@ var NativeRenderer = class {
38847
39593
  if (!Number.isFinite(y) || y < pricePane.bounds.top || y > pricePane.bounds.top + pricePane.bounds.height) y = null;
38848
39594
  }
38849
39595
  }
38850
- return { x, y, time: this.coords.logicalToTime(logical), price: y != null ? ext.price : null };
39596
+ return { x, y, time: this.coords.logicalToTime(Math.max(logical, 0)), price: y != null ? ext.price : null, line };
38851
39597
  }
38852
39598
  /** Set the sticky magnet mode (called by the drawings toolbar's 3-state button). */
38853
39599
  setSnapMode(mode) {
@@ -43523,6 +44269,7 @@ var Vela = class {
43523
44269
  this.panesControl = new PanesControl(this.orchestrator);
43524
44270
  this.drawingsControl = new DrawingsControl(this.orchestrator.drawings);
43525
44271
  this.marksControl = new MarksControl(this.orchestrator.marks);
44272
+ this.replayControl = new ReplayControl(this.orchestrator);
43526
44273
  }
43527
44274
  /**
43528
44275
  * Register a scripting engine so `addIndicator({ language })` can run that
@@ -43762,6 +44509,16 @@ var Vela = class {
43762
44509
  get marks() {
43763
44510
  return this.marksControl;
43764
44511
  }
44512
+ /**
44513
+ * The chart's bar-replay control surface: rewind to a past bar and reveal the
44514
+ * following ones by hand or on a timer, from the history already loaded —
44515
+ * `await chart.replay.start({ from })`, `chart.replay.play(2000)`, `chart.replay.step()`,
44516
+ * `chart.replay.stop()`. Revealed bars behave like live bars for indicators; live
44517
+ * updates pause meanwhile. Follow it with the `replay:*` events.
44518
+ */
44519
+ get replay() {
44520
+ return this.replayControl;
44521
+ }
43765
44522
  on(event, handler) {
43766
44523
  return this.orchestrator.events.on(event, handler);
43767
44524
  }
@@ -43957,9 +44714,15 @@ var CSS21 = `
43957
44714
  }
43958
44715
  .vela-statusline .vela-sl-symbol { font-weight: 600; font-size: var(--vela-font-size-lg); }
43959
44716
  .vela-statusline .vela-sl-meta { color: var(--vela-fg-muted); font-size: var(--vela-font-size-md); font-weight: 600; }
44717
+ /* The meta opens with "\xB7 " \u2014 sit its dot one space-width after the ticker, as far as the
44718
+ * venue sits after it, not a full row gap away. */
44719
+ .vela-statusline .vela-sl-symbol + .vela-sl-meta { margin-left: calc(var(--vela-space-1) - var(--vela-space-2)); }
43960
44720
  /* Market status badge \u2014 a kit callout bubble (icon-only 16px circle, label on hover
43961
- * via the kit tooltip); the session tint is applied per status in setMarketStatus. */
43962
- .vela-statusline .vela-sl-market { align-self: center; }
44721
+ * via the kit tooltip); the session tint is applied per status in setMarketStatus. While
44722
+ * the chart replays past bars it wears the replay badge instead (the inverse chip). */
44723
+ .vela-statusline .vela-sl-market { align-self: center; display: inline-flex; }
44724
+ .vela-statusline .vela-sl-market > [hidden] { display: none !important; }
44725
+ .vela-statusline .vela-sl-replay-badge, .vela-statusline .vela-sl-replay-badge svg { display: block; width: 16px; height: 16px; }
43963
44726
  .vela-statusline .vela-sl-ohlc { display: flex; gap: var(--vela-space-1); color: var(--vela-fg-muted); }
43964
44727
  .vela-statusline .vela-sl-ohlc b { color: var(--vela-fg); font-weight: 500; }
43965
44728
  /* The change value wears the SAME ink as the OHLC values (set inline per render) \u2014
@@ -44012,6 +44775,8 @@ var MARKET_LABELS = {
44012
44775
  closed: "Market Closed",
44013
44776
  holiday: "Market Holiday"
44014
44777
  };
44778
+ var REPLAY_LABEL = "Replay Mode";
44779
+ var REPLAY_BADGE_SVG = '<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><circle cx="8" cy="8" r="8" style="fill: var(--vela-selected-bg)"/><path d="M11.7 4.75v6.5L7.2 8zM7.2 4.75v6.5L2.7 8z" style="fill: var(--vela-selected-fg); stroke: var(--vela-selected-fg); stroke-width: 0.8; stroke-linejoin: round"/></svg>';
44015
44780
  var MARKET_INKS = {
44016
44781
  open: "var(--vela-up)",
44017
44782
  pre: SESSION_PRE,
@@ -44050,6 +44815,9 @@ var Statusline = class {
44050
44815
  this.host = host;
44051
44816
  this.iconFor = iconFor;
44052
44817
  this.parts = { logo: true, name: true, market: true, ohlc: true, change: true };
44818
+ this.marketStatus = "open";
44819
+ /** The chart replays past bars — the badge shows the replay mode (see {@link setReplaying}). */
44820
+ this.replaying = false;
44053
44821
  /** The right-click action menu — present once a host wires it via {@link attachMenu}. */
44054
44822
  this.menu = null;
44055
44823
  this.menuHooks = null;
@@ -44101,8 +44869,13 @@ var Statusline = class {
44101
44869
  label: MARKET_LABELS.open,
44102
44870
  host
44103
44871
  });
44104
- this.marketEl = this.marketBubble.el;
44105
- this.marketEl.classList.add("vela-sl-market");
44872
+ this.replayBadge = doc.createElement("span");
44873
+ this.replayBadge.className = "vela-sl-replay-badge";
44874
+ this.replayBadge.innerHTML = REPLAY_BADGE_SVG;
44875
+ this.replayBadge.hidden = true;
44876
+ this.marketEl = doc.createElement("span");
44877
+ this.marketEl.className = "vela-sl-market";
44878
+ this.marketEl.append(this.marketBubble.el, this.replayBadge);
44106
44879
  this.ohlcEl = doc.createElement("span");
44107
44880
  this.ohlcEl.className = "vela-sl-ohlc";
44108
44881
  this.changeEl = doc.createElement("span");
@@ -44224,6 +44997,25 @@ var Statusline = class {
44224
44997
  /** Dress the market badge for a session state: its icon, tinted circle, and the
44225
44998
  * hover label. Callers with no session model leave the constructor's 'open'. */
44226
44999
  setMarketStatus(status) {
45000
+ this.marketStatus = status;
45001
+ this.dressBadge();
45002
+ }
45003
+ /** The chart entered or left a bar replay: the badge shows the replay mode meanwhile
45004
+ * (the session state of a past bar says nothing about the market now). */
45005
+ setReplaying(replaying) {
45006
+ if (replaying === this.replaying) return;
45007
+ this.replaying = replaying;
45008
+ this.dressBadge();
45009
+ }
45010
+ dressBadge() {
45011
+ this.marketBubble.el.hidden = this.replaying;
45012
+ this.replayBadge.hidden = !this.replaying;
45013
+ if (this.replaying) {
45014
+ this.marketEl.dataset.status = "replay";
45015
+ this.marketTip.setContent(REPLAY_LABEL);
45016
+ return;
45017
+ }
45018
+ const status = this.marketStatus;
44227
45019
  this.marketEl.dataset.status = status;
44228
45020
  const ink = MARKET_INKS[status];
44229
45021
  this.marketBubble.set({
@@ -44296,11 +45088,23 @@ var Statusline = class {
44296
45088
  this.lastBar = b;
44297
45089
  if (!this.hoverBar) this.render();
44298
45090
  }),
45091
+ // A replay cut or restore replaces the newest bar without a `bar` event.
45092
+ chart.on("replay:start", () => {
45093
+ this.lastBar = null;
45094
+ this.setReplaying(true);
45095
+ this.render();
45096
+ }),
45097
+ chart.on("replay:end", () => {
45098
+ this.lastBar = null;
45099
+ this.setReplaying(false);
45100
+ this.render();
45101
+ }),
44299
45102
  chart.renderer.onCrosshairMove((e) => {
44300
45103
  this.hoverBar = e.ohlc;
44301
45104
  this.render();
44302
45105
  })
44303
45106
  );
45107
+ this.setReplaying(chart.replay.state.active);
44304
45108
  this.render();
44305
45109
  }
44306
45110
  destroy() {
@@ -44541,26 +45345,26 @@ function isWeekday(ms, tz) {
44541
45345
  return false;
44542
45346
  }
44543
45347
  }
44544
- function deriveMarketStatus(now, w, tz, overnight = false) {
44545
- const within = (ws) => ws.find(([s, e]) => now >= s && now < e);
45348
+ function deriveMarketStatus(now2, w, tz, overnight = false) {
45349
+ const within = (ws) => ws.find(([s, e]) => now2 >= s && now2 < e);
44546
45350
  if (within(w.regular)) return "open";
44547
45351
  const ext = within(w.extended);
44548
45352
  if (ext) {
44549
45353
  if (overnight) return "extended";
44550
- return w.regular.some(([s]) => s >= now && s < ext[1]) ? "pre" : "post";
45354
+ return w.regular.some(([s]) => s >= now2 && s < ext[1]) ? "pre" : "post";
44551
45355
  }
44552
- if (isWeekday(now, tz)) {
44553
- const today = civilDate(now, tz);
45356
+ if (isWeekday(now2, tz)) {
45357
+ const today = civilDate(now2, tz);
44554
45358
  if (today !== "" && !w.extended.some(([s]) => civilDate(s, tz) === today)) return "holiday";
44555
45359
  }
44556
45360
  return "closed";
44557
45361
  }
44558
- function nextStatusBoundary(now, w) {
45362
+ function nextStatusBoundary(now2, w) {
44559
45363
  let next = null;
44560
45364
  for (const ws of [w.regular, w.extended]) {
44561
45365
  for (const [s, e] of ws) {
44562
45366
  for (const t of [s, e]) {
44563
- if (t > now && (next == null || t < next)) next = t;
45367
+ if (t > now2 && (next == null || t < next)) next = t;
44564
45368
  }
44565
45369
  }
44566
45370
  }
@@ -44599,8 +45403,8 @@ var MarketStatusTracker = class {
44599
45403
  return;
44600
45404
  }
44601
45405
  const tz = typeof si?.timezone === "string" && si.timezone !== "" ? si.timezone : "Etc/UTC";
44602
- const now = Date.now();
44603
- const range = { from: now - FETCH_BACK_MS, to: now + FETCH_AHEAD_MS };
45406
+ const now2 = Date.now();
45407
+ const range = { from: now2 - FETCH_BACK_MS, to: now2 + FETCH_AHEAD_MS };
44604
45408
  const [regular, extended] = await Promise.all([
44605
45409
  provider.getCalendar(resolved.ticker, { ...range, session: "regular" }).catch(() => null),
44606
45410
  provider.getCalendar(resolved.ticker, { ...range, session: "extended" }).catch(() => null)
@@ -44612,9 +45416,9 @@ var MarketStatusTracker = class {
44612
45416
  }
44613
45417
  const w = { regular, extended };
44614
45418
  const overnight = parseSessionSpec(si)?.overnight === true;
44615
- this.onStatus(deriveMarketStatus(now, w, tz, overnight));
44616
- const boundary = nextStatusBoundary(now, w);
44617
- const delay = Math.min(boundary != null ? boundary - now : MAX_TIMER_MS, MAX_TIMER_MS);
45419
+ this.onStatus(deriveMarketStatus(now2, w, tz, overnight));
45420
+ const boundary = nextStatusBoundary(now2, w);
45421
+ const delay = Math.min(boundary != null ? boundary - now2 : MAX_TIMER_MS, MAX_TIMER_MS);
44618
45422
  this.arm(my, data, symbol, Math.max(delay, MIN_TIMER_MS));
44619
45423
  }
44620
45424
  arm(my, data, symbol, delay) {
@@ -44647,6 +45451,7 @@ var CSS22 = `
44647
45451
  left: var(--vela-toolbar-gutter, 0px);
44648
45452
  right: var(--vela-scale-gutter, 0px);
44649
45453
  display: flex;
45454
+ flex-direction: column;
44650
45455
  align-items: center;
44651
45456
  justify-content: center;
44652
45457
  overflow: hidden;
@@ -44660,6 +45465,14 @@ var CSS22 = `
44660
45465
  user-select: none;
44661
45466
  white-space: nowrap;
44662
45467
  }
45468
+ .vela-watermark-replay {
45469
+ display: inline-flex;
45470
+ align-items: center;
45471
+ gap: 0.25em;
45472
+ margin-top: 0.1em;
45473
+ font-size: 0.8em;
45474
+ }
45475
+ .vela-watermark [hidden] { display: none !important; }
44663
45476
  `;
44664
45477
  function watermarkFontPx(availPx, textPxAtMax) {
44665
45478
  if (textPxAtMax <= 0) return MAX_FONT_PX;
@@ -44670,6 +45483,10 @@ var Watermark = class {
44670
45483
  this.resizeObserver = null;
44671
45484
  /** The host's visibility preference (the persisted watermark toggle). */
44672
45485
  this.shown = true;
45486
+ /** The host's preference for the replay line (its own persisted toggle). */
45487
+ this.replayShown = true;
45488
+ /** The chart is replaying past bars. */
45489
+ this.replaying = false;
44673
45490
  /** A bar load is in flight with nothing painted — the loading affordance owns the
44674
45491
  * canvas, so the mark stays out of its way. Starts true: the FIRST `load:start`
44675
45492
  * fires during chart construction, before any subscriber can see it. */
@@ -44679,7 +45496,10 @@ var Watermark = class {
44679
45496
  this.el.className = "vela-watermark";
44680
45497
  this.el.dataset.velaScreenshot = "under";
44681
45498
  this.text = host.ownerDocument.createElement("span");
44682
- this.el.appendChild(this.text);
45499
+ this.replayLine = host.ownerDocument.createElement("span");
45500
+ this.replayLine.className = "vela-watermark-replay";
45501
+ this.replayLine.append(iconEl("replay", host.ownerDocument), host.ownerDocument.createTextNode("Replay"));
45502
+ this.el.append(this.text, this.replayLine);
44683
45503
  host.appendChild(this.el);
44684
45504
  if (typeof ResizeObserver !== "undefined") {
44685
45505
  this.resizeObserver = new ResizeObserver(() => this.fit());
@@ -44692,13 +45512,26 @@ var Watermark = class {
44692
45512
  this.shown = visible;
44693
45513
  this.sync();
44694
45514
  }
45515
+ /** Show/hide the replay line (the host's toggle); it only ever shows while replaying. */
45516
+ setReplayVisible(visible) {
45517
+ this.replayShown = visible;
45518
+ this.sync();
45519
+ }
45520
+ /** The chart entered or left a bar replay. */
45521
+ setReplaying(replaying) {
45522
+ this.replaying = replaying;
45523
+ this.sync();
45524
+ }
44695
45525
  /** Loading and the watermark never share the canvas — hidden while a load is up. */
44696
45526
  setLoading(loading) {
44697
45527
  this.loading = loading;
44698
45528
  this.sync();
44699
45529
  }
44700
45530
  sync() {
44701
- this.el.style.display = this.shown && !this.loading ? "" : "none";
45531
+ const replay = this.replayShown && this.replaying;
45532
+ this.text.hidden = !this.shown;
45533
+ this.replayLine.hidden = !replay;
45534
+ this.el.style.display = (this.shown || replay) && !this.loading ? "" : "none";
44702
45535
  }
44703
45536
  update(symbol, timeframe) {
44704
45537
  this.text.textContent = symbol ? `${parseSymbol(symbol).ticker} \xB7 ${timeframeLabel(timeframe)}` : "";
@@ -45287,8 +46120,12 @@ var ChartCell = class {
45287
46120
  this.indicatorValuesOn = seed.indicatorValues ?? true;
45288
46121
  if (!this.indicatorValuesOn) this.inner.renderer.set("indicatorValues", false);
45289
46122
  this.watermarkOn = seed.watermark ?? deps.watermark;
46123
+ this.replayWatermarkOn = seed.replayWatermark ?? true;
45290
46124
  this.watermark = deps.watermark ? new Watermark(this.host, symbol ?? "", seed.timeframe ?? "60") : null;
45291
46125
  if (!this.watermarkOn) this.watermark?.setVisible(false);
46126
+ if (!this.replayWatermarkOn) this.watermark?.setReplayVisible(false);
46127
+ this.inner.on("replay:start", () => this.watermark?.setReplaying(true));
46128
+ this.inner.on("replay:end", () => this.watermark?.setReplaying(false));
45292
46129
  this.statusline = deps.statusline ? new Statusline(this.host, symbol ?? "", (sym) => this.inner?.data.symbolIcon(sym)) : null;
45293
46130
  this.statusline?.setMeta(seed.timeframe ?? "60", this.state.provider ?? "");
45294
46131
  this.statusline?.onChart(this.inner);
@@ -45483,10 +46320,10 @@ var ChartCell = class {
45483
46320
  const chart = this.inner;
45484
46321
  const symbol = this.state.symbol;
45485
46322
  if (!chart || !symbol) return;
45486
- const now = Date.now();
46323
+ const now2 = Date.now();
45487
46324
  const requestedSpan = Math.max(this.state.bars ?? 1e3, this.rangeBars) * timeframeMs(this.state.timeframe ?? "60");
45488
46325
  const fallbackSpan = Number.isFinite(requestedSpan) ? Math.max(3 * 864e5, requestedSpan) : 3 * 864e5;
45489
- const range = chart.getVisibleRange() ?? { from: now - fallbackSpan, to: now };
46326
+ const range = chart.getVisibleRange() ?? { from: now2 - fallbackSpan, to: now2 };
45490
46327
  this.sessionShading.track(chart.data, symbol, { session: this.session, timeframe: this.timeframe, range });
45491
46328
  }
45492
46329
  /** The session-shade colors live in the renderer CONFIG (persisted with it, edited
@@ -45602,6 +46439,13 @@ var ChartCell = class {
45602
46439
  id: "visible",
45603
46440
  get: () => this.watermarkOn,
45604
46441
  set: (v) => this.setWatermarkVisible(v)
46442
+ },
46443
+ {
46444
+ kind: "toggle",
46445
+ label: "Replay watermark",
46446
+ id: "replay",
46447
+ get: () => this.replayWatermarkOn,
46448
+ set: (v) => this.setReplayWatermarkVisible(v)
45605
46449
  }
45606
46450
  ]
45607
46451
  };
@@ -45670,6 +46514,12 @@ var ChartCell = class {
45670
46514
  this.watermark?.setVisible(visible);
45671
46515
  this.deps.onStateDirty();
45672
46516
  }
46517
+ /** Show/hide the "Replay" line under this cell's watermark while it replays (persisted per cell). */
46518
+ setReplayWatermarkVisible(visible) {
46519
+ this.replayWatermarkOn = visible;
46520
+ this.watermark?.setReplayVisible(visible);
46521
+ this.deps.onStateDirty();
46522
+ }
45673
46523
  /** Show/hide this cell's indicator titles — the in-chart legend rows (persisted per cell). */
45674
46524
  setIndicatorTitlesVisible(visible) {
45675
46525
  this.indicatorTitlesOn = visible;
@@ -45910,7 +46760,7 @@ var ChartCell = class {
45910
46760
  * then the manifest in the host's own order. */
45911
46761
  libraryRows() {
45912
46762
  return [
45913
- ...this.supportedNatives().map((n) => ({ name: n.title, category: "Vela", native: true, nativeType: n.type, beta: n.beta })),
46763
+ ...this.supportedNatives().map((n) => ({ name: n.title, category: "Built-in", native: true, nativeType: n.type, beta: n.beta })),
45914
46764
  ...this.manifest.map((e) => ({ name: e.name, language: e.language, category: e.category }))
45915
46765
  ];
45916
46766
  }
@@ -46143,6 +46993,7 @@ var ChartCell = class {
46143
46993
  if (!this.inner || this.destroyed) return;
46144
46994
  if (cs.priceStyle && cs.priceStyle !== this.priceStyle) this.setPriceStyle(cs.priceStyle);
46145
46995
  if (cs.watermark !== void 0 && cs.watermark !== this.watermarkOn) this.setWatermarkVisible(cs.watermark);
46996
+ if (cs.replayWatermark !== void 0 && cs.replayWatermark !== this.replayWatermarkOn) this.setReplayWatermarkVisible(cs.replayWatermark);
46146
46997
  if (cs.indicatorTitles !== void 0 && cs.indicatorTitles !== this.indicatorTitlesOn) this.setIndicatorTitlesVisible(cs.indicatorTitles);
46147
46998
  if (cs.indicatorValues !== void 0 && cs.indicatorValues !== this.indicatorValuesOn) this.setIndicatorValuesVisible(cs.indicatorValues);
46148
46999
  if (cs.rendererConfig != null) this.inner.renderer.applyConfig(cs.rendererConfig);
@@ -46177,6 +47028,7 @@ var ChartCell = class {
46177
47028
  ...live ? { symbol: live.symbol, provider: live.provider, timeframe: live.timeframe } : {},
46178
47029
  priceStyle: this.priceStyle,
46179
47030
  watermark: this.watermarkOn,
47031
+ replayWatermark: this.replayWatermarkOn,
46180
47032
  indicatorTitles: this.indicatorTitlesOn,
46181
47033
  indicatorValues: this.indicatorValuesOn,
46182
47034
  rendererConfig: this.inner?.renderer.getConfig() ?? void 0,
@@ -46241,6 +47093,7 @@ function buildContext(host) {
46241
47093
  setPriceStyle: (style) => host.active()?.setPriceStyle(style),
46242
47094
  openSymbolSearch: (query) => host.openSymbolSearch(query),
46243
47095
  togglePanel: (id, open2) => host.togglePanel(id, open2),
47096
+ dockStrip: (el) => host.dockStrip(el),
46244
47097
  host: host.root,
46245
47098
  toast: (message, kind) => host.toast(message, kind),
46246
47099
  addIndicator: (entry) => host.active()?.addExternalIndicator(entry),
@@ -46252,10 +47105,303 @@ function buildContext(host) {
46252
47105
  get activeCellId() {
46253
47106
  return host.active()?.id ?? "";
46254
47107
  },
46255
- setActiveCell: (id) => host.setActiveCell(id)
47108
+ setActiveCell: (id) => host.setActiveCell(id),
47109
+ replay: host.replay
46256
47110
  };
46257
47111
  }
46258
47112
 
47113
+ // src/workspace/WorkspaceReplay.ts
47114
+ var MAX_BARS_PER_STEP = 1e5;
47115
+ var WorkspaceReplay = class {
47116
+ constructor(host) {
47117
+ this.host = host;
47118
+ this.bus = new TypedEventBus();
47119
+ /** The running session: the shared replay time (every chart shows the bars closed by it). */
47120
+ this.session = null;
47121
+ this.playing = false;
47122
+ this.intervalMs = 1e3;
47123
+ this.timer = null;
47124
+ /** Cells whose replay a symbol switch ended — restarted at the shared time once their bars land. */
47125
+ this.rejoining = /* @__PURE__ */ new Set();
47126
+ this.cellSubs = /* @__PURE__ */ new Map();
47127
+ /** Inside a group step: chart events are the controller's own doing. */
47128
+ this.stepping = false;
47129
+ /** Inside `finish`: the charts' `replay:end` events are the controller's own doing. */
47130
+ this.stopping = false;
47131
+ /** Driving a chart's own timer: its play/pause events are not the user's. */
47132
+ this.muted = 0;
47133
+ /** A chart finished during a group step — the session ends right after it. */
47134
+ this.finishedDuringStep = false;
47135
+ for (const cell of host.cells()) this.watch(cell);
47136
+ this.offHost = host.onCells((e) => {
47137
+ if (e.kind === "created") this.onCellCreated(e.id);
47138
+ else if (e.kind === "destroyed") this.onCellDestroyed(e.id);
47139
+ });
47140
+ }
47141
+ /**
47142
+ * Enter replay on every cell (or seek, when already replaying) — paused. Resolves once
47143
+ * every chart shows its rewound history (a chart whose history does not reach that far
47144
+ * loads older bars first). A chart with nothing to replay after the shared time stays
47145
+ * as it is.
47146
+ */
47147
+ async start(opts) {
47148
+ const cells = this.host.cells();
47149
+ const ref = cells.find((c) => c.id === (opts.cell ?? this.host.activeId())) ?? cells[0];
47150
+ if (!ref) return;
47151
+ if (this.playing) this.pause();
47152
+ this.rejoining.clear();
47153
+ await ref.chart.replay.start({ from: opts.from });
47154
+ const cursor = ref.chart.replay.state.cursorTime;
47155
+ if (cursor == null) return;
47156
+ const session = { clock: barClose(cursor, ref.chart.market.timeframe) };
47157
+ this.session = session;
47158
+ await Promise.all(this.host.cells().filter((c) => c.id !== ref.id).map((c) => this.startCell(c, session.clock)));
47159
+ if (this.session !== session) return;
47160
+ this.syncPlayback();
47161
+ const s = this.activeChart()?.replay.state;
47162
+ this.bus.emit("replay:start", { cursorTime: s?.cursorTime ?? cursor, remaining: s?.remaining ?? 0 });
47163
+ }
47164
+ /** Reveal the next bar(s): the clock moves to the next bar close on any chart. Returns false when nothing is left. */
47165
+ step() {
47166
+ const solo = this.solo();
47167
+ return solo ? solo.replay.step() : this.stepGroup();
47168
+ }
47169
+ /** Reveal the next update — in a single-chart layout, the chart's `stepUpdate()` (tick replay); otherwise {@link step}. */
47170
+ stepUpdate() {
47171
+ const solo = this.solo();
47172
+ return solo ? solo.replay.stepUpdate() : this.stepGroup();
47173
+ }
47174
+ /** Advance one update every `intervalMs` (default: the last pace, initially 1000). Calling it again while playing changes the pace. */
47175
+ play(intervalMs2) {
47176
+ if (!this.session) return this;
47177
+ if (intervalMs2 !== void 0) {
47178
+ if (!(Number.isFinite(intervalMs2) && intervalMs2 > 0)) {
47179
+ console.warn(`[vela] workspace.replay.play(${intervalMs2}) ignored \u2014 the interval must be a positive number of ms`);
47180
+ return this;
47181
+ }
47182
+ this.intervalMs = intervalMs2;
47183
+ }
47184
+ this.playing = true;
47185
+ this.clearTimer();
47186
+ this.syncPlayback();
47187
+ this.bus.emit("replay:play", { intervalMs: this.intervalMs });
47188
+ return this;
47189
+ }
47190
+ pause() {
47191
+ if (!this.playing) return this;
47192
+ this.playing = false;
47193
+ this.syncPlayback();
47194
+ this.bus.emit("replay:pause", void 0);
47195
+ return this;
47196
+ }
47197
+ /** Leave replay on every chart: full history back, live updates resumed. */
47198
+ stop() {
47199
+ this.finish("stopped");
47200
+ return this;
47201
+ }
47202
+ /** `active`/`playing`/`intervalMs` for the whole workspace; the cursor fields read the ACTIVE cell's chart. */
47203
+ get state() {
47204
+ const s = this.session ? this.activeChart()?.replay.state : void 0;
47205
+ return {
47206
+ active: this.session !== null,
47207
+ playing: this.playing,
47208
+ cursorTime: s?.cursorTime ?? null,
47209
+ remaining: s?.remaining ?? 0,
47210
+ nextTime: s?.nextTime ?? null,
47211
+ intervalMs: this.intervalMs
47212
+ };
47213
+ }
47214
+ /** The active cell's replayable history (`chart.replay.bounds`). */
47215
+ get bounds() {
47216
+ return this.activeChart()?.replay.bounds ?? null;
47217
+ }
47218
+ on(event, handler) {
47219
+ return this.bus.on(event, handler);
47220
+ }
47221
+ destroy() {
47222
+ this.session = null;
47223
+ this.playing = false;
47224
+ this.clearTimer();
47225
+ this.offHost();
47226
+ for (const off of this.cellSubs.values()) off();
47227
+ this.cellSubs.clear();
47228
+ this.bus.clear();
47229
+ }
47230
+ // ── internals ──
47231
+ /** The one chart of a single-chart layout — it replays on its own, tick replay included. */
47232
+ solo() {
47233
+ const cells = this.host.cells();
47234
+ return cells.length === 1 ? cells[0].chart : null;
47235
+ }
47236
+ activeChart() {
47237
+ const cells = this.host.cells();
47238
+ return (cells.find((c) => c.id === this.host.activeId()) ?? cells[0])?.chart ?? null;
47239
+ }
47240
+ /** Rewind one chart to the shared time: every bar that closed by `clock`, none after. */
47241
+ async startCell(cell, clock) {
47242
+ const replay = cell.chart.replay;
47243
+ const from = lastOpenClosedBy(clock, cell.chart.market.timeframe);
47244
+ const bounds = replay.bounds;
47245
+ if (!bounds || bounds.last <= from) return;
47246
+ await replay.start({ from });
47247
+ }
47248
+ /** Move the clock to the next bar close on any chart; each chart reveals what closed by then. */
47249
+ stepGroup() {
47250
+ const session = this.session;
47251
+ if (!session) return false;
47252
+ const cells = this.host.cells();
47253
+ let next = Infinity;
47254
+ for (const c of cells) {
47255
+ const t = c.chart.replay.state.nextTime;
47256
+ if (t != null) next = Math.min(next, barClose(t, c.chart.market.timeframe));
47257
+ }
47258
+ if (!Number.isFinite(next)) return false;
47259
+ this.stepping = true;
47260
+ try {
47261
+ for (const c of cells) {
47262
+ const tf = c.chart.market.timeframe;
47263
+ for (let n = 0; n < MAX_BARS_PER_STEP; n += 1) {
47264
+ const t = c.chart.replay.state.nextTime;
47265
+ if (t == null || barClose(t, tf) > next) break;
47266
+ if (!c.chart.replay.step()) break;
47267
+ }
47268
+ }
47269
+ } finally {
47270
+ this.stepping = false;
47271
+ }
47272
+ session.clock = next;
47273
+ if (this.finishedDuringStep) {
47274
+ this.finishedDuringStep = false;
47275
+ this.finish("finished");
47276
+ return true;
47277
+ }
47278
+ const s = this.activeChart()?.replay.state;
47279
+ if (s?.cursorTime != null) this.bus.emit("replay:step", { cursorTime: s.cursorTime, remaining: s.remaining });
47280
+ return true;
47281
+ }
47282
+ /** Put the timers in the state `playing` asks for: a single chart runs its own (it paces
47283
+ * ticks), several run on the controller's clock. */
47284
+ syncPlayback() {
47285
+ const solo = this.solo();
47286
+ const run = this.playing && this.session !== null;
47287
+ this.muted += 1;
47288
+ try {
47289
+ for (const c of this.host.cells()) {
47290
+ if (c.chart === solo && run) {
47291
+ if (!c.chart.replay.state.playing || c.chart.replay.state.intervalMs !== this.intervalMs) c.chart.replay.play(this.intervalMs);
47292
+ } else if (c.chart.replay.state.playing) {
47293
+ c.chart.replay.pause();
47294
+ }
47295
+ }
47296
+ } finally {
47297
+ this.muted -= 1;
47298
+ }
47299
+ if (!run || solo) this.clearTimer();
47300
+ else if (this.timer == null) this.schedule();
47301
+ }
47302
+ schedule() {
47303
+ this.timer = setTimeout(() => {
47304
+ this.timer = null;
47305
+ if (!this.playing || !this.session || this.solo()) return;
47306
+ if (this.stepGroup() || this.rejoining.size > 0) {
47307
+ if (this.playing && this.session && this.timer == null) this.schedule();
47308
+ } else {
47309
+ this.pause();
47310
+ }
47311
+ }, this.intervalMs);
47312
+ }
47313
+ clearTimer() {
47314
+ if (this.timer != null) clearTimeout(this.timer);
47315
+ this.timer = null;
47316
+ }
47317
+ /** End the session on every chart (idempotent). */
47318
+ finish(reason) {
47319
+ const had = this.session !== null;
47320
+ this.session = null;
47321
+ this.playing = false;
47322
+ this.clearTimer();
47323
+ this.rejoining.clear();
47324
+ this.stopping = true;
47325
+ try {
47326
+ for (const c of this.host.cells()) c.chart.replay.stop();
47327
+ } finally {
47328
+ this.stopping = false;
47329
+ }
47330
+ if (had) this.bus.emit("replay:end", { reason });
47331
+ }
47332
+ watch(cell) {
47333
+ const { id, chart } = cell;
47334
+ const isActive = () => (this.host.activeId() ?? this.host.cells()[0]?.id) === id;
47335
+ const offs = [
47336
+ chart.on("replay:end", ({ reason }) => {
47337
+ if (!this.session || this.stopping) return;
47338
+ if (reason === "market") this.rejoining.add(id);
47339
+ else if (this.stepping) this.finishedDuringStep || (this.finishedDuringStep = reason === "finished");
47340
+ else this.finish(reason);
47341
+ }),
47342
+ chart.on("market:changed", () => {
47343
+ if (!this.rejoining.delete(id) || !this.session) return;
47344
+ void this.startCell(cell, this.session.clock).then(() => this.syncPlayback());
47345
+ }),
47346
+ chart.on("replay:step", (e) => {
47347
+ if (this.session && !this.stepping && isActive()) this.bus.emit("replay:step", e);
47348
+ }),
47349
+ chart.on("replay:tick", (e) => {
47350
+ if (this.session && isActive()) this.bus.emit("replay:tick", e);
47351
+ }),
47352
+ // A single chart runs its own timer: a pause it takes on its own (a seek) is the session's.
47353
+ chart.on("replay:pause", () => {
47354
+ if (!this.session || this.muted > 0 || !this.playing || chart !== this.solo()) return;
47355
+ this.playing = false;
47356
+ this.bus.emit("replay:pause", void 0);
47357
+ })
47358
+ ];
47359
+ this.cellSubs.set(id, () => {
47360
+ for (const off of offs) off();
47361
+ });
47362
+ }
47363
+ onCellCreated(id) {
47364
+ const cell = this.host.cells().find((c) => c.id === id);
47365
+ if (!cell) return;
47366
+ this.watch(cell);
47367
+ const session = this.session;
47368
+ if (!session) return;
47369
+ this.rejoining.add(id);
47370
+ void cell.chart.ready().then(async () => {
47371
+ if (this.session !== session || !this.rejoining.delete(id)) return;
47372
+ await this.startCell(cell, session.clock);
47373
+ this.syncPlayback();
47374
+ });
47375
+ this.syncPlayback();
47376
+ }
47377
+ onCellDestroyed(id) {
47378
+ this.cellSubs.get(id)?.();
47379
+ this.cellSubs.delete(id);
47380
+ this.rejoining.delete(id);
47381
+ this.syncPlayback();
47382
+ }
47383
+ };
47384
+ function barClose(open2, timeframe) {
47385
+ const months = monthSpan(timeframe);
47386
+ if (months > 0) {
47387
+ const d = new Date(open2);
47388
+ return Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + months, d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes());
47389
+ }
47390
+ return open2 + timeframeToMs(timeframe ?? "60");
47391
+ }
47392
+ function lastOpenClosedBy(time, timeframe) {
47393
+ const months = monthSpan(timeframe);
47394
+ if (months > 0) {
47395
+ const d = new Date(time);
47396
+ return Date.UTC(d.getUTCFullYear(), d.getUTCMonth() - months, d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes());
47397
+ }
47398
+ return time - timeframeToMs(timeframe ?? "60");
47399
+ }
47400
+ function monthSpan(timeframe) {
47401
+ const m = /^(\d*)M$/.exec((timeframe ?? "").trim());
47402
+ return m ? Number(m[1] || 1) : 0;
47403
+ }
47404
+
46259
47405
  // src/workspace/layouts.ts
46260
47406
  var registry5 = /* @__PURE__ */ new Map();
46261
47407
  function registerLayout(def) {
@@ -46560,6 +47706,8 @@ var CSS24 = `
46560
47706
  .vela-workspace { position: relative; width: 100%; height: 100%; display: flex; flex-direction: column; background: var(--vela-bg); }
46561
47707
  .vela-ws-main { position: relative; display: flex; flex-direction: row; flex: 1 1 auto; min-height: 0; }
46562
47708
  .vela-ws-toolbar { position: relative; flex: none; }
47709
+ .vela-ws-strips { position: relative; flex: none; display: flex; flex-direction: column; min-width: 0; }
47710
+ .vela-ws-strips:empty { display: none; }
46563
47711
  .vela-ws-grid { position: relative; flex: 1 1 auto; min-width: 0; display: grid; gap: ${GAP_PX}px; background: var(--vela-border-soft); }
46564
47712
  .vela-cell { background: var(--vela-bg); position: relative; }
46565
47713
  /* Active-cell highlight: an overlay ring ABOVE the chart's own canvas stack (a plain
@@ -46618,11 +47766,11 @@ function resolveTyping(key, open2) {
46618
47766
  if (open2.dialogs > 0) {
46619
47767
  if (key.length !== 1) return null;
46620
47768
  if (open2.symbolSearch) return { target: "symbol", text: key.toUpperCase() };
46621
- if (open2.timeframeEntry) return { target: "timeframe", text: key };
47769
+ if (open2.timeframeEntry) return { target: "timeframe", text: key.toUpperCase() };
46622
47770
  return null;
46623
47771
  }
46624
- if (/^[a-zA-Z]$/.test(key)) return { target: "symbol", text: key.toUpperCase() };
46625
- if (/^[0-9]$/.test(key)) return { target: "timeframe", text: key };
47772
+ if (/^[a-zA-Z0]$/.test(key)) return { target: "symbol", text: key.toUpperCase() };
47773
+ if (/^[1-9]$/.test(key)) return { target: "timeframe", text: key };
46626
47774
  return null;
46627
47775
  }
46628
47776
  var VelaWorkspace = class {
@@ -46731,6 +47879,20 @@ var VelaWorkspace = class {
46731
47879
  const hostEl = typeof container === "string" ? document.querySelector(container) : container;
46732
47880
  if (!hostEl) throw new Error(`VelaWorkspace: container not found: ${String(container)}`);
46733
47881
  this.opts = opts;
47882
+ this.replay = new WorkspaceReplay({
47883
+ cells: () => this.cells(),
47884
+ activeId: () => this.activeId,
47885
+ onCells: (handler) => {
47886
+ const offs = [
47887
+ this.events.on("cell:created", ({ id }) => handler({ kind: "created", id })),
47888
+ this.events.on("cell:destroyed", ({ id }) => handler({ kind: "destroyed", id })),
47889
+ this.events.on("cell:active", ({ id }) => handler({ kind: "active", id }))
47890
+ ];
47891
+ return () => {
47892
+ for (const off of offs) off();
47893
+ };
47894
+ }
47895
+ });
46734
47896
  this.persistKey = opts.persist === void 0 || opts.persist === false ? null : opts.persist === true ? "vela-workspace" : opts.persist;
46735
47897
  this.storage = opts.storage ?? localStorageAdapter();
46736
47898
  let boot = null;
@@ -46868,6 +48030,9 @@ var VelaWorkspace = class {
46868
48030
  this.dock.refresh();
46869
48031
  if (boot?.panels) this.dock.applyState(boot.panels);
46870
48032
  this.root.appendChild(main);
48033
+ this.stripsEl = doc.createElement("div");
48034
+ this.stripsEl.className = "vela-ws-strips";
48035
+ this.root.appendChild(this.stripsEl);
46871
48036
  this.toastHost = new Toast(this.gridEl);
46872
48037
  const attribution = rendererDefaults().attribution;
46873
48038
  if (attribution !== false) {
@@ -47102,9 +48267,16 @@ var VelaWorkspace = class {
47102
48267
  setActiveCell: (id) => this.setActiveCell(id),
47103
48268
  openSymbolSearch: (query) => this.symbolPicker.open(query ?? ""),
47104
48269
  togglePanel: (id, open2) => this.dock.toggle(id, open2),
48270
+ dockStrip: (el) => {
48271
+ this.stripsEl.appendChild(el);
48272
+ return () => {
48273
+ if (el.parentElement === this.stripsEl) el.remove();
48274
+ };
48275
+ },
47105
48276
  root: this.root,
47106
48277
  toast: (message, kind) => this.toastHost.show(message, kind),
47107
- stateDirty: () => this.markStateDirty()
48278
+ stateDirty: () => this.markStateDirty(),
48279
+ replay: this.replay
47108
48280
  });
47109
48281
  }
47110
48282
  /** Re-project contributed topbar actions + side panels, and mount late-registered attachments. */
@@ -47412,6 +48584,7 @@ var VelaWorkspace = class {
47412
48584
  if (this.destroyed) return;
47413
48585
  this.flushPendingState();
47414
48586
  this.destroyed = true;
48587
+ this.replay.destroy();
47415
48588
  if (this.persistKey !== null && typeof window !== "undefined") window.removeEventListener("beforeunload", this.onUnload);
47416
48589
  this.resizeObserver?.disconnect();
47417
48590
  this.splitters.destroy();
@@ -48178,14 +49351,13 @@ var VelaWorkspace = class {
48178
49351
  panels: () => has("panels") ? [...this.dock.list()] : [],
48179
49352
  onTogglePanel: (id) => this.dock.toggle(id),
48180
49353
  ...has("alerts") ? { alerts: () => this.alerts.map((a) => ({ title: `${a.source} \xB7 ${a.title}`, message: a.message, time: a.time })) } : {},
48181
- // Left-aligned actions have their own bottom-bar stop — only the rest
48182
- // lands in the drawer, or every left action would appear twice. Built-in-id
48183
- // actions are slot OVERRIDES: they reach the drawer through the slot's own
48184
- // routed button (screenshot) or stop (indicators), never as an extra row.
48185
- actions: () => {
48186
- const builtin = new Set(TOPBAR_BUILTIN_IDS);
48187
- return widgetActions("topbar", this.context()).filter((a) => a.align !== "left" && !builtin.has(a.id)).map((a) => ({ label: a.label, icon: a.icon, run: () => a.run(this.context()) }));
48188
- },
49354
+ // Actions placed on the bottom bar (left-aligned by default) have their stop
49355
+ // there — only the menu-placed ones land in the drawer, or they would appear
49356
+ // twice: left (primary) ones with the primary rows, right ones at the end.
49357
+ // Built-in-id actions are slot OVERRIDES: they reach the drawer through the
49358
+ // slot's own routed button (screenshot) or stop (indicators), never as a row.
49359
+ primaryActions: () => this.drawerActions("left"),
49360
+ actions: () => this.drawerActions("right"),
48189
49361
  // The desktop layout dropdown's whole surface — the grid canvas, the
48190
49362
  // non-canvas presets and the sync switches — relocated into the kebab
48191
49363
  // drawer (the topbar is hidden on mobile). Same reads as the topbar block;
@@ -48210,6 +49382,11 @@ var VelaWorkspace = class {
48210
49382
  }));
48211
49383
  this.moreDrawer.open();
48212
49384
  }
49385
+ /** The contributed topbar actions the mobile menu lists, from one cluster. */
49386
+ drawerActions(cluster) {
49387
+ const builtin = new Set(TOPBAR_BUILTIN_IDS);
49388
+ return widgetActions("topbar", this.context()).filter((a) => mobilePlacement(a) === "menu" && a.align === "left" === (cluster === "left") && !builtin.has(a.id)).map((a) => ({ label: a.label, icon: a.icon, run: () => a.run(this.context()) }));
49389
+ }
48213
49390
  openTimezoneDrawer() {
48214
49391
  this.timezoneDrawer ?? (this.timezoneDrawer = new TimezoneDrawer({
48215
49392
  host: this.root,
@@ -48331,7 +49508,7 @@ var VelaWorkspace = class {
48331
49508
  }
48332
49509
  });
48333
49510
  }
48334
- /** Bare-typing router: letters → symbol search (seeded), digits → timeframe entry. */
49511
+ /** Bare-typing router: letters and `0` → symbol search (seeded), digits 1–9 → timeframe entry. */
48335
49512
  routeTyping(ev) {
48336
49513
  if (this.destroyed) return;
48337
49514
  if (ev.ctrlKey || ev.metaKey || ev.altKey) return;
@@ -48376,12 +49553,15 @@ var VelaWorkspace = class {
48376
49553
  exports.ChartCell = ChartCell;
48377
49554
  exports.GRID_PICKER_MAX = GRID_PICKER_MAX;
48378
49555
  exports.VelaWorkspace = VelaWorkspace;
49556
+ exports.WorkspaceReplay = WorkspaceReplay;
48379
49557
  exports.activeAfterLayout = activeAfterLayout;
49558
+ exports.barClose = barClose;
48380
49559
  exports.decodeState = decodeState;
48381
49560
  exports.encodeState = encodeState;
48382
49561
  exports.ensureLayout = ensureLayout;
48383
49562
  exports.evenTracks = evenTracks;
48384
49563
  exports.gridStyles = gridStyles;
49564
+ exports.lastOpenClosedBy = lastOpenClosedBy;
48385
49565
  exports.layoutDefinition = layoutDefinition;
48386
49566
  exports.layoutForGrid = layoutForGrid;
48387
49567
  exports.layoutShape = layoutShape;