@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.
- package/NOTICE +28 -28
- package/README.md +15 -7
- package/dist/{DataProvider-CGa4KHRa.d.cts → DataProvider-BrlxntPN.d.cts} +1 -1
- package/dist/{DataProvider-CmWUtDVL.d.ts → DataProvider-DwFIoV4r.d.ts} +1 -1
- package/dist/{chunk-N4ZZYLMP.js → chunk-ATGTCWHJ.js} +726 -273
- package/dist/{chunk-CAFCLMPF.js → chunk-BZQM2XO7.js} +1 -0
- package/dist/{chunk-M6GP7DAX.js → chunk-DBDXKZUF.js} +474 -47
- package/dist/{chunk-BFA32GOU.js → chunk-EZ5FWVLA.js} +5 -2
- package/dist/{chunk-PVGWNF7V.js → chunk-LMBYEGUR.js} +2 -2
- package/dist/{chunk-WTWGEB7K.js → chunk-NELQJCGK.js} +298 -5
- package/dist/{contributions-DF_Ea07m.d.cts → contributions-CtlptxR8.d.cts} +187 -5
- package/dist/{contributions-nKOSeLy8.d.ts → contributions-tjNExo1o.d.ts} +187 -5
- package/dist/index.cjs +1044 -272
- package/dist/index.d.cts +24 -8
- package/dist/index.d.ts +24 -8
- package/dist/index.js +28 -4
- package/dist/{options-D1AJKsn_.d.ts → options-BnEPUuVV.d.cts} +1 -1
- package/dist/{options-D1AJKsn_.d.cts → options-BnEPUuVV.d.ts} +1 -1
- package/dist/{plugin-6F04gjQe.d.cts → plugin-BWxJRKzS.d.cts} +10 -4
- package/dist/{plugin-BLsNipkc.d.ts → plugin-e2WUEtiI.d.ts} +10 -4
- package/dist/plugin.cjs +1 -0
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- package/dist/plugin.js +2 -2
- package/dist/providers/binance.d.cts +2 -2
- package/dist/providers/binance.d.ts +2 -2
- package/dist/providers/coinbase.d.cts +2 -2
- package/dist/providers/coinbase.d.ts +2 -2
- package/dist/providers/hyperliquid.d.cts +2 -2
- package/dist/providers/hyperliquid.d.ts +2 -2
- package/dist/{statusline-model-DAFyqVmR.d.ts → statusline-model-DFY7tuHj.d.cts} +5 -3
- package/dist/{statusline-model-BoWJ3cWQ.d.cts → statusline-model-zKXtGIjj.d.ts} +5 -3
- package/dist/ui.cjs +301 -3
- package/dist/ui.d.cts +75 -2
- package/dist/ui.d.ts +75 -2
- package/dist/ui.js +3 -3
- package/dist/vela.global.js +1046 -274
- package/dist/vela.global.min.js +82 -87
- package/dist/widget.cjs +1504 -327
- package/dist/widget.d.cts +26 -7
- package/dist/widget.d.ts +26 -7
- package/dist/widget.js +7 -7
- package/dist/workspace.cjs +1507 -327
- package/dist/workspace.d.cts +125 -7
- package/dist/workspace.d.ts +125 -7
- package/dist/workspace.js +6 -6
- package/package.json +2 -2
package/dist/workspace.cjs
CHANGED
|
@@ -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("
|
|
3620
|
-
pctBox.
|
|
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.
|
|
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(
|
|
6041
|
-
this.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(
|
|
6058
|
-
const intoSecond = (
|
|
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
|
|
6065
|
-
for (const cb of this.subs) cb(
|
|
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", () =>
|
|
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,
|
|
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
|
|
15292
|
-
* indicators slot (call after
|
|
15293
|
-
*
|
|
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
|
|
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/
|
|
18022
|
-
var
|
|
18023
|
-
var
|
|
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://
|
|
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
|
-
/*
|
|
18034
|
-
|
|
18035
|
-
|
|
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:
|
|
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
|
-
/*
|
|
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:
|
|
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:
|
|
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:
|
|
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 =
|
|
18455
|
+
symbol.innerHTML = VELA_MARK_SVG;
|
|
18144
18456
|
const wordmark = doc.createElement("span");
|
|
18145
18457
|
wordmark.className = "vela-attr-wordmark";
|
|
18146
|
-
wordmark.innerHTML =
|
|
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
|
-
|
|
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
|
|
20670
|
-
if (
|
|
20671
|
-
this.contextEmitAt.set(id,
|
|
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
|
|
20678
|
-
if (cause === "tick" &&
|
|
20679
|
-
this.runEmitAt.set(id,
|
|
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) =>
|
|
22040
|
-
|
|
22041
|
-
|
|
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,
|
|
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 =
|
|
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,
|
|
28526
|
+
function countdownText(barOpen, barMs, now2) {
|
|
27823
28527
|
if (!(barMs > 0)) return null;
|
|
27824
|
-
const remaining = barOpen + barMs -
|
|
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 =
|
|
28640
|
-
|
|
28641
|
-
|
|
28642
|
-
|
|
28643
|
-
|
|
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
|
-
|
|
28646
|
-
|
|
28647
|
-
|
|
28648
|
-
|
|
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 =
|
|
28682
|
-
if (
|
|
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 = (
|
|
37050
|
-
const p = Math.min(1, (
|
|
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 = (
|
|
37067
|
-
this.modelAlpha = Math.min(1, (
|
|
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
|
|
38276
|
-
if (this.scene.marksFlash && this.scene.marksFlash.until <=
|
|
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 &&
|
|
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 <
|
|
38840
|
-
const x = this.coords.logicalToX(logical);
|
|
38841
|
-
if (!Number.isFinite(x)
|
|
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
|
-
|
|
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.
|
|
44105
|
-
this.
|
|
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(
|
|
44545
|
-
const within = (ws) => ws.find(([s, 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 >=
|
|
45354
|
+
return w.regular.some(([s]) => s >= now2 && s < ext[1]) ? "pre" : "post";
|
|
44551
45355
|
}
|
|
44552
|
-
if (isWeekday(
|
|
44553
|
-
const today = civilDate(
|
|
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(
|
|
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 >
|
|
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
|
|
44603
|
-
const range = { from:
|
|
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(
|
|
44616
|
-
const boundary = nextStatusBoundary(
|
|
44617
|
-
const delay = Math.min(boundary != null ? boundary -
|
|
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.
|
|
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
|
-
|
|
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
|
|
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:
|
|
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: "
|
|
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-
|
|
46625
|
-
if (/^[
|
|
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
|
-
//
|
|
48182
|
-
//
|
|
48183
|
-
//
|
|
48184
|
-
//
|
|
48185
|
-
|
|
48186
|
-
|
|
48187
|
-
|
|
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;
|